Services
Expose typed, schema-validated methods that other extensions and the AI agent can call.
A service is a set of callable methods your extension exposes. Other extensions call them through the service broker, and the built-in AI agent can call them as tools. Services turn your extension into reusable infrastructure — an OCR engine, an FFmpeg wrapper, a YouTube downloader.
The contract-first pattern
Real service extensions (ffmpeg, youtube-tools) share a contract module between provider and consumers, so both sides agree on method names, inputs, and outputs at compile time.
1. Define the contract (shared)
// src/contract.ts
import * as v from "valibot";
import { defineServiceContract, method, type InferService } from "@kunkunsh/sdk/contract";
export const mathContract = defineServiceContract({
id: "com.you.math",
serviceName: "math",
description: "Basic math operations",
methods: {
add: method({
description: "Add two numbers",
input: v.object({ a: v.number(), b: v.number() }),
output: v.object({ result: v.number() }),
}),
},
});
export type MathService = InferService<typeof mathContract>;Export it from a dedicated subpath so consumers can import it:
{ "exports": { "./contract": { "types": "./dist/contract.d.ts", "default": "./dist/contract.js" } } }2. Implement it (provider)
implementService wires your handlers to the contract and validates input/output automatically:
// src/MathService.ts
import { implementService } from "@kunkunsh/sdk/runtime";
import { mathContract } from "./contract";
export default implementService(mathContract, {
async add({ a, b }) {
return { result: a + b };
},
async onInit() { /* open resources */ },
async onDestroy() { /* clean up */ },
});Build it with the service mode:
await Bun.build({
entrypoints: ["./src/MathService.ts"],
outdir: "./dist/node",
target: "node", format: "esm", minify: true,
plugins: [kunkunCommandPlugin({ mode: "service" }) as BunPlugin],
});3. Declare it in the manifest
The services[] array is what the store and the broker see (the methods mirror your contract; JSON schemas drive validation and AI tool descriptions):
{
"kunkun": {
"services": [
{
"name": "math",
"description": "Basic math operations",
"main": "dist/node/MathService.js",
"serviceMode": "node-headless",
"methods": [
{
"name": "add",
"description": "Add two numbers",
"inputSchema": { "type": "object", "properties": { "a": { "type": "number" }, "b": { "type": "number" } }, "required": ["a", "b"] },
"outputSchema": { "type": "object", "properties": { "result": { "type": "number" } } }
}
]
}
]
}
}Consuming a service
Three tiers, from loosest to safest:
Tier 1 — untyped (no imports):
import { getHostAPI } from "@kunkunsh/sdk/runtime";
const { result } = await getHostAPI().services.call("com.you.math", "math", "add", { a: 5, b: 3 });Tier 2 — compile-time types (zero runtime cost):
import { createServiceClient } from "@kunkunsh/sdk/runtime";
import type { MathService } from "com.you.math/contract";
const math = createServiceClient<{ math: MathService }>("com.you.math");
const { result } = await math.math.add({ a: 5, b: 3 }); // fully typedTier 3 — types + runtime validation (input validated before send, output after receive):
import { createValidatedServiceClient } from "@kunkunsh/sdk/runtime";
import { mathSchemas } from "com.you.math/contract";
const math = createValidatedServiceClient("com.you.math", mathSchemas);
const { result } = await math.math.add({ a: 5, b: 3 });Declare the dependency
Pre-approve a provider (so calls don't prompt) with serviceDependencies:
{
"kunkun": {
"serviceDependencies": [
{ "extensionIdentifier": "com.you.math", "service": "math" }
]
}
}Undeclared calls prompt the user for approval via the broker.
Permission composition
Service calls compose permissions so callers don't inherit the provider's private details:
- The provider supplies its own implementation permissions (e.g. FFmpeg's scoped
shellaccess to theffmpegbinary). - The caller supplies resource permissions for user-chosen inputs (e.g.
fs-read/fs-writefor the files being converted).
So video-processing can call ffmpeg.convertImage on a user-selected file without ever declaring shell access to ffmpeg. See Permissions → intersection.
AI agent bridging
Every service method is automatically registered as an AI tool named:
plugin__<extensionIdentifier>__<serviceName>__<methodName>Install a service extension and the agent can call it — a well-described OCR service means "extract the text from this image" just works.
Options & lifecycle
await math.math.add({ a: 1, b: 2 }); // uses default 30s timeout
// per-call: createServiceClient(id, { timeout: 5000 })- Default timeout 30s, max 10 minutes.
onInit/onDestroyhooks run when the service worker starts/stops (onDestroyhas a ~5s hard timeout).
Real examples
ffmpeg(provider) ↔video-processing(consumer) — image/video conversionyoutube-tools(provider) ↔youtube-podcast-generator(consumer) — metadata, transcripts, downloads