@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,191 @@
1
+ // The `RyuPlugin` host API — the contract a Ryu plugin's `activate()` receives.
2
+ //
3
+ // This is the SINGLE canonical home for the host API surface. The desktop
4
+ // extension host (proprietary, closed source) IMPLEMENTS this contract; the
5
+ // types ship here in the OSS `@ryuhq/sdk` so plugins stay buildable against a
6
+ // stable, open contract (the open-core invariant: the host is closed, the
7
+ // contract is open). See `docs/desktop-extension-host-spec.md`.
8
+ //
9
+ // Design notes:
10
+ // - Every `register*()` returns a `Disposable` (the VS Code pattern). A plugin
11
+ // collects them in `context.subscriptions`; `deactivate()` disposes all, so
12
+ // disable/uninstall is leak-free — no dangling routes/commands/panels.
13
+ // - This file is TYPES + factory shape ONLY. It imports NOTHING (not even React)
14
+ // and has no runtime side effects, so it is safe in the OSS SDK, when installed
15
+ // standalone (`@ryuhq/sdk` is published), and in any bundler. The desktop host
16
+ // provides the concrete `RyuPlugin` instance.
17
+ // - UI contributions are declared DECLARATIVELY here (a route path/title + a
18
+ // sandboxed-webview `entry`). The OSS contract stays framework-agnostic: it does
19
+ // NOT carry a React component type. Trusted, in-process React components are a
20
+ // desktop-HOST concern (the host's own registry owns the `(tab) => ReactNode`
21
+ // render-fn, see `apps/desktop/src/contributions/registry.ts`), not the public
22
+ // contract. WHERE plugin UI renders (trusted host registry vs sandboxed child
23
+ // webview) is the host's decision per the three-tier UI model in the spec.
24
+
25
+ /** A handle that undoes a registration. Idempotent: calling `dispose()` twice is
26
+ * a no-op. */
27
+ export interface Disposable {
28
+ dispose(): void;
29
+ }
30
+
31
+ /** Build a {@link Disposable} from a teardown function. */
32
+ export function toDisposable(teardown: () => void): Disposable {
33
+ let done = false;
34
+ return {
35
+ dispose() {
36
+ if (done) {
37
+ return;
38
+ }
39
+ done = true;
40
+ teardown();
41
+ },
42
+ };
43
+ }
44
+
45
+ // ── Contribution descriptors ──────────────────────────────────────────────────
46
+
47
+ /** A tab/route a plugin adds. `path` is matched against `tab.path` (exact match
48
+ * unless `pattern` is set). The host loads `webview.entry` in a sandboxed
49
+ * surface as the tab body.
50
+ *
51
+ * Note: the OSS contract is declarative + sandboxed-webview only. A trusted,
52
+ * in-process React component is a desktop-host capability (the host's registry
53
+ * accepts a `(tab) => ReactNode` render-fn for BUILT-INS and first-party bundled
54
+ * plugins); it is intentionally absent from this framework-agnostic contract. */
55
+ export interface RouteContribution {
56
+ /** Exact path (e.g. "/my-plugin") or, with `pattern: true`, a RegExp source
57
+ * string matched against the full path (e.g. "^/my-plugin/[^/]+$"). */
58
+ path: string;
59
+ /** Treat `path` as a RegExp source string rather than an exact match. */
60
+ pattern?: boolean;
61
+ /** Human title (tab label, command-palette entry). */
62
+ title: string;
63
+ /** Load plugin UI in a SANDBOXED webview with NO Tauri IPC; the plugin reaches
64
+ * capabilities only over the host RPC bridge. */
65
+ webview: { entry: string };
66
+ }
67
+
68
+ /** A named mount region a panel can target. Generalizes the desktop's existing
69
+ * per-section slots (sidebar sections, settings tabs, the chat side-panel, the
70
+ * companion overlay) into declared contribution points. */
71
+ export type PanelRegion =
72
+ | "sidebar-section"
73
+ | "settings-tab"
74
+ | "chat-side-panel"
75
+ | "companion-overlay";
76
+
77
+ export interface PanelContribution {
78
+ /** Stable id within the region (also the host's key). */
79
+ id: string;
80
+ region: PanelRegion;
81
+ title: string;
82
+ webview: { entry: string };
83
+ }
84
+
85
+ /** A command-palette entry. Shaped to map 1:1 onto the desktop's
86
+ * `CommandAction` (from `@ryu/command/types`) so a contributed command lands in
87
+ * the same palette as built-ins with no shim. */
88
+ export interface CommandContribution {
89
+ /** Group heading in the palette. Defaults to the plugin's display name. */
90
+ group?: string;
91
+ id: string;
92
+ /** Extra fuzzy-search terms. */
93
+ keywords?: string;
94
+ /** The side effect. Runs in the host; for sandboxed plugins it is an RPC. */
95
+ run(): void | Promise<void>;
96
+ /** Right-aligned keyboard hint (e.g. "⌘⇧P"). */
97
+ shortcut?: string;
98
+ title: string;
99
+ }
100
+
101
+ export interface SettingsSectionContribution {
102
+ id: string;
103
+ title: string;
104
+ webview: { entry: string };
105
+ }
106
+
107
+ /** A section in the unified Store/marketplace surface. */
108
+ export interface StoreSectionContribution {
109
+ id: string;
110
+ title: string;
111
+ webview: { entry: string };
112
+ }
113
+
114
+ /** A theme a plugin contributes (CSS custom-property overrides keyed by token). */
115
+ export interface ThemeContribution {
116
+ id: string;
117
+ name: string;
118
+ tokens: Record<string, string>;
119
+ }
120
+
121
+ // ── Host services (proxied to Core over the host RPC) ─────────────────────────
122
+
123
+ /** The host-service surface a plugin calls back into. Each method is mediated by
124
+ * the host and grant-gated by the manifest (#443). For a sandboxed plugin these
125
+ * are RPCs over the postMessage bridge; the plugin never holds a Core token or
126
+ * Tauri IPC handle directly. */
127
+ export interface RyuHostServices {
128
+ /** Run a registered command by id (built-in or contributed). */
129
+ commands: { execute(id: string, ...args: unknown[]): Promise<unknown> };
130
+ /** Gateway-governed model access (chat/embed). Mirrors `@ryuhq/sdk` model
131
+ * client semantics; every call still routes through the Gateway. */
132
+ gateway: {
133
+ chat(
134
+ model: string,
135
+ messages: { role: string; content: string }[]
136
+ ): Promise<string>;
137
+ };
138
+ /** List the agents on the active node, PROJECTED to `{id,name}` only. The host
139
+ * holds the Core token and performs the fetch; the plugin never sees a token
140
+ * or any other agent field (invariant: no capability returns a secret). Gated
141
+ * by the `core:list_agents` grant. */
142
+ listAgents(): Promise<{ id: string; name: string }[]>;
143
+ /** Open a tab at a path (built-in or a route this plugin contributed). */
144
+ openTab(path: string): void;
145
+ /** Read/write the plugin's own Spaces docs (scoped by grant). */
146
+ spaces: {
147
+ ingestDocument(
148
+ spaceId: string,
149
+ title: string,
150
+ markdown: string
151
+ ): Promise<{ docId: string }>;
152
+ };
153
+ }
154
+
155
+ // ── The host API a plugin's activate() receives ───────────────────────────────
156
+
157
+ /** Everything a plugin can contribute. Each `register*` returns a
158
+ * {@link Disposable}; collect them in {@link PluginContext.subscriptions}. */
159
+ export interface RyuPlugin {
160
+ /** Host services the plugin calls back into (grant-gated). */
161
+ readonly host: RyuHostServices;
162
+ registerCommand(contribution: CommandContribution): Disposable;
163
+ registerPanel(contribution: PanelContribution): Disposable;
164
+ registerRoute(contribution: RouteContribution): Disposable;
165
+ registerSettingsSection(
166
+ contribution: SettingsSectionContribution
167
+ ): Disposable;
168
+ registerStoreSection(contribution: StoreSectionContribution): Disposable;
169
+ registerTheme(contribution: ThemeContribution): Disposable;
170
+ }
171
+
172
+ /** Passed to `activate(context)`. The plugin pushes its disposables onto
173
+ * `subscriptions`; the host disposes them all on `deactivate`. */
174
+ export interface PluginContext {
175
+ readonly plugin: RyuPlugin;
176
+ /** The plugin's own id (from `plugin.json`). */
177
+ readonly pluginId: string;
178
+ /** Disposables auto-cleaned on deactivate. */
179
+ readonly subscriptions: Disposable[];
180
+ }
181
+
182
+ /** The shape the host expects a plugin's entry module to export. */
183
+ export interface RyuPluginModule {
184
+ activate(context: PluginContext): void | Promise<void>;
185
+ deactivate?(): void | Promise<void>;
186
+ }
187
+
188
+ /** Identity helper for authoring a typed plugin module (no runtime behavior). */
189
+ export function definePlugin(plugin: RyuPluginModule): RyuPluginModule {
190
+ return plugin;
191
+ }
@@ -0,0 +1,338 @@
1
+ /**
2
+ * defineAgent — factory for Runnable agents.
3
+ *
4
+ * An agent is a Runnable that drives a multi-turn model loop. It may reference a
5
+ * workflow as a named tool by including a Runnable with kind="workflow" in its
6
+ * `tools` list; the agent's run() implementation calls it like any other tool.
7
+ *
8
+ * All model calls must go through `ctx.gateway` — no direct provider imports.
9
+ *
10
+ * ## Composable primitive slots (the "Pokémon card" model, program §6b)
11
+ *
12
+ * `defineAgent` additionally accepts swappable-provider SLOTS —
13
+ * `defineAgent({ chat, rag, memory, tools, tts, stt })` — where each slot picks
14
+ * a provider for one attribute of the card. They **lower** to the manifest:
15
+ * - `chat` → the agent `RunnableMeta.config` (model / engine / persona);
16
+ * - `rag` / `memory` / `tts` / `stt` → `requires.capabilities` edges the
17
+ * capability broker binds to a provider (with an optional explicit override);
18
+ * - `tools` → the tool ids the agent exposes.
19
+ *
20
+ * The slots are ADDITIVE: the classic `defineAgent({ id, name, run })` signature
21
+ * is unchanged. When `run` is omitted, a thin default run drives the `chat` slot
22
+ * through `ctx.gateway` (never a direct provider).
23
+ */
24
+
25
+ import type {
26
+ CapabilityReq,
27
+ PluginManifest,
28
+ RunnableMeta,
29
+ } from "../manifest.ts";
30
+ import { PluginManifestSchema } from "../manifest.ts";
31
+ import type { Runnable, RunnableContext } from "./runnable-types.ts";
32
+
33
+ /**
34
+ * A capability-backed slot (rag / memory / tts / stt). Written as:
35
+ * - `true` — require the capability; the broker/registry picks the provider;
36
+ * - `"com.acme.graphrag"` — bind this explicit provider app id;
37
+ * - `{ provider?, minVersion? }` — provider override and/or a version floor.
38
+ */
39
+ export type CapabilitySlot =
40
+ | boolean
41
+ | string
42
+ | { provider?: string; minVersion?: string };
43
+
44
+ /**
45
+ * The chat/model slot — the agent's own, swappable model config. Written as a
46
+ * model-id string shorthand or the full object. Every field is a `string`; no
47
+ * provider union, so a new provider never needs an SDK change.
48
+ */
49
+ export type ChatSlot =
50
+ | string
51
+ | {
52
+ /** Model id (swappable). */
53
+ model?: string;
54
+ /** Engine id (e.g. `"llamacpp"`, `"openai-compat"`). */
55
+ engine?: string;
56
+ /** System persona / instructions. */
57
+ persona?: string;
58
+ /** Preference key Core resolves to a model id (swappable, not hardcoded). */
59
+ modelPrefKey?: string;
60
+ };
61
+
62
+ /** The composable slots an agent card declares. All optional. */
63
+ export interface AgentSlots {
64
+ /** The chat/model slot (model + engine + persona). */
65
+ chat?: ChatSlot;
66
+ /** Memory provider slot → `requires.capabilities: [{ capability: "memory" }]`. */
67
+ memory?: CapabilitySlot;
68
+ /** RAG provider slot → `requires.capabilities: [{ capability: "rag" }]`. */
69
+ rag?: CapabilitySlot;
70
+ /** STT provider slot → `requires.capabilities: [{ capability: "stt" }]`. */
71
+ stt?: CapabilitySlot;
72
+ /** Tools the agent exposes — Runnables (workflows/tools) or bare tool ids. */
73
+ tools?: readonly (Runnable | string)[];
74
+ /** TTS provider slot → `requires.capabilities: [{ capability: "tts" }]`. */
75
+ tts?: CapabilitySlot;
76
+ }
77
+
78
+ /** Options accepted by `defineAgent`. */
79
+ export interface AgentOptions<TInput, TOutput> extends AgentSlots {
80
+ /** Stable unique identifier (e.g. "agent-researcher"). */
81
+ id: string;
82
+ /** Human-readable display name. */
83
+ name: string;
84
+ /**
85
+ * The agent's run implementation. OPTIONAL when slots are declared — a
86
+ * `chat`-slot default is synthesized (drives `ctx.gateway`). All model calls
87
+ * MUST go through `ctx.gateway`.
88
+ */
89
+ run?(input: TInput, ctx: RunnableContext): Promise<TOutput>;
90
+ }
91
+
92
+ /**
93
+ * The lowered "card": the swappable slots resolved to manifest-ready pieces —
94
+ * the persona/model config plus the `requires.capabilities` edges. This is what
95
+ * makes the Pokémon-card model literal in code.
96
+ */
97
+ export interface AgentCard {
98
+ /** Lowered capability edges for `requires.capabilities`. */
99
+ capabilities: CapabilityReq[];
100
+ /** Engine id from the `chat` slot. */
101
+ engine?: string;
102
+ /** Model id from the `chat` slot. */
103
+ model?: string;
104
+ /** Preference key from the `chat` slot. */
105
+ modelPrefKey?: string;
106
+ /** Persona/instructions from the `chat` slot. */
107
+ persona?: string;
108
+ /** Explicit provider bindings by capability (a slot that named a provider). */
109
+ providers: Record<string, string>;
110
+ /** Tool ids the agent exposes. */
111
+ tools: string[];
112
+ }
113
+
114
+ /** A defined agent: a Runnable plus its lowered card + a manifest lowering. */
115
+ export interface AgentRunnable<TInput = unknown, TOutput = unknown>
116
+ extends Runnable<TInput, TOutput> {
117
+ /** The lowered slot card (empty edges when no slots were declared). */
118
+ readonly card: AgentCard;
119
+ /**
120
+ * Lower this agent (card + run identity) to a single-agent `plugin.json`
121
+ * `PluginManifest`: the agent `RunnableMeta` carries the persona/model
122
+ * config; `requires.capabilities` carries the slot edges. Throws if the
123
+ * assembled manifest is invalid.
124
+ */
125
+ toManifest(options: AgentManifestOptions): PluginManifest;
126
+ }
127
+
128
+ /** Options for {@link AgentRunnable.toManifest}. */
129
+ export interface AgentManifestOptions {
130
+ /** Extra permission grants beyond those implied by slots. */
131
+ grants?: string[];
132
+ /** Reverse-domain plugin id (e.g. `"com.acme.researcher"`). */
133
+ id: string;
134
+ /** Display name (defaults to the agent's name). */
135
+ name?: string;
136
+ /** Semver version (e.g. `"1.0.0"`). */
137
+ version: string;
138
+ }
139
+
140
+ const CAPABILITY_SLOTS = ["rag", "memory", "tts", "stt"] as const;
141
+
142
+ /** Normalize a chat slot to the card's model fields. */
143
+ function lowerChatSlot(
144
+ chat: ChatSlot | undefined
145
+ ): Pick<AgentCard, "model" | "engine" | "persona" | "modelPrefKey"> {
146
+ if (chat === undefined) {
147
+ return {};
148
+ }
149
+ if (typeof chat === "string") {
150
+ return { model: chat };
151
+ }
152
+ return {
153
+ model: chat.model,
154
+ engine: chat.engine,
155
+ persona: chat.persona,
156
+ modelPrefKey: chat.modelPrefKey,
157
+ };
158
+ }
159
+
160
+ /** Lower one capability slot to an edge + optional provider override. */
161
+ function lowerCapabilitySlot(
162
+ capability: string,
163
+ slot: CapabilitySlot | undefined,
164
+ card: AgentCard
165
+ ): void {
166
+ if (slot === undefined || slot === false) {
167
+ return;
168
+ }
169
+ if (slot === true) {
170
+ card.capabilities.push({ capability });
171
+ return;
172
+ }
173
+ if (typeof slot === "string") {
174
+ card.capabilities.push({ capability });
175
+ card.providers[capability] = slot;
176
+ return;
177
+ }
178
+ card.capabilities.push(
179
+ slot.minVersion
180
+ ? { capability, min_version: slot.minVersion }
181
+ : { capability }
182
+ );
183
+ if (slot.provider) {
184
+ card.providers[capability] = slot.provider;
185
+ }
186
+ }
187
+
188
+ /** Build the lowered {@link AgentCard} from an agent's slots. */
189
+ function lowerSlots(options: AgentSlots): AgentCard {
190
+ const card: AgentCard = {
191
+ ...lowerChatSlot(options.chat),
192
+ capabilities: [],
193
+ providers: {},
194
+ tools: [],
195
+ };
196
+ for (const capability of CAPABILITY_SLOTS) {
197
+ lowerCapabilitySlot(capability, options[capability], card);
198
+ }
199
+ for (const t of options.tools ?? []) {
200
+ card.tools.push(typeof t === "string" ? t : t.id);
201
+ }
202
+ return card;
203
+ }
204
+
205
+ /** Synthesize a default run that drives the `chat` slot through `ctx.gateway`. */
206
+ function defaultRun(card: AgentCard) {
207
+ return async (input: unknown, ctx: RunnableContext): Promise<string> => {
208
+ const content =
209
+ typeof input === "string" ? input : JSON.stringify(input ?? "");
210
+ const messages = card.persona
211
+ ? [
212
+ { role: "system" as const, content: card.persona },
213
+ { role: "user" as const, content },
214
+ ]
215
+ : [{ role: "user" as const, content }];
216
+ const result = await ctx.gateway.chat(messages);
217
+ return result.content;
218
+ };
219
+ }
220
+
221
+ /** The manifest `config` block a card lowers to (kept opaque by Core). */
222
+ function cardConfig(card: AgentCard): Record<string, unknown> {
223
+ const config: Record<string, unknown> = {};
224
+ if (card.model) {
225
+ config.model = card.model;
226
+ }
227
+ if (card.engine) {
228
+ config.engine = card.engine;
229
+ }
230
+ if (card.persona) {
231
+ config.persona = card.persona;
232
+ }
233
+ if (card.modelPrefKey) {
234
+ config.model_pref_key = card.modelPrefKey;
235
+ }
236
+ if (Object.keys(card.providers).length > 0) {
237
+ config.capability_providers = card.providers;
238
+ }
239
+ if (card.tools.length > 0) {
240
+ config.tools = card.tools;
241
+ }
242
+ return config;
243
+ }
244
+
245
+ /** Lower an agent to a single-agent `plugin.json` `PluginManifest`. */
246
+ function agentToManifest(
247
+ agent: Runnable & { card: AgentCard },
248
+ options: AgentManifestOptions
249
+ ): PluginManifest {
250
+ const config = cardConfig(agent.card);
251
+ const meta: RunnableMeta = {
252
+ id: agent.id,
253
+ name: agent.name,
254
+ kind: "agent",
255
+ ...(Object.keys(config).length > 0 ? { config } : {}),
256
+ };
257
+ const hasRequires =
258
+ agent.card.capabilities.length > 0 || (options.grants?.length ?? 0) > 0;
259
+ const raw = {
260
+ id: options.id,
261
+ name: options.name ?? agent.name,
262
+ version: options.version,
263
+ runnables: [meta],
264
+ ...(hasRequires
265
+ ? {
266
+ requires: {
267
+ apps: [],
268
+ capabilities: agent.card.capabilities,
269
+ grants: options.grants ?? [],
270
+ },
271
+ }
272
+ : {}),
273
+ };
274
+ const result = PluginManifestSchema.safeParse(raw);
275
+ if (!result.success) {
276
+ const first = result.error.issues[0];
277
+ const field = first?.path.join(".") ?? "unknown";
278
+ const message = first?.message ?? "validation failed";
279
+ throw new Error(
280
+ `agent manifest validation failed at '${field}': ${message}`
281
+ );
282
+ }
283
+ return result.data;
284
+ }
285
+
286
+ /**
287
+ * Create a Runnable agent.
288
+ *
289
+ * The returned value satisfies `Runnable<TInput, TOutput>` with `kind = "agent"`
290
+ * and additionally exposes the lowered {@link AgentCard} + a `toManifest()`
291
+ * lowering, so a slot-composed agent round-trips to a valid `plugin.json`.
292
+ *
293
+ * @example Classic (unchanged, back-compat):
294
+ * ```ts
295
+ * const a = defineAgent({
296
+ * id: "agent-researcher",
297
+ * name: "Researcher",
298
+ * async run({ query }, ctx) {
299
+ * const r = await ctx.gateway.chat([{ role: "user", content: query }]);
300
+ * return { answer: r.content };
301
+ * },
302
+ * });
303
+ * ```
304
+ *
305
+ * @example Composable slots (the Pokémon card):
306
+ * ```ts
307
+ * const cmo = defineAgent({
308
+ * id: "agent-cmo",
309
+ * name: "CMO",
310
+ * chat: { model: "gpt-4o", persona: "You are a CMO." },
311
+ * rag: true, // requires.capabilities: [{ capability: "rag" }]
312
+ * memory: { minVersion: "1.2" },
313
+ * tts: "com.acme.elevenlabs", // explicit provider override
314
+ * });
315
+ * const manifest = cmo.toManifest({ id: "com.acme.cmo", version: "1.0.0" });
316
+ * ```
317
+ */
318
+ export function defineAgent<TInput = unknown, TOutput = unknown>(
319
+ options: AgentOptions<TInput, TOutput>
320
+ ): AgentRunnable<TInput, TOutput> {
321
+ const { id, name } = options;
322
+ const card = lowerSlots(options);
323
+ const run = (options.run ?? defaultRun(card)) as Runnable<
324
+ TInput,
325
+ TOutput
326
+ >["run"];
327
+
328
+ return {
329
+ id,
330
+ name,
331
+ kind: "agent",
332
+ run,
333
+ card,
334
+ toManifest(manifestOptions: AgentManifestOptions): PluginManifest {
335
+ return agentToManifest(this, manifestOptions);
336
+ },
337
+ };
338
+ }