@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,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
|
+
}
|