@ryuhq/sdk 0.0.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +179 -0
- package/README.md +31 -0
- package/dist/agent.cjs +761 -0
- package/dist/agent.d.cts +3 -0
- package/dist/agent.d.ts +3 -0
- package/dist/agent.js +23 -0
- package/dist/chunk-GXHL5CO7.js +353 -0
- package/dist/chunk-KPKMMGVC.js +671 -0
- package/dist/chunk-ODFEUVPW.js +100 -0
- package/dist/cli.cjs +858 -0
- package/dist/cli.d.cts +1 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +454 -0
- package/dist/index-CEbS1SlS.d.cts +988 -0
- package/dist/index-DAxq7Y0R.d.ts +988 -0
- package/dist/index.cjs +1900 -0
- package/dist/index.d.cts +759 -0
- package/dist/index.d.ts +759 -0
- package/dist/index.js +771 -0
- package/dist/manifest.cjs +399 -0
- package/dist/manifest.d.cts +355 -0
- package/dist/manifest.d.ts +355 -0
- package/dist/manifest.js +38 -0
- package/package.json +56 -0
- package/src/agent/agent.ts +208 -0
- package/src/agent/index.ts +51 -0
- package/src/agent/loop.test.ts +261 -0
- package/src/agent/loop.ts +259 -0
- package/src/agent/model-call.ts +190 -0
- package/src/agent/query.ts +40 -0
- package/src/agent/tools.ts +295 -0
- package/src/builder.ts +473 -0
- package/src/cli/dev.test.ts +178 -0
- package/src/cli/dev.ts +425 -0
- package/src/cli.ts +390 -0
- package/src/contracts-lockstep.test.ts +77 -0
- package/src/generated/plugin-manifest.ts +1121 -0
- package/src/index.ts +141 -0
- package/src/manifest.test.ts +610 -0
- package/src/manifest.ts +589 -0
- package/src/mcp/bridge.test.ts +196 -0
- package/src/mcp/client.ts +253 -0
- package/src/mcp/fixture-server.ts +23 -0
- package/src/mcp/server.ts +351 -0
- package/src/model/client.test.ts +107 -0
- package/src/model/client.ts +179 -0
- package/src/model/gateway.ts +41 -0
- package/src/plugin/ryu-plugin.ts +191 -0
- package/src/runnable/agent.ts +338 -0
- package/src/runnable/app.ts +233 -0
- package/src/runnable/index.ts +61 -0
- package/src/runnable/primitives-hostapi.test.ts +73 -0
- package/src/runnable/primitives.test.ts +286 -0
- package/src/runnable/primitives.ts +610 -0
- package/src/runnable/runnable-types.ts +113 -0
- package/src/runnable/runnable.test.ts +397 -0
- package/src/runnable/skill.ts +60 -0
- package/src/runnable/tool.ts +260 -0
- package/src/runnable/turn-hook.test.ts +81 -0
- package/src/runnable/turn-hook.ts +191 -0
- package/src/runnable/workflow.ts +76 -0
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* defineTool — factory for Runnable tools.
|
|
3
|
+
*
|
|
4
|
+
* A tool is a stateless function invoked by an agent or workflow step.
|
|
5
|
+
* It accepts a typed schema (Zod-style field definitions) that is converted
|
|
6
|
+
* to a JSON Schema object compatible with Core's `ToolInfo.schema` shape
|
|
7
|
+
* (apps/core/src/sidecar/adapters/mod.rs:66-71).
|
|
8
|
+
*
|
|
9
|
+
* Input is validated against the schema at run() time; invalid input throws
|
|
10
|
+
* a descriptive Error before the tool body executes.
|
|
11
|
+
*
|
|
12
|
+
* Tools do NOT require model calls and therefore do not need `ctx.gateway`
|
|
13
|
+
* to be present, but the context is still injected so tools can optionally
|
|
14
|
+
* call gateway.chat() when they need model assistance.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import type { RunnableMeta } from "../manifest.ts";
|
|
18
|
+
import type { Runnable, RunnableContext } from "./runnable-types.ts";
|
|
19
|
+
|
|
20
|
+
// ── JSON Schema types ─────────────────────────────────────────────────────────
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* A single JSON Schema property descriptor — the subset required by Core's
|
|
24
|
+
* `ToolInfo.schema` field and the OpenAI function-calling format.
|
|
25
|
+
*/
|
|
26
|
+
export interface JsonSchemaProperty {
|
|
27
|
+
/** Human-readable description surfaced to the model. */
|
|
28
|
+
description?: string;
|
|
29
|
+
/** Allowed enum values. */
|
|
30
|
+
enum?: unknown[];
|
|
31
|
+
/** Array item schema (required when type is "array"). */
|
|
32
|
+
items?: JsonSchemaProperty;
|
|
33
|
+
/** Nested object properties (used when type is "object"). */
|
|
34
|
+
properties?: Record<string, JsonSchemaProperty>;
|
|
35
|
+
/** Required keys list (used when type is "object"). */
|
|
36
|
+
required?: string[];
|
|
37
|
+
/** JSON Schema type string. */
|
|
38
|
+
type: "string" | "number" | "integer" | "boolean" | "array" | "object";
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Zod-style schema descriptor for a tool's input.
|
|
43
|
+
*
|
|
44
|
+
* Keys are field names; values describe their JSON Schema shape. Required
|
|
45
|
+
* fields are listed separately under `required`.
|
|
46
|
+
*
|
|
47
|
+
* This intentionally mirrors the shape that `ToolInfo.schema` expects in
|
|
48
|
+
* `apps/core/src/sidecar/adapters/mod.rs` so a `defineTool` output can be
|
|
49
|
+
* forwarded to Core without transformation.
|
|
50
|
+
*/
|
|
51
|
+
export interface ToolSchema {
|
|
52
|
+
/** Field definitions. */
|
|
53
|
+
properties: Record<string, JsonSchemaProperty>;
|
|
54
|
+
/** Names of fields that must be present in the input. */
|
|
55
|
+
required?: string[];
|
|
56
|
+
/** Type is always "object" for a tool's top-level input schema. */
|
|
57
|
+
type: "object";
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// ── Options ───────────────────────────────────────────────────────────────────
|
|
61
|
+
|
|
62
|
+
/** Options accepted by `defineTool`. */
|
|
63
|
+
export interface ToolOptions<TInput extends Record<string, unknown>, TOutput> {
|
|
64
|
+
/** Stable unique identifier (e.g. "tool-web-search"). */
|
|
65
|
+
id: string;
|
|
66
|
+
/** Human-readable display name. */
|
|
67
|
+
name: string;
|
|
68
|
+
/**
|
|
69
|
+
* The tool's run implementation.
|
|
70
|
+
*
|
|
71
|
+
* Called only after input validation passes. Model calls are optional but
|
|
72
|
+
* must go through `ctx.gateway` if used.
|
|
73
|
+
*/
|
|
74
|
+
run(input: TInput, ctx: RunnableContext): Promise<TOutput>;
|
|
75
|
+
/** JSON Schema describing the tool's input — used for input validation and Core ToolInfo. */
|
|
76
|
+
schema: ToolSchema;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// ── Internal: input validation ────────────────────────────────────────────────
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Validate `input` against `schema`.
|
|
83
|
+
*
|
|
84
|
+
* Throws a descriptive `Error` naming the first failing field. This mirrors
|
|
85
|
+
* the validation behaviour of `PluginManifestSchema.safeParse` used elsewhere in
|
|
86
|
+
* the SDK — consistent error handling, no silent fallback.
|
|
87
|
+
*/
|
|
88
|
+
function validateInput(
|
|
89
|
+
input: unknown,
|
|
90
|
+
schema: ToolSchema,
|
|
91
|
+
toolId: string
|
|
92
|
+
): void {
|
|
93
|
+
if (typeof input !== "object" || input === null || Array.isArray(input)) {
|
|
94
|
+
throw new Error(
|
|
95
|
+
`[ryu-sdk] Tool "${toolId}" input must be an object, got ${JSON.stringify(input)}`
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const record = input as Record<string, unknown>;
|
|
100
|
+
|
|
101
|
+
for (const requiredKey of schema.required ?? []) {
|
|
102
|
+
if (!(requiredKey in record)) {
|
|
103
|
+
throw new Error(
|
|
104
|
+
`[ryu-sdk] Tool "${toolId}" input missing required field "${requiredKey}"`
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
for (const [key, prop] of Object.entries(schema.properties)) {
|
|
110
|
+
if (!(key in record)) {
|
|
111
|
+
continue; // optional field — skip
|
|
112
|
+
}
|
|
113
|
+
const value = record[key];
|
|
114
|
+
if (!checkType(value, prop.type)) {
|
|
115
|
+
throw new Error(
|
|
116
|
+
`[ryu-sdk] Tool "${toolId}" input field "${key}" expected type "${prop.type}", ` +
|
|
117
|
+
`got ${JSON.stringify(value)}`
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** Check that `value` matches the expected JSON Schema primitive type. */
|
|
124
|
+
function checkType(value: unknown, type: JsonSchemaProperty["type"]): boolean {
|
|
125
|
+
switch (type) {
|
|
126
|
+
case "string":
|
|
127
|
+
return typeof value === "string";
|
|
128
|
+
case "number":
|
|
129
|
+
case "integer":
|
|
130
|
+
return typeof value === "number";
|
|
131
|
+
case "boolean":
|
|
132
|
+
return typeof value === "boolean";
|
|
133
|
+
case "array":
|
|
134
|
+
return Array.isArray(value);
|
|
135
|
+
case "object":
|
|
136
|
+
return (
|
|
137
|
+
typeof value === "object" && value !== null && !Array.isArray(value)
|
|
138
|
+
);
|
|
139
|
+
default:
|
|
140
|
+
return false;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// ── ToolRunnable ──────────────────────────────────────────────────────────────
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* A `Runnable` with an extra `schema` field exposing the tool's JSON Schema.
|
|
148
|
+
*
|
|
149
|
+
* The `schema` is compatible with Core's `ToolInfo.schema` shape so it can
|
|
150
|
+
* be forwarded verbatim to Core's MCP/ACP layer.
|
|
151
|
+
*/
|
|
152
|
+
export interface ToolRunnable<
|
|
153
|
+
TInput extends Record<string, unknown> = Record<string, unknown>,
|
|
154
|
+
TOutput = unknown,
|
|
155
|
+
> extends Runnable<TInput, TOutput> {
|
|
156
|
+
readonly kind: "tool";
|
|
157
|
+
/** JSON Schema for this tool's input — compatible with Core's ToolInfo.schema. */
|
|
158
|
+
readonly schema: ToolSchema;
|
|
159
|
+
/**
|
|
160
|
+
* The `run` body serialized for Core's `inline_deno` tool backend — the exact
|
|
161
|
+
* same technique `defineTurnHook` uses for its `code`. This is what makes a
|
|
162
|
+
* `defineTool` **shippable**: bundled into a plugin manifest (see
|
|
163
|
+
* {@link inlineToolRunnable} / `definePlugin({ tools })`), Core runs it in the
|
|
164
|
+
* Deno sandbox, so the tool ships NEW behavior instead of only aliasing an
|
|
165
|
+
* existing tool.
|
|
166
|
+
*
|
|
167
|
+
* IMPORTANT: like a hook body, the serialized function is **self-contained** —
|
|
168
|
+
* it runs in a fresh sandbox with only `input` (the call arguments) and `host`
|
|
169
|
+
* (the capability bridge: `host.sideModel` / `host.storage` / `host.log`, each
|
|
170
|
+
* gated by the plugin's grants) in scope. It cannot capture outer variables,
|
|
171
|
+
* imports, or closures, and `ctx.gateway` is **not** available in the sandbox
|
|
172
|
+
* — a shipped tool reaches models through `host.sideModel`. When run in-process
|
|
173
|
+
* via {@link ToolRunnable.run} the normal `(input, ctx)` contract still holds;
|
|
174
|
+
* the sandbox form is the second parameter aliased to `host`.
|
|
175
|
+
*/
|
|
176
|
+
readonly code: string;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// ── Factory ───────────────────────────────────────────────────────────────────
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Create a Runnable tool with input validation.
|
|
183
|
+
*
|
|
184
|
+
* The returned value satisfies `ToolRunnable<TInput, TOutput>` (which extends
|
|
185
|
+
* `Runnable`) with `kind = "tool"`. The `schema` property is the JSON Schema
|
|
186
|
+
* descriptor passed in options — forwarding it to Core's `ToolInfo.schema`
|
|
187
|
+
* requires no transformation.
|
|
188
|
+
*
|
|
189
|
+
* Input is validated at `run()` time: missing required fields or type
|
|
190
|
+
* mismatches throw before the tool body executes.
|
|
191
|
+
*
|
|
192
|
+
* @example
|
|
193
|
+
* ```ts
|
|
194
|
+
* const searchTool = defineTool({
|
|
195
|
+
* id: "tool-web-search",
|
|
196
|
+
* name: "Web Search",
|
|
197
|
+
* schema: {
|
|
198
|
+
* type: "object",
|
|
199
|
+
* properties: { query: { type: "string", description: "Search query" } },
|
|
200
|
+
* required: ["query"],
|
|
201
|
+
* },
|
|
202
|
+
* async run({ query }, _ctx) {
|
|
203
|
+
* return { results: [`Result for: ${query}`] };
|
|
204
|
+
* },
|
|
205
|
+
* });
|
|
206
|
+
* // schema is Core-compatible:
|
|
207
|
+
* console.log(searchTool.schema); // { type: "object", properties: { query: ... }, required: [...] }
|
|
208
|
+
* ```
|
|
209
|
+
*/
|
|
210
|
+
export function defineTool<
|
|
211
|
+
TInput extends Record<string, unknown> = Record<string, unknown>,
|
|
212
|
+
TOutput = unknown,
|
|
213
|
+
>(options: ToolOptions<TInput, TOutput>): ToolRunnable<TInput, TOutput> {
|
|
214
|
+
const { id, name, schema, run } = options;
|
|
215
|
+
|
|
216
|
+
// Serialize the run body for Core's `inline_deno` backend — the same approach
|
|
217
|
+
// `defineTurnHook` uses: the sandbox wraps this in an async IIFE where `input`
|
|
218
|
+
// and `host` are in scope and a bare `return` reports the tool result.
|
|
219
|
+
const code = `return await (${run.toString()})(input, host);`;
|
|
220
|
+
|
|
221
|
+
return {
|
|
222
|
+
id,
|
|
223
|
+
name,
|
|
224
|
+
kind: "tool",
|
|
225
|
+
schema,
|
|
226
|
+
code,
|
|
227
|
+
run(input: TInput, ctx: RunnableContext): Promise<TOutput> {
|
|
228
|
+
validateInput(input, schema, id);
|
|
229
|
+
return run(input, ctx);
|
|
230
|
+
},
|
|
231
|
+
} satisfies ToolRunnable<TInput, TOutput>;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Convert a {@link ToolRunnable} into a `plugin.json` `kind:"tool"` runnable that
|
|
236
|
+
* ships its `run` body as Core's `inline_deno` backend. The emitted config
|
|
237
|
+
* mirrors Core's `ToolConfig` (`apps/core/src/plugin_manifest/schema.rs`):
|
|
238
|
+
* `{ slug, backend:"inline_deno", code, description?, input_schema }`. Core
|
|
239
|
+
* registers it as `app__<slug>` — discoverable via `/api/tools/search` and
|
|
240
|
+
* executed in the grant-gated sandbox.
|
|
241
|
+
*
|
|
242
|
+
* The plugin must declare the `tool:execute` grant (see `definePlugin`).
|
|
243
|
+
*/
|
|
244
|
+
export function inlineToolRunnable(
|
|
245
|
+
tool: ToolRunnable,
|
|
246
|
+
options?: { description?: string }
|
|
247
|
+
): RunnableMeta {
|
|
248
|
+
return {
|
|
249
|
+
id: tool.id,
|
|
250
|
+
name: tool.name,
|
|
251
|
+
kind: "tool",
|
|
252
|
+
config: {
|
|
253
|
+
slug: tool.id,
|
|
254
|
+
backend: "inline_deno",
|
|
255
|
+
code: tool.code,
|
|
256
|
+
input_schema: tool.schema,
|
|
257
|
+
...(options?.description ? { description: options.description } : {}),
|
|
258
|
+
},
|
|
259
|
+
};
|
|
260
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { describe, expect, it } from "bun:test";
|
|
2
|
+
import { PluginManifestSchema } from "../manifest.ts";
|
|
3
|
+
import {
|
|
4
|
+
definePlugin,
|
|
5
|
+
defineTurnHook,
|
|
6
|
+
type HookDirective,
|
|
7
|
+
} from "./turn-hook.ts";
|
|
8
|
+
|
|
9
|
+
describe("defineTurnHook", () => {
|
|
10
|
+
it("serializes the run function into sandbox code and defaults `on`", () => {
|
|
11
|
+
const hook = defineTurnHook({
|
|
12
|
+
id: "x.review",
|
|
13
|
+
run: (ctx) =>
|
|
14
|
+
({ kind: "note", text: `t:${ctx.transcript.length}` }) as HookDirective,
|
|
15
|
+
});
|
|
16
|
+
expect(hook.id).toBe("x.review");
|
|
17
|
+
expect(hook.on).toBe("post_assistant_turn");
|
|
18
|
+
// The code calls the serialized function with the sandbox globals.
|
|
19
|
+
expect(hook.code).toContain("(ctx, host)");
|
|
20
|
+
expect(hook.code.startsWith("return await (")).toBe(true);
|
|
21
|
+
expect(hook.code).toContain("kind");
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it("honors an explicit `on`", () => {
|
|
25
|
+
const hook = defineTurnHook({
|
|
26
|
+
id: "x.pre",
|
|
27
|
+
on: "pre_user_turn",
|
|
28
|
+
run: () => ({ kind: "none" }),
|
|
29
|
+
});
|
|
30
|
+
expect(hook.on).toBe("pre_user_turn");
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
describe("definePlugin", () => {
|
|
35
|
+
it("produces a manifest that matches the Core PluginManifest shape", () => {
|
|
36
|
+
const manifest = definePlugin({
|
|
37
|
+
id: "com.example.double-check",
|
|
38
|
+
name: "Example Double Check",
|
|
39
|
+
version: "1.0.0",
|
|
40
|
+
grants: ["hook:side-model"],
|
|
41
|
+
turnHooks: [
|
|
42
|
+
defineTurnHook({
|
|
43
|
+
id: "dc.review",
|
|
44
|
+
run: async (ctx, host) => {
|
|
45
|
+
const last = ctx.transcript.at(-1);
|
|
46
|
+
const review = await host.sideModel({
|
|
47
|
+
prompt: last ? last.content : "",
|
|
48
|
+
model_pref_key: "double-check-model",
|
|
49
|
+
});
|
|
50
|
+
return { kind: "note", text: review };
|
|
51
|
+
},
|
|
52
|
+
}),
|
|
53
|
+
],
|
|
54
|
+
composerControls: [
|
|
55
|
+
{ id: "dc.toggle", type: "toggle", flag: "com.example.double-check" },
|
|
56
|
+
],
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
// Validates against the SDK's zod schema (which mirrors Core's serde shape).
|
|
60
|
+
const parsed = PluginManifestSchema.parse(manifest);
|
|
61
|
+
expect(parsed.id).toBe("com.example.double-check");
|
|
62
|
+
expect(parsed.runnables).toEqual([]);
|
|
63
|
+
expect(parsed.activation_events).toEqual(["*"]);
|
|
64
|
+
expect(parsed.contributes?.turn_hooks).toHaveLength(1);
|
|
65
|
+
expect(parsed.contributes?.turn_hooks[0]?.id).toBe("dc.review");
|
|
66
|
+
expect(parsed.contributes?.composer_controls).toHaveLength(1);
|
|
67
|
+
expect(parsed.permission_grants).toContain("hook:side-model");
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("defaults empty contribution arrays and grants", () => {
|
|
71
|
+
const manifest = definePlugin({
|
|
72
|
+
id: "com.example.empty",
|
|
73
|
+
name: "Empty",
|
|
74
|
+
version: "0.1.0",
|
|
75
|
+
});
|
|
76
|
+
const parsed = PluginManifestSchema.parse(manifest);
|
|
77
|
+
expect(parsed.permission_grants).toEqual([]);
|
|
78
|
+
expect(parsed.contributes?.turn_hooks).toEqual([]);
|
|
79
|
+
expect(parsed.contributes?.slash_commands).toEqual([]);
|
|
80
|
+
});
|
|
81
|
+
});
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Turn-hook + plugin authoring factories.
|
|
3
|
+
*
|
|
4
|
+
* A turn hook is plugin-authored logic that runs after each assistant turn in
|
|
5
|
+
* Ryu's Core plugin sandbox (`apps/core/src/plugin_host/`). The hook reaches Core
|
|
6
|
+
* only through capability-gated `host` functions and returns a directive. This is
|
|
7
|
+
* what makes features like double-check and goal real, installable plugins.
|
|
8
|
+
*
|
|
9
|
+
* `defineTurnHook` serializes your typed `run(ctx, host)` function to the `code`
|
|
10
|
+
* string the sandbox executes. IMPORTANT: the function must be **self-contained**
|
|
11
|
+
* — it runs in a fresh sandbox with only `ctx` and `host` in scope, so it cannot
|
|
12
|
+
* capture outer variables, imports, or closures (same constraint as a Web Worker
|
|
13
|
+
* body). Reference only `ctx`, `host`, and language built-ins.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import type {
|
|
17
|
+
Contributes,
|
|
18
|
+
PluginManifest,
|
|
19
|
+
RunnableMeta,
|
|
20
|
+
Surface,
|
|
21
|
+
TurnHookContribution,
|
|
22
|
+
} from "../manifest.ts";
|
|
23
|
+
import type { DefineAppRequires } from "./app.ts";
|
|
24
|
+
import { inlineToolRunnable, type ToolRunnable } from "./tool.ts";
|
|
25
|
+
|
|
26
|
+
/** The context a `post_assistant_turn` hook receives. */
|
|
27
|
+
export interface HookContext {
|
|
28
|
+
/** The agent that produced the turn. */
|
|
29
|
+
agent_id?: string;
|
|
30
|
+
/** The conversation id (also the natural per-conversation storage key). */
|
|
31
|
+
conversation_id?: string;
|
|
32
|
+
/** Per-request plugin flags (e.g. a composer toggle): `{ "<pluginId>": true }`. */
|
|
33
|
+
flags: Record<string, boolean>;
|
|
34
|
+
/** Recent transcript (oldest → newest). */
|
|
35
|
+
transcript: Array<{ role: string; content: string }>;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Arguments to a `host.sideModel` call. */
|
|
39
|
+
export interface SideModelArgs {
|
|
40
|
+
/** Reasoning effort, forwarded when non-empty. */
|
|
41
|
+
effort?: string;
|
|
42
|
+
/** Explicit model id (wins over `model_pref_key`). */
|
|
43
|
+
model?: string;
|
|
44
|
+
/** A preference key Core resolves to a model id (swappable, not hardcoded). */
|
|
45
|
+
model_pref_key?: string;
|
|
46
|
+
/** The user prompt for the side model. Required. */
|
|
47
|
+
prompt: string;
|
|
48
|
+
/** Optional system prompt. */
|
|
49
|
+
system?: string;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** The capability bridge available to a hook (gated by manifest grants). */
|
|
53
|
+
export interface HostApi {
|
|
54
|
+
/** Captured logging. */
|
|
55
|
+
log(...args: unknown[]): void;
|
|
56
|
+
/** One non-streaming gateway completion. Grant: `hook:side-model`. */
|
|
57
|
+
sideModel(args: SideModelArgs): Promise<string>;
|
|
58
|
+
/** The plugin's own namespaced KV store. Grant: `storage:kv`. */
|
|
59
|
+
storage: {
|
|
60
|
+
get(key: string): Promise<string | null>;
|
|
61
|
+
set(key: string, value: unknown): Promise<boolean>;
|
|
62
|
+
delete(key: string): Promise<boolean>;
|
|
63
|
+
keys(): Promise<string[]>;
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** What a hook asks the chat path to do after the assistant turn. */
|
|
68
|
+
export type HookDirective =
|
|
69
|
+
| { kind: "none" }
|
|
70
|
+
| { kind: "note"; text: string }
|
|
71
|
+
| { kind: "continue"; text: string };
|
|
72
|
+
|
|
73
|
+
/** A typed hook implementation: `(ctx, host) => directive`. */
|
|
74
|
+
export type HookRun = (
|
|
75
|
+
ctx: HookContext,
|
|
76
|
+
host: HostApi
|
|
77
|
+
) => HookDirective | Promise<HookDirective>;
|
|
78
|
+
|
|
79
|
+
export interface DefineTurnHookOptions {
|
|
80
|
+
/** Stable id for this hook, unique within the plugin. */
|
|
81
|
+
id: string;
|
|
82
|
+
/** Turn boundary (default `"post_assistant_turn"`). */
|
|
83
|
+
on?: string;
|
|
84
|
+
/** The hook body. Must be self-contained (no captured variables). */
|
|
85
|
+
run: HookRun;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Build a turn-hook contribution from a typed `run` function. The function source
|
|
90
|
+
* is serialized into the sandbox `code` string and invoked with `ctx`/`host` at
|
|
91
|
+
* run time.
|
|
92
|
+
*/
|
|
93
|
+
export function defineTurnHook(
|
|
94
|
+
options: DefineTurnHookOptions
|
|
95
|
+
): TurnHookContribution {
|
|
96
|
+
const source = options.run.toString();
|
|
97
|
+
// The sandbox wraps `code` in an async IIFE where `ctx`/`host` are in scope
|
|
98
|
+
// and a bare `return` reports the directive — so call the serialized function
|
|
99
|
+
// with them and return its result.
|
|
100
|
+
const code = `return await (${source})(ctx, host);`;
|
|
101
|
+
return {
|
|
102
|
+
id: options.id,
|
|
103
|
+
on: options.on ?? "post_assistant_turn",
|
|
104
|
+
code,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export interface DefinePluginOptions {
|
|
109
|
+
/** Activation events (default `["*"]` — driven by the enabled flag). */
|
|
110
|
+
activationEvents?: string[];
|
|
111
|
+
/** Declarative composer widgets (toggle/chip), passed verbatim to the desktop. */
|
|
112
|
+
composerControls?: Record<string, unknown>[];
|
|
113
|
+
/** Capability grants the hooks need (e.g. `["hook:side-model", "storage:kv"]`). */
|
|
114
|
+
grants?: string[];
|
|
115
|
+
/** Reverse-domain id (e.g. `"com.example.my-plugin"`). */
|
|
116
|
+
id: string;
|
|
117
|
+
/** Display name. */
|
|
118
|
+
name: string;
|
|
119
|
+
/**
|
|
120
|
+
* Plugin-to-plugin dependencies. Core auto-enables them (in dependency order)
|
|
121
|
+
* before this plugin, and refuses to disable one while this plugin needs it.
|
|
122
|
+
* Omit for the common case — the key is then absent from the emitted manifest.
|
|
123
|
+
*/
|
|
124
|
+
requires?: DefineAppRequires;
|
|
125
|
+
/** Declarative settings tabs (model pickers, fields), passed verbatim. */
|
|
126
|
+
settingsTabs?: Record<string, unknown>[];
|
|
127
|
+
/** Declarative slash commands, passed verbatim. */
|
|
128
|
+
slashCommands?: Record<string, unknown>[];
|
|
129
|
+
/**
|
|
130
|
+
* Host surfaces this plugin runs on. **Omitted/empty = every surface** (the
|
|
131
|
+
* backward-compatible default); it never means "hidden".
|
|
132
|
+
*/
|
|
133
|
+
targets?: Surface[];
|
|
134
|
+
/**
|
|
135
|
+
* Inline tools the plugin ships — each a {@link ToolRunnable} from `defineTool`
|
|
136
|
+
* whose `run` body is bundled as Core's `inline_deno` backend (registered as
|
|
137
|
+
* `app__<tool.id>`). Shipping any tool auto-adds the `tool:execute` grant.
|
|
138
|
+
*/
|
|
139
|
+
tools?: ToolRunnable[];
|
|
140
|
+
/** Turn hooks the plugin contributes. */
|
|
141
|
+
turnHooks?: TurnHookContribution[];
|
|
142
|
+
/** Semver version (e.g. `"1.0.0"`). */
|
|
143
|
+
version: string;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Assemble a `plugin.json` manifest for a turn-hook plugin. The result matches
|
|
148
|
+
* Core's `PluginManifest` serde shape and can be written to disk or validated via
|
|
149
|
+
* `validateManifestStrict`.
|
|
150
|
+
*/
|
|
151
|
+
export function definePlugin(options: DefinePluginOptions): PluginManifest {
|
|
152
|
+
const contributes: Contributes = {
|
|
153
|
+
turn_hooks: options.turnHooks ?? [],
|
|
154
|
+
composer_controls: options.composerControls ?? [],
|
|
155
|
+
settings_tabs: options.settingsTabs ?? [],
|
|
156
|
+
slash_commands: options.slashCommands ?? [],
|
|
157
|
+
// A turn-hook plugin contributes no app widgets; the field is required on the
|
|
158
|
+
// resolved `Contributes` type (zod default applied), so set it explicitly.
|
|
159
|
+
widgets: [],
|
|
160
|
+
};
|
|
161
|
+
// Ship each inline tool as a `kind:"tool"` runnable (Core's `inline_deno`
|
|
162
|
+
// backend). Shipping tools requires the `tool:execute` grant; add it once.
|
|
163
|
+
const tools = options.tools ?? [];
|
|
164
|
+
const runnables: RunnableMeta[] = tools.map((t) => inlineToolRunnable(t));
|
|
165
|
+
const grants = new Set(options.grants ?? []);
|
|
166
|
+
if (tools.length > 0) {
|
|
167
|
+
grants.add("tool:execute");
|
|
168
|
+
}
|
|
169
|
+
return {
|
|
170
|
+
id: options.id,
|
|
171
|
+
name: options.name,
|
|
172
|
+
version: options.version,
|
|
173
|
+
runnables,
|
|
174
|
+
permission_grants: [...grants],
|
|
175
|
+
activation_events: options.activationEvents ?? ["*"],
|
|
176
|
+
contributes,
|
|
177
|
+
// Empty = EVERY surface (Core's backward-compatible default), never "hidden".
|
|
178
|
+
targets: options.targets ?? [],
|
|
179
|
+
// Absent (not `{apps:[],grants:[]}`) when undeclared, matching Core's
|
|
180
|
+
// `Option<Requires>` + `skip_serializing_if = "Option::is_none"`.
|
|
181
|
+
...(options.requires
|
|
182
|
+
? {
|
|
183
|
+
requires: {
|
|
184
|
+
apps: options.requires.apps ?? [],
|
|
185
|
+
capabilities: options.requires.capabilities ?? [],
|
|
186
|
+
grants: options.requires.grants ?? [],
|
|
187
|
+
},
|
|
188
|
+
}
|
|
189
|
+
: {}),
|
|
190
|
+
};
|
|
191
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* defineWorkflow — factory for Runnable workflows.
|
|
3
|
+
*
|
|
4
|
+
* A workflow orchestrates agents (and other Runnables) as sequential steps.
|
|
5
|
+
* It exposes the peer relationship described in packages/sdk/README.md §2:
|
|
6
|
+
* a workflow may list an agent as a step and call it via its run() method.
|
|
7
|
+
*
|
|
8
|
+
* All model calls must go through `ctx.gateway` — no direct provider imports.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type { Runnable, RunnableContext } from "./runnable-types.ts";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* A single step inside a workflow definition.
|
|
15
|
+
*
|
|
16
|
+
* A step is any `Runnable` — most commonly an agent, but may also be a tool
|
|
17
|
+
* or a nested workflow (allowing composition without a strict hierarchy).
|
|
18
|
+
*/
|
|
19
|
+
export type WorkflowStep<
|
|
20
|
+
TStepInput = unknown,
|
|
21
|
+
TStepOutput = unknown,
|
|
22
|
+
> = Runnable<TStepInput, TStepOutput>;
|
|
23
|
+
|
|
24
|
+
/** Options accepted by `defineWorkflow`. */
|
|
25
|
+
export interface WorkflowOptions<TInput, TOutput> {
|
|
26
|
+
/** Stable unique identifier (e.g. "workflow-report"). */
|
|
27
|
+
id: string;
|
|
28
|
+
/** Human-readable display name. */
|
|
29
|
+
name: string;
|
|
30
|
+
/**
|
|
31
|
+
* The workflow's run implementation.
|
|
32
|
+
*
|
|
33
|
+
* May call any step via `step.run(input, ctx)`. All model calls that
|
|
34
|
+
* steps make MUST go through `ctx.gateway`.
|
|
35
|
+
*/
|
|
36
|
+
run(input: TInput, ctx: RunnableContext): Promise<TOutput>;
|
|
37
|
+
/**
|
|
38
|
+
* Optional list of Runnables this workflow orchestrates as steps.
|
|
39
|
+
*
|
|
40
|
+
* An agent may be listed here so the workflow can invoke it by calling
|
|
41
|
+
* `step.run(input, ctx)` — this is the peer relationship described in
|
|
42
|
+
* packages/sdk/README.md §2.
|
|
43
|
+
*/
|
|
44
|
+
steps?: readonly Runnable[];
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Create a Runnable workflow.
|
|
49
|
+
*
|
|
50
|
+
* The returned value satisfies the `Runnable<TInput, TOutput>` interface with
|
|
51
|
+
* `kind = "workflow"`.
|
|
52
|
+
*
|
|
53
|
+
* @example
|
|
54
|
+
* ```ts
|
|
55
|
+
* const myWorkflow = defineWorkflow({
|
|
56
|
+
* id: "workflow-report",
|
|
57
|
+
* name: "Report Workflow",
|
|
58
|
+
* steps: [researchAgent],
|
|
59
|
+
* async run({ topic }, ctx) {
|
|
60
|
+
* const { answer } = await researchAgent.run({ query: topic }, ctx);
|
|
61
|
+
* return { report: answer };
|
|
62
|
+
* },
|
|
63
|
+
* });
|
|
64
|
+
* ```
|
|
65
|
+
*/
|
|
66
|
+
export function defineWorkflow<TInput = unknown, TOutput = unknown>(
|
|
67
|
+
options: WorkflowOptions<TInput, TOutput>
|
|
68
|
+
): Runnable<TInput, TOutput> {
|
|
69
|
+
const { id, name, run } = options;
|
|
70
|
+
return {
|
|
71
|
+
id,
|
|
72
|
+
name,
|
|
73
|
+
kind: "workflow",
|
|
74
|
+
run,
|
|
75
|
+
} satisfies Runnable<TInput, TOutput>;
|
|
76
|
+
}
|