@ryuhq/sdk 0.2.0 → 0.2.3
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/README.md +51 -2
- package/dist/action.cjs +839 -0
- package/dist/action.d.cts +88 -0
- package/dist/action.d.ts +88 -0
- package/dist/action.js +8 -0
- package/dist/agent-plugin.d.cts +1 -1
- package/dist/agent-plugin.d.ts +1 -1
- package/dist/agent.cjs +7 -0
- package/dist/agent.d.cts +1 -1
- package/dist/agent.d.ts +1 -1
- package/dist/agent.js +2 -2
- package/dist/{app-DNaGmLVf.d.cts → app-B0Z9Ew_R.d.cts} +18 -6
- package/dist/{app-Bkw7LlCK.d.ts → app-C-BDJwfG.d.ts} +18 -6
- package/dist/builder.cjs +110 -11
- package/dist/builder.d.cts +7 -2
- package/dist/builder.d.ts +7 -2
- package/dist/builder.js +4 -3
- package/dist/chunk-4TPUZDTI.js +94 -0
- package/dist/{chunk-T5676WL2.js → chunk-BC3A7HMO.js} +1 -77
- package/dist/{chunk-IOLP5FFE.js → chunk-HLKJZAFK.js} +9 -2
- package/dist/{chunk-IEUQ3CDG.js → chunk-NZKVOSC2.js} +71 -5
- package/dist/chunk-QYFUNJOH.js +83 -0
- package/dist/{chunk-W3KPP4WN.js → chunk-SN2QBJUF.js} +20 -7
- package/dist/{chunk-ULSVL7EC.js → chunk-Z57QDDJR.js} +1 -1
- package/dist/{chunk-A3RGEPDG.js → chunk-ZTJWBRUL.js} +32 -0
- package/dist/cli.cjs +93 -8
- package/dist/cli.js +33 -8
- package/dist/index.cjs +297 -87
- package/dist/index.d.cts +10 -9
- package/dist/index.d.ts +10 -9
- package/dist/index.js +34 -19
- package/dist/manifest.cjs +76 -6
- package/dist/manifest.d.cts +157 -5
- package/dist/manifest.d.ts +157 -5
- package/dist/manifest.js +9 -1
- package/dist/mcp/server.d.cts +2 -1
- package/dist/mcp/server.d.ts +2 -1
- package/dist/runnable.cjs +279 -71
- package/dist/runnable.d.cts +6 -3
- package/dist/runnable.d.ts +6 -3
- package/dist/runnable.js +11 -5
- package/dist/{tool-DSx2bFx8.d.ts → tool-AjkdFvhE.d.ts} +54 -4
- package/dist/{tool-u-VR0fLF.d.cts → tool-CgzW92O_.d.cts} +54 -4
- package/package.json +11 -2
- package/src/agent-plugin.ts +1 -1
- package/src/builder.ts +9 -0
- package/src/cli-security.test.ts +109 -0
- package/src/cli.ts +43 -10
- package/src/contracts-lockstep.test.ts +16 -2
- package/src/exports-lockstep.test.ts +1 -0
- package/src/generated/plugin-manifest.ts +82 -5
- package/src/index.ts +18 -0
- package/src/manifest-schema.test.ts +1 -1
- package/src/manifest.fixtures.test.ts +9 -3
- package/src/manifest.test.ts +69 -0
- package/src/manifest.ts +137 -18
- package/src/mcp/server.ts +2 -1
- package/src/runnable/action.test.ts +128 -0
- package/src/runnable/action.ts +202 -0
- package/src/runnable/app.ts +45 -12
- package/src/runnable/index.ts +18 -3
- package/src/runnable/primitives.test.ts +34 -0
- package/src/runnable/primitives.ts +59 -0
- package/src/runnable/runnable-types.ts +3 -0
- package/src/runnable/tool.ts +35 -4
- package/src/runnable/turn-hook.ts +4 -2
- package/src/slash-command.test.ts +69 -0
package/src/runnable/app.ts
CHANGED
|
@@ -14,11 +14,13 @@
|
|
|
14
14
|
* - A **companion** tool (`accessible:true`) is a call target a mounted widget
|
|
15
15
|
* may invoke: it carries `widget_accessible:true` and gets no widget template.
|
|
16
16
|
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
* `
|
|
17
|
+
* A tool may also attach a `ToolRunnable`. When present, its self-contained run
|
|
18
|
+
* body is emitted as Core's grant-gated `inline_deno` backend, so the widget and
|
|
19
|
+
* the behavior ship in one bundle. Without a runnable, the tool remains the
|
|
20
|
+
* declarative pass-through form: the widget renders from
|
|
21
|
+
* `window.openai.toolInput`/`toolOutput` and Core echoes the validated arguments
|
|
22
|
+
* as `structuredContent`. `ryu pack` bundles the `uiEntry` source into the
|
|
23
|
+
* manifest's `ui_code`.
|
|
22
24
|
*/
|
|
23
25
|
|
|
24
26
|
import type {
|
|
@@ -26,11 +28,13 @@ import type {
|
|
|
26
28
|
PluginManifest,
|
|
27
29
|
Requires,
|
|
28
30
|
RunnableMeta,
|
|
31
|
+
SlashCommandContribution,
|
|
29
32
|
Surface,
|
|
30
33
|
ToolAppConfig,
|
|
31
34
|
WidgetContribution,
|
|
32
35
|
} from "../manifest.ts";
|
|
33
36
|
import { PluginManifestSchema } from "../manifest.ts";
|
|
37
|
+
import { inlineToolRunnable, type ToolRunnable } from "./tool.ts";
|
|
34
38
|
|
|
35
39
|
/** The default widget MIME dialect (mirrors Core `default_widget_mime`). */
|
|
36
40
|
const DEFAULT_APP_WIDGET_MIME = "text/html+skybridge";
|
|
@@ -59,6 +63,21 @@ function withWidgetRenderGrant(
|
|
|
59
63
|
return out;
|
|
60
64
|
}
|
|
61
65
|
|
|
66
|
+
/** Add the grant required for any executable inline tool body. */
|
|
67
|
+
function withToolExecuteGrant(
|
|
68
|
+
grants: readonly string[],
|
|
69
|
+
tools: readonly AppToolSpec[]
|
|
70
|
+
): string[] {
|
|
71
|
+
const out = [...grants];
|
|
72
|
+
if (
|
|
73
|
+
tools.some((tool) => tool.runnable !== undefined) &&
|
|
74
|
+
!out.includes("tool:execute")
|
|
75
|
+
) {
|
|
76
|
+
out.push("tool:execute");
|
|
77
|
+
}
|
|
78
|
+
return out;
|
|
79
|
+
}
|
|
80
|
+
|
|
62
81
|
/** One tool a Ryu App declares. */
|
|
63
82
|
export interface AppToolSpec {
|
|
64
83
|
/**
|
|
@@ -76,6 +95,13 @@ export interface AppToolSpec {
|
|
|
76
95
|
invoking?: string;
|
|
77
96
|
/** Tool name (unqualified). The wire id is `<server>.<name>`. */
|
|
78
97
|
name: string;
|
|
98
|
+
/**
|
|
99
|
+
* Optional executable body for this tool. Core receives it as an
|
|
100
|
+
* `inline_deno` runnable and runs it in the deny-by-default tool sandbox.
|
|
101
|
+
* The body must be self-contained; use the injected `host` capability surface
|
|
102
|
+
* for governed model calls and other platform primitives.
|
|
103
|
+
*/
|
|
104
|
+
runnable?: Pick<ToolRunnable, "code" | "id" | "name" | "schema">;
|
|
79
105
|
}
|
|
80
106
|
|
|
81
107
|
/**
|
|
@@ -116,6 +142,8 @@ export interface DefineAppOptions {
|
|
|
116
142
|
requires?: DefineAppRequires;
|
|
117
143
|
/** MCP server namespace for the tool ids. Defaults to `slug`. */
|
|
118
144
|
server?: string;
|
|
145
|
+
/** Slash commands this app exposes in the chat composer. */
|
|
146
|
+
slashCommands?: SlashCommandContribution[];
|
|
119
147
|
/**
|
|
120
148
|
* App slug — used to build the widget uri (`ui://widget/<slug>.html`) and, when
|
|
121
149
|
* `server` is omitted, the MCP server namespace that qualifies each tool id.
|
|
@@ -182,8 +210,12 @@ export function defineApp(options: DefineAppOptions): PluginManifest {
|
|
|
182
210
|
for (const spec of options.tools) {
|
|
183
211
|
const isRender = spec.accessible !== true;
|
|
184
212
|
const id = appToolId(server, spec.name);
|
|
213
|
+
const executable = spec.runnable
|
|
214
|
+
? inlineToolRunnable(spec.runnable)
|
|
215
|
+
: undefined;
|
|
185
216
|
|
|
186
|
-
const config: ToolAppConfig = {
|
|
217
|
+
const config: ToolAppConfig & Record<string, unknown> = {
|
|
218
|
+
...(executable?.config ?? {}),
|
|
187
219
|
slug: id,
|
|
188
220
|
description: spec.description,
|
|
189
221
|
widget: isRender,
|
|
@@ -220,7 +252,7 @@ export function defineApp(options: DefineAppOptions): PluginManifest {
|
|
|
220
252
|
hook_events: [],
|
|
221
253
|
composer_controls: [],
|
|
222
254
|
settings_tabs: [],
|
|
223
|
-
slash_commands: [],
|
|
255
|
+
slash_commands: options.slashCommands ?? [],
|
|
224
256
|
sidebar_sections: [],
|
|
225
257
|
sidebar_buttons: [],
|
|
226
258
|
dock_panels: [],
|
|
@@ -238,6 +270,7 @@ export function defineApp(options: DefineAppOptions): PluginManifest {
|
|
|
238
270
|
pi_extensions: [],
|
|
239
271
|
output_styles: [],
|
|
240
272
|
message_actions: [],
|
|
273
|
+
selection_actions: [],
|
|
241
274
|
widgets,
|
|
242
275
|
};
|
|
243
276
|
|
|
@@ -252,15 +285,15 @@ export function defineApp(options: DefineAppOptions): PluginManifest {
|
|
|
252
285
|
// Core gates widget promotion on declared-AND-enabled-AND-granted, and a
|
|
253
286
|
// missing grant fails as `DeniedNoGrant` — which is an `info!` log and
|
|
254
287
|
// nothing else. The widget silently renders as plain text, with no error
|
|
255
|
-
// in the UI and nothing pointing at the manifest.
|
|
256
|
-
// through `defineApp` hit that, because the only fix was a grant string
|
|
257
|
-
// the templates never mention and the builder never added; the one
|
|
258
|
-
// working example on disk hand-writes it.
|
|
288
|
+
// in the UI and nothing pointing at the manifest.
|
|
259
289
|
//
|
|
260
290
|
// Added only when there is a widget to render, and unioned rather than
|
|
261
291
|
// overwritten so an author's own `grants` list survives and re-declaring
|
|
262
292
|
// it is not an error.
|
|
263
|
-
permission_grants:
|
|
293
|
+
permission_grants: withToolExecuteGrant(
|
|
294
|
+
withWidgetRenderGrant(options.grants ?? [], widgets),
|
|
295
|
+
options.tools
|
|
296
|
+
),
|
|
264
297
|
activation_events: options.activationEvents ?? ["*"],
|
|
265
298
|
contributes,
|
|
266
299
|
// `targets: []` means EVERY surface, so an app that declares none is
|
package/src/runnable/index.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Runnable — the
|
|
3
|
-
* in the Ryu SDK.
|
|
2
|
+
* Runnable — the execution contract unifying Agent, Workflow, Tool, and Skill
|
|
3
|
+
* in the Ryu SDK. `defineAction` is the governed business-operation facade over
|
|
4
|
+
* the Tool kind.
|
|
4
5
|
*
|
|
5
6
|
* Design rules (from the M8 spike doc packages/sdk/README.md):
|
|
6
7
|
* - An agent may invoke a workflow as a named tool.
|
|
@@ -12,6 +13,14 @@
|
|
|
12
13
|
* import from `@ryuhq/sdk/runnable` as a single entry point.
|
|
13
14
|
*/
|
|
14
15
|
|
|
16
|
+
export type {
|
|
17
|
+
ActionAnnotations,
|
|
18
|
+
ActionEffect,
|
|
19
|
+
ActionManifestOptions,
|
|
20
|
+
ActionOptions,
|
|
21
|
+
ActionRunnable,
|
|
22
|
+
} from "./action.ts";
|
|
23
|
+
export { defineAction } from "./action.ts";
|
|
15
24
|
export type {
|
|
16
25
|
AgentCard,
|
|
17
26
|
AgentManifestOptions,
|
|
@@ -42,6 +51,7 @@ export type {
|
|
|
42
51
|
RealtimeClient,
|
|
43
52
|
RealtimeSubscription,
|
|
44
53
|
RyuPrimitives,
|
|
54
|
+
StorageClient,
|
|
45
55
|
SttClient,
|
|
46
56
|
TtsClient,
|
|
47
57
|
} from "./primitives.ts";
|
|
@@ -57,7 +67,12 @@ export type {
|
|
|
57
67
|
} from "./runnable-types.ts";
|
|
58
68
|
export type { SkillOptions } from "./skill.ts";
|
|
59
69
|
export { defineSkill } from "./skill.ts";
|
|
60
|
-
export type {
|
|
70
|
+
export type {
|
|
71
|
+
InlineToolManifestOptions,
|
|
72
|
+
JsonSchemaProperty,
|
|
73
|
+
ToolOptions,
|
|
74
|
+
ToolSchema,
|
|
75
|
+
} from "./tool.ts";
|
|
61
76
|
export { defineTool } from "./tool.ts";
|
|
62
77
|
export type { WorkflowOptions, WorkflowStep } from "./workflow.ts";
|
|
63
78
|
export { defineWorkflow } from "./workflow.ts";
|
|
@@ -158,6 +158,35 @@ describe("createPrimitives — transport routing mirrors rpc.ts", () => {
|
|
|
158
158
|
expect(ragCall.body).toMatchObject({ op: "retrieve" });
|
|
159
159
|
});
|
|
160
160
|
|
|
161
|
+
it("storage primitives use the grant-gated app-owned KV bridge", async () => {
|
|
162
|
+
const { transport, calls } = recordingTransport();
|
|
163
|
+
const p = createPrimitives(transport);
|
|
164
|
+
await p.storage.get({ namespace: "tickets", key: "t-1" });
|
|
165
|
+
await p.storage.set({ namespace: "tickets", key: "t-1", value: "open" });
|
|
166
|
+
await p.storage.delete({ namespace: "tickets", key: "t-1" });
|
|
167
|
+
await p.storage.keys({ namespace: "tickets" });
|
|
168
|
+
await p.storage.compareAndSet({
|
|
169
|
+
namespace: "tickets",
|
|
170
|
+
key: "t-1",
|
|
171
|
+
expected: null,
|
|
172
|
+
value: "open",
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
expect(calls.map((c) => c.target)).toEqual([
|
|
176
|
+
"storage.get",
|
|
177
|
+
"storage.set",
|
|
178
|
+
"storage.delete",
|
|
179
|
+
"storage.keys",
|
|
180
|
+
"storage.compareAndSet",
|
|
181
|
+
]);
|
|
182
|
+
expect(calls.every((c) => c.op === "bridge")).toBe(true);
|
|
183
|
+
expect(calls[1]?.body).toEqual({
|
|
184
|
+
namespace: "tickets",
|
|
185
|
+
key: "t-1",
|
|
186
|
+
value: "open",
|
|
187
|
+
});
|
|
188
|
+
});
|
|
189
|
+
|
|
161
190
|
it("every declared primitive has a binding entry (no silent drift)", () => {
|
|
162
191
|
const expected = [
|
|
163
192
|
"engines.complete",
|
|
@@ -174,6 +203,11 @@ describe("createPrimitives — transport routing mirrors rpc.ts", () => {
|
|
|
174
203
|
"realtime.subscribe",
|
|
175
204
|
"durable.checkpoint",
|
|
176
205
|
"durable.resume",
|
|
206
|
+
"storage.get",
|
|
207
|
+
"storage.set",
|
|
208
|
+
"storage.delete",
|
|
209
|
+
"storage.keys",
|
|
210
|
+
"storage.compareAndSet",
|
|
177
211
|
];
|
|
178
212
|
for (const key of expected) {
|
|
179
213
|
expect(PRIMITIVE_BINDINGS[key]).toBeDefined();
|
|
@@ -326,6 +326,31 @@ export const PRIMITIVE_BINDINGS: Record<string, PrimitiveBinding> = {
|
|
|
326
326
|
method: "background.stop",
|
|
327
327
|
grant: "background:control",
|
|
328
328
|
},
|
|
329
|
+
"storage.get": {
|
|
330
|
+
transport: "bridge",
|
|
331
|
+
method: "storage.get",
|
|
332
|
+
grant: "storage:kv",
|
|
333
|
+
},
|
|
334
|
+
"storage.set": {
|
|
335
|
+
transport: "bridge",
|
|
336
|
+
method: "storage.set",
|
|
337
|
+
grant: "storage:kv",
|
|
338
|
+
},
|
|
339
|
+
"storage.delete": {
|
|
340
|
+
transport: "bridge",
|
|
341
|
+
method: "storage.delete",
|
|
342
|
+
grant: "storage:kv",
|
|
343
|
+
},
|
|
344
|
+
"storage.keys": {
|
|
345
|
+
transport: "bridge",
|
|
346
|
+
method: "storage.keys",
|
|
347
|
+
grant: "storage:kv",
|
|
348
|
+
},
|
|
349
|
+
"storage.compareAndSet": {
|
|
350
|
+
transport: "bridge",
|
|
351
|
+
method: "storage.compareAndSet",
|
|
352
|
+
grant: "storage:kv",
|
|
353
|
+
},
|
|
329
354
|
// Host-direct media data-path (the host holds the node token; returns data: URLs).
|
|
330
355
|
"image.generate": {
|
|
331
356
|
transport: "direct",
|
|
@@ -509,6 +534,25 @@ export interface BackgroundClient {
|
|
|
509
534
|
}>;
|
|
510
535
|
}
|
|
511
536
|
|
|
537
|
+
/** Durable app-owned key/value state (`storage:kv` via the host bridge). */
|
|
538
|
+
export interface StorageClient {
|
|
539
|
+
/** Atomically replace a value when it still equals `expected`. */
|
|
540
|
+
compareAndSet(input: {
|
|
541
|
+
expected: string | null;
|
|
542
|
+
key: string;
|
|
543
|
+
namespace?: string;
|
|
544
|
+
value: string | null;
|
|
545
|
+
}): Promise<boolean>;
|
|
546
|
+
/** Delete one value. */
|
|
547
|
+
delete(input: { key: string; namespace?: string }): Promise<void>;
|
|
548
|
+
/** Read one value; missing keys return `null`. */
|
|
549
|
+
get(input: { key: string; namespace?: string }): Promise<string | null>;
|
|
550
|
+
/** List keys in an app-owned namespace. */
|
|
551
|
+
keys(input?: { namespace?: string }): Promise<string[]>;
|
|
552
|
+
/** Write one string value. JSON can be encoded by the app when needed. */
|
|
553
|
+
set(input: { key: string; namespace?: string; value: string }): Promise<void>;
|
|
554
|
+
}
|
|
555
|
+
|
|
512
556
|
/** TTS primitive — speech synthesis (`crates/ryu-tts`). */
|
|
513
557
|
export interface TtsClient {
|
|
514
558
|
/**
|
|
@@ -561,6 +605,7 @@ export interface RyuPrimitives {
|
|
|
561
605
|
memory: MemoryClient;
|
|
562
606
|
rag: RagClient;
|
|
563
607
|
realtime: RealtimeClient;
|
|
608
|
+
storage: StorageClient;
|
|
564
609
|
stt: SttClient;
|
|
565
610
|
tts: TtsClient;
|
|
566
611
|
}
|
|
@@ -599,6 +644,20 @@ export function createPrimitives(transport: PrimitiveTransport): RyuPrimitives {
|
|
|
599
644
|
process_id: string;
|
|
600
645
|
}>,
|
|
601
646
|
},
|
|
647
|
+
storage: {
|
|
648
|
+
get: (input) =>
|
|
649
|
+
transport.bridge("storage.get", input) as Promise<string | null>,
|
|
650
|
+
set: (input) =>
|
|
651
|
+
transport.bridge("storage.set", input).then(() => undefined),
|
|
652
|
+
delete: (input) =>
|
|
653
|
+
transport.bridge("storage.delete", input).then(() => undefined),
|
|
654
|
+
keys: (input) =>
|
|
655
|
+
transport
|
|
656
|
+
.bridge("storage.keys", input ?? {})
|
|
657
|
+
.then((value) => (Array.isArray(value) ? (value as string[]) : [])),
|
|
658
|
+
compareAndSet: (input) =>
|
|
659
|
+
transport.bridge("storage.compareAndSet", input) as Promise<boolean>,
|
|
660
|
+
},
|
|
602
661
|
rag: {
|
|
603
662
|
retrieve: (input) =>
|
|
604
663
|
brokerCall(transport, "rag", "retrieve", input) as Promise<RagChunk[]>,
|
|
@@ -14,6 +14,7 @@ import type {
|
|
|
14
14
|
MemoryClient,
|
|
15
15
|
RagClient,
|
|
16
16
|
RealtimeClient,
|
|
17
|
+
StorageClient,
|
|
17
18
|
SttClient,
|
|
18
19
|
TtsClient,
|
|
19
20
|
} from "./primitives.ts";
|
|
@@ -81,6 +82,8 @@ export interface RunnableContext {
|
|
|
81
82
|
sessionId?: string;
|
|
82
83
|
/** Signal to abort a long-running run. */
|
|
83
84
|
signal?: AbortSignal;
|
|
85
|
+
/** Durable app-owned KV state, gated by the manifest's `storage:kv` grant. */
|
|
86
|
+
storage?: StorageClient;
|
|
84
87
|
/** STT primitive: transcribe (`crates/ryu-stt`). */
|
|
85
88
|
stt?: SttClient;
|
|
86
89
|
/** TTS primitive: speak (`crates/ryu-tts`). */
|
package/src/runnable/tool.ts
CHANGED
|
@@ -55,12 +55,16 @@ export interface ToolSchema {
|
|
|
55
55
|
required?: string[];
|
|
56
56
|
/** Type is always "object" for a tool's top-level input schema. */
|
|
57
57
|
type: "object";
|
|
58
|
+
/** Preserve additional JSON Schema keywords for Core/MCP consumers. */
|
|
59
|
+
[key: string]: unknown;
|
|
58
60
|
}
|
|
59
61
|
|
|
60
62
|
// ── Options ───────────────────────────────────────────────────────────────────
|
|
61
63
|
|
|
62
64
|
/** Options accepted by `defineTool`. */
|
|
63
65
|
export interface ToolOptions<TInput extends Record<string, unknown>, TOutput> {
|
|
66
|
+
/** Optional description surfaced to models and tool discovery. */
|
|
67
|
+
description?: string;
|
|
64
68
|
/** Stable unique identifier (e.g. "tool-web-search"). */
|
|
65
69
|
id: string;
|
|
66
70
|
/** Human-readable display name. */
|
|
@@ -171,6 +175,8 @@ export interface ToolRunnable<
|
|
|
171
175
|
* the sandbox form is the second parameter aliased to `host`.
|
|
172
176
|
*/
|
|
173
177
|
readonly code: string;
|
|
178
|
+
/** Optional description surfaced to models and tool discovery. */
|
|
179
|
+
readonly description?: string;
|
|
174
180
|
readonly kind: "tool";
|
|
175
181
|
/** JSON Schema for this tool's input — compatible with Core's ToolInfo.schema. */
|
|
176
182
|
readonly schema: ToolSchema;
|
|
@@ -211,7 +217,7 @@ export function defineTool<
|
|
|
211
217
|
TInput extends Record<string, unknown> = Record<string, unknown>,
|
|
212
218
|
TOutput = unknown,
|
|
213
219
|
>(options: ToolOptions<TInput, TOutput>): ToolRunnable<TInput, TOutput> {
|
|
214
|
-
const { id, name, schema, run } = options;
|
|
220
|
+
const { description, id, name, schema, run } = options;
|
|
215
221
|
|
|
216
222
|
// Serialize the run body for Core's `inline_deno` backend — the same approach
|
|
217
223
|
// `defineTurnHook` uses: the sandbox wraps this in an async IIFE where `input`
|
|
@@ -223,6 +229,7 @@ export function defineTool<
|
|
|
223
229
|
name,
|
|
224
230
|
kind: "tool",
|
|
225
231
|
schema,
|
|
232
|
+
...(description === undefined ? {} : { description }),
|
|
226
233
|
code,
|
|
227
234
|
run(input: TInput, ctx: RunnableContext): Promise<TOutput> {
|
|
228
235
|
validateInput(input, schema, id);
|
|
@@ -231,6 +238,20 @@ export function defineTool<
|
|
|
231
238
|
} satisfies ToolRunnable<TInput, TOutput>;
|
|
232
239
|
}
|
|
233
240
|
|
|
241
|
+
/** Optional metadata lowered alongside a tool's executable backend. */
|
|
242
|
+
export interface InlineToolManifestOptions {
|
|
243
|
+
/** Marks the entry as a semantic Ryu Action. */
|
|
244
|
+
action?: boolean;
|
|
245
|
+
/** MCP-style effect annotations, for example `readOnlyHint`. */
|
|
246
|
+
annotations?: Record<string, boolean | undefined>;
|
|
247
|
+
/** Description shown in Core's unified tool catalog. */
|
|
248
|
+
description?: string;
|
|
249
|
+
/** Force the human approval gate before the tool runs. */
|
|
250
|
+
needsApproval?: boolean;
|
|
251
|
+
/** JSON Schema for the structured result. */
|
|
252
|
+
outputSchema?: Record<string, unknown>;
|
|
253
|
+
}
|
|
254
|
+
|
|
234
255
|
/**
|
|
235
256
|
* Convert a {@link ToolRunnable} into a `manifest.json` `kind:"tool"` runnable that
|
|
236
257
|
* ships its `run` body as Core's `inline_deno` backend. The emitted config
|
|
@@ -242,8 +263,8 @@ export function defineTool<
|
|
|
242
263
|
* The plugin must declare the `tool:execute` grant (see `definePlugin`).
|
|
243
264
|
*/
|
|
244
265
|
export function inlineToolRunnable(
|
|
245
|
-
tool: ToolRunnable,
|
|
246
|
-
options?:
|
|
266
|
+
tool: Pick<ToolRunnable, "code" | "description" | "id" | "name" | "schema">,
|
|
267
|
+
options?: InlineToolManifestOptions
|
|
247
268
|
): RunnableMeta {
|
|
248
269
|
return {
|
|
249
270
|
id: tool.id,
|
|
@@ -254,7 +275,17 @@ export function inlineToolRunnable(
|
|
|
254
275
|
backend: "inline_deno",
|
|
255
276
|
code: tool.code,
|
|
256
277
|
input_schema: tool.schema,
|
|
257
|
-
...(options?.description
|
|
278
|
+
...(options?.description === undefined
|
|
279
|
+
? tool.description === undefined
|
|
280
|
+
? {}
|
|
281
|
+
: { description: tool.description }
|
|
282
|
+
: { description: options.description }),
|
|
283
|
+
...(options?.outputSchema ? { output_schema: options.outputSchema } : {}),
|
|
284
|
+
...(options?.annotations ? { annotations: options.annotations } : {}),
|
|
285
|
+
...(options?.action === undefined ? {} : { action: options.action }),
|
|
286
|
+
...(options?.needsApproval === undefined
|
|
287
|
+
? {}
|
|
288
|
+
: { needs_approval: options.needsApproval }),
|
|
258
289
|
},
|
|
259
290
|
};
|
|
260
291
|
}
|
|
@@ -18,6 +18,7 @@ import type {
|
|
|
18
18
|
HookEventContribution,
|
|
19
19
|
PluginManifest,
|
|
20
20
|
RunnableMeta,
|
|
21
|
+
SlashCommandContribution,
|
|
21
22
|
Surface,
|
|
22
23
|
TurnHookContribution,
|
|
23
24
|
} from "../manifest.ts";
|
|
@@ -144,8 +145,8 @@ export interface DefinePluginOptions {
|
|
|
144
145
|
requires?: DefineAppRequires;
|
|
145
146
|
/** Declarative settings tabs (model pickers, fields), passed verbatim. */
|
|
146
147
|
settingsTabs?: Record<string, unknown>[];
|
|
147
|
-
/** Declarative slash commands,
|
|
148
|
-
slashCommands?:
|
|
148
|
+
/** Declarative slash commands, including sequential argument choices. */
|
|
149
|
+
slashCommands?: SlashCommandContribution[];
|
|
149
150
|
/**
|
|
150
151
|
* Host surfaces this plugin runs on. **Omitted/empty = every surface** (the
|
|
151
152
|
* backward-compatible default); it never means "hidden".
|
|
@@ -190,6 +191,7 @@ export function definePlugin(options: DefinePluginOptions): PluginManifest {
|
|
|
190
191
|
pi_extensions: [],
|
|
191
192
|
output_styles: [],
|
|
192
193
|
message_actions: [],
|
|
194
|
+
selection_actions: [],
|
|
193
195
|
};
|
|
194
196
|
// Ship each inline tool as a `kind:"tool"` runnable (Core's `inline_deno`
|
|
195
197
|
// backend). Shipping tools requires the `tool:execute` grant; add it once.
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import {
|
|
3
|
+
app,
|
|
4
|
+
defineApp,
|
|
5
|
+
definePlugin,
|
|
6
|
+
SlashCommandContributionSchema,
|
|
7
|
+
} from "./index.ts";
|
|
8
|
+
|
|
9
|
+
const slashCommand = {
|
|
10
|
+
args: [
|
|
11
|
+
{
|
|
12
|
+
custom: { label: "Use another environment" },
|
|
13
|
+
name: "environment",
|
|
14
|
+
options: [
|
|
15
|
+
{ label: "Staging", value: "staging" },
|
|
16
|
+
{ label: "Production", value: "production" },
|
|
17
|
+
],
|
|
18
|
+
},
|
|
19
|
+
],
|
|
20
|
+
command: "/deploy",
|
|
21
|
+
description: "Deploy the current project",
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
describe("slash command contributions", () => {
|
|
25
|
+
test("validate sequential options and custom choices", () => {
|
|
26
|
+
expect(SlashCommandContributionSchema.parse(slashCommand)).toEqual(
|
|
27
|
+
slashCommand
|
|
28
|
+
);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test("defineApp publishes slash commands", () => {
|
|
32
|
+
const manifest = defineApp({
|
|
33
|
+
id: "com.example.deploy",
|
|
34
|
+
title: "Deploy",
|
|
35
|
+
version: "1.0.0",
|
|
36
|
+
slug: "deploy",
|
|
37
|
+
uiEntry: "src/deploy.tsx",
|
|
38
|
+
tools: [{ name: "render", description: "Render deploy status" }],
|
|
39
|
+
slashCommands: [slashCommand],
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
expect(manifest.contributes?.slash_commands).toEqual([slashCommand]);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test("definePlugin publishes slash commands", () => {
|
|
46
|
+
const manifest = definePlugin({
|
|
47
|
+
id: "com.example.deploy-plugin",
|
|
48
|
+
name: "Deploy",
|
|
49
|
+
version: "1.0.0",
|
|
50
|
+
slashCommands: [slashCommand],
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
expect(manifest.contributes?.slash_commands).toEqual([slashCommand]);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test("AppBuilder publishes slash commands", () => {
|
|
57
|
+
const manifest = app()
|
|
58
|
+
.id("com.example.deploy-builder")
|
|
59
|
+
.title("Deploy")
|
|
60
|
+
.version("1.0.0")
|
|
61
|
+
.slug("deploy-builder")
|
|
62
|
+
.uiEntry("src/deploy.tsx")
|
|
63
|
+
.tool({ name: "render", description: "Render deploy status" })
|
|
64
|
+
.slashCommand(slashCommand)
|
|
65
|
+
.build();
|
|
66
|
+
|
|
67
|
+
expect(manifest.contributes?.slash_commands).toEqual([slashCommand]);
|
|
68
|
+
});
|
|
69
|
+
});
|