@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.
Files changed (61) hide show
  1. package/LICENSE +179 -0
  2. package/README.md +31 -0
  3. package/dist/agent.cjs +761 -0
  4. package/dist/agent.d.cts +3 -0
  5. package/dist/agent.d.ts +3 -0
  6. package/dist/agent.js +23 -0
  7. package/dist/chunk-GXHL5CO7.js +353 -0
  8. package/dist/chunk-KPKMMGVC.js +671 -0
  9. package/dist/chunk-ODFEUVPW.js +100 -0
  10. package/dist/cli.cjs +858 -0
  11. package/dist/cli.d.cts +1 -0
  12. package/dist/cli.d.ts +1 -0
  13. package/dist/cli.js +454 -0
  14. package/dist/index-CEbS1SlS.d.cts +988 -0
  15. package/dist/index-DAxq7Y0R.d.ts +988 -0
  16. package/dist/index.cjs +1900 -0
  17. package/dist/index.d.cts +759 -0
  18. package/dist/index.d.ts +759 -0
  19. package/dist/index.js +771 -0
  20. package/dist/manifest.cjs +399 -0
  21. package/dist/manifest.d.cts +355 -0
  22. package/dist/manifest.d.ts +355 -0
  23. package/dist/manifest.js +38 -0
  24. package/package.json +56 -0
  25. package/src/agent/agent.ts +208 -0
  26. package/src/agent/index.ts +51 -0
  27. package/src/agent/loop.test.ts +261 -0
  28. package/src/agent/loop.ts +259 -0
  29. package/src/agent/model-call.ts +190 -0
  30. package/src/agent/query.ts +40 -0
  31. package/src/agent/tools.ts +295 -0
  32. package/src/builder.ts +473 -0
  33. package/src/cli/dev.test.ts +178 -0
  34. package/src/cli/dev.ts +425 -0
  35. package/src/cli.ts +390 -0
  36. package/src/contracts-lockstep.test.ts +77 -0
  37. package/src/generated/plugin-manifest.ts +1121 -0
  38. package/src/index.ts +141 -0
  39. package/src/manifest.test.ts +610 -0
  40. package/src/manifest.ts +589 -0
  41. package/src/mcp/bridge.test.ts +196 -0
  42. package/src/mcp/client.ts +253 -0
  43. package/src/mcp/fixture-server.ts +23 -0
  44. package/src/mcp/server.ts +351 -0
  45. package/src/model/client.test.ts +107 -0
  46. package/src/model/client.ts +179 -0
  47. package/src/model/gateway.ts +41 -0
  48. package/src/plugin/ryu-plugin.ts +191 -0
  49. package/src/runnable/agent.ts +338 -0
  50. package/src/runnable/app.ts +233 -0
  51. package/src/runnable/index.ts +61 -0
  52. package/src/runnable/primitives-hostapi.test.ts +73 -0
  53. package/src/runnable/primitives.test.ts +286 -0
  54. package/src/runnable/primitives.ts +610 -0
  55. package/src/runnable/runnable-types.ts +113 -0
  56. package/src/runnable/runnable.test.ts +397 -0
  57. package/src/runnable/skill.ts +60 -0
  58. package/src/runnable/tool.ts +260 -0
  59. package/src/runnable/turn-hook.test.ts +81 -0
  60. package/src/runnable/turn-hook.ts +191 -0
  61. 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
+ }