@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,610 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Composable primitive-client surface for the Ryu SDK runtime context.
|
|
3
|
+
*
|
|
4
|
+
* Now that each capability is its own crate with a clean trait
|
|
5
|
+
* (`crates/ryu-rag`, `crates/ryu-memory`, `crates/ryu-realtime`,
|
|
6
|
+
* `crates/ryu-durable`, `crates/ryu-engines`, `crates/ryu-tts`,
|
|
7
|
+
* `crates/ryu-stt`, `crates/ryu-image`), the SDK exposes each as a typed,
|
|
8
|
+
* **gateway-mandatory** client on `RunnableContext`: `ctx.rag.retrieve()`,
|
|
9
|
+
* `ctx.memory.recall()`, `ctx.engines.complete()`, and so on. An agent composes
|
|
10
|
+
* the same building blocks a developer does — the DX payoff of decomposition
|
|
11
|
+
* (program §6b).
|
|
12
|
+
*
|
|
13
|
+
* These clients are **thin typed wrappers** over the EXISTING host transport
|
|
14
|
+
* families — they invent NO new backend endpoints. The method names and grants
|
|
15
|
+
* mirror the canonical vocabulary in
|
|
16
|
+
* `packages/app-host/src/rpc.ts` (`METHOD_CAPABILITY` / `GRANT_CAPABILITY`); the
|
|
17
|
+
* arg/result shapes mirror the `RpcServices` signatures there. We MIRROR that
|
|
18
|
+
* vocabulary rather than importing it: `@ryuhq/sdk` is a published package and
|
|
19
|
+
* `@ryu/app-host` is a desktop-host package in a disjoint lane.
|
|
20
|
+
*
|
|
21
|
+
* Three real transport shapes exist in `rpc.ts`, so the transport exposes three
|
|
22
|
+
* ops (all reach a Core node the host holds the token for):
|
|
23
|
+
* - `bridge` → the `PluginHookBridge` families (`POST /api/plugins/:id/host`,
|
|
24
|
+
* `{ method, args }`) — e.g. `model.complete` → `host.sideModel`.
|
|
25
|
+
* - `direct` → host-direct Core data-path calls the host makes on the frame's
|
|
26
|
+
* behalf (`POST /api/images/generate`, `/api/voice/speak`,
|
|
27
|
+
* `/api/voice/transcribe`).
|
|
28
|
+
* - `capability` → the capability broker (`POST /api/host/capability/:cap`) for
|
|
29
|
+
* caps that have no rpc family yet (rag/memory/realtime/durable/
|
|
30
|
+
* engines.embed). These are marked `@requires-grant`: the caller
|
|
31
|
+
* must DECLARE the edge in `requires.capabilities` and hold the
|
|
32
|
+
* bound provider's grant, else Core fails closed (404/403).
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
import { assertAllowedEgressUrl } from "../model/gateway.ts";
|
|
36
|
+
|
|
37
|
+
// ── Transport ────────────────────────────────────────────────────────────────
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Low-level dispatcher a primitive client uses to reach a Core node. Injected
|
|
41
|
+
* so the SDK stays transport-agnostic (the same seam as `ctx.gateway`); a
|
|
42
|
+
* default HTTP implementation is {@link httpPrimitiveTransport}.
|
|
43
|
+
*/
|
|
44
|
+
export interface PrimitiveTransport {
|
|
45
|
+
/**
|
|
46
|
+
* Invoke a `PluginHookBridge` family method (`POST /api/plugins/:id/host`).
|
|
47
|
+
* `method` is the exact `rpc.ts` `METHOD_CAPABILITY` key (e.g.
|
|
48
|
+
* `"model.complete"`); the host maps it to the closed `host.*` bridge path.
|
|
49
|
+
*/
|
|
50
|
+
bridge(method: string, args: unknown): Promise<unknown>;
|
|
51
|
+
/**
|
|
52
|
+
* Invoke an abstract capability through the broker
|
|
53
|
+
* (`POST /api/host/capability/:cap`). `@requires-grant`: the caller must have
|
|
54
|
+
* declared `requires.capabilities: [{ capability: cap }]` and hold the bound
|
|
55
|
+
* provider's grant, or Core fails closed.
|
|
56
|
+
*/
|
|
57
|
+
capability(cap: string, body: unknown): Promise<unknown>;
|
|
58
|
+
/**
|
|
59
|
+
* Invoke a host-direct Core data-path endpoint (`POST {path}`). Used for the
|
|
60
|
+
* media families the host reaches directly (`/api/images/generate`,
|
|
61
|
+
* `/api/voice/speak`, `/api/voice/transcribe`) rather than via the bridge.
|
|
62
|
+
*
|
|
63
|
+
* The two voice endpoints do NOT speak plain JSON-in/JSON-out — a real Core
|
|
64
|
+
* node requires a multipart `file` upload for transcription and streams raw
|
|
65
|
+
* `audio/wav` bytes back from synthesis. The default {@link httpPrimitiveTransport}
|
|
66
|
+
* therefore reshapes those two calls (mirroring the desktop host's `rpc.ts`):
|
|
67
|
+
* - `/api/voice/transcribe` — `{ audio: data-URL, filename? }` → multipart
|
|
68
|
+
* `file` upload; resolves to the transcript `string`.
|
|
69
|
+
* - `/api/voice/speak` — JSON in; resolves to a renderable `data:` audio URL
|
|
70
|
+
* built from the returned `audio/wav` bytes.
|
|
71
|
+
* Every other path is a straight JSON round-trip.
|
|
72
|
+
*/
|
|
73
|
+
direct(path: string, body: unknown): Promise<unknown>;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// ── Data-URL <-> bytes (voice media reshaping, no external deps) ────────────────
|
|
77
|
+
|
|
78
|
+
/** Decode a `data:` URL into its raw bytes + declared media type. */
|
|
79
|
+
function dataUrlToBytes(dataUrl: string): {
|
|
80
|
+
bytes: Uint8Array;
|
|
81
|
+
mediaType: string;
|
|
82
|
+
} {
|
|
83
|
+
const match = /^data:([^;,]*)(;base64)?,([\s\S]*)$/.exec(dataUrl);
|
|
84
|
+
if (!match) {
|
|
85
|
+
throw new Error(
|
|
86
|
+
"stt.transcribe expects an `audio` value that is a data: URL (data:<mime>;base64,<data>)"
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
const mediaType = match[1] || "application/octet-stream";
|
|
90
|
+
const isBase64 = Boolean(match[2]);
|
|
91
|
+
const payload = match[3] ?? "";
|
|
92
|
+
if (isBase64) {
|
|
93
|
+
const binary = atob(payload);
|
|
94
|
+
const bytes = new Uint8Array(binary.length);
|
|
95
|
+
for (let i = 0; i < binary.length; i++) {
|
|
96
|
+
bytes[i] = binary.charCodeAt(i);
|
|
97
|
+
}
|
|
98
|
+
return { bytes, mediaType };
|
|
99
|
+
}
|
|
100
|
+
return { bytes: new TextEncoder().encode(decodeURIComponent(payload)), mediaType };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** Encode raw bytes as a `data:<mediaType>;base64,...` URL. */
|
|
104
|
+
function bytesToDataUrl(bytes: Uint8Array, mediaType: string): string {
|
|
105
|
+
let binary = "";
|
|
106
|
+
// Chunk to stay well under the argument-count ceiling of String.fromCharCode.
|
|
107
|
+
const chunk = 0x8000;
|
|
108
|
+
for (let i = 0; i < bytes.length; i += chunk) {
|
|
109
|
+
binary += String.fromCharCode(...bytes.subarray(i, i + chunk));
|
|
110
|
+
}
|
|
111
|
+
return `data:${mediaType};base64,${btoa(binary)}`;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** Options for {@link httpPrimitiveTransport}. */
|
|
115
|
+
export interface HttpPrimitiveTransportOptions {
|
|
116
|
+
/** Injectable `fetch` (defaults to the global). */
|
|
117
|
+
fetchImpl?: typeof fetch;
|
|
118
|
+
/**
|
|
119
|
+
* Core **node** base URL (no trailing slash) — NEVER a direct provider. The
|
|
120
|
+
* URL is validated against the direct-provider egress blocklist so every
|
|
121
|
+
* primitive call stays governed (the gateway-mandatory rule).
|
|
122
|
+
*/
|
|
123
|
+
nodeUrl: string;
|
|
124
|
+
/**
|
|
125
|
+
* The calling plugin's reverse-domain id. REQUIRED for the `bridge` op —
|
|
126
|
+
* `/api/plugins/:id/host` authenticates this id and gates on its
|
|
127
|
+
* Gateway-approved grants. Omit only if the caller never uses bridge-backed
|
|
128
|
+
* primitives (`ctx.engines.complete`).
|
|
129
|
+
*/
|
|
130
|
+
pluginId?: string;
|
|
131
|
+
/** Node bearer token forwarded on every call. */
|
|
132
|
+
token?: string;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* A default HTTP {@link PrimitiveTransport} targeting a Core node. Validates
|
|
137
|
+
* `nodeUrl` against the direct-provider egress blocklist at construction, so a
|
|
138
|
+
* mis-pointed transport can never route a primitive call at a raw provider.
|
|
139
|
+
*/
|
|
140
|
+
export function httpPrimitiveTransport(
|
|
141
|
+
options: HttpPrimitiveTransportOptions
|
|
142
|
+
): PrimitiveTransport {
|
|
143
|
+
const base = options.nodeUrl.replace(/\/+$/, "");
|
|
144
|
+
// Gateway-mandatory: reject a direct-provider base URL. A Core node URL
|
|
145
|
+
// (e.g. http://127.0.0.1:7980) passes; api.openai.com et al. throw.
|
|
146
|
+
assertAllowedEgressUrl(base);
|
|
147
|
+
const doFetch = options.fetchImpl ?? fetch;
|
|
148
|
+
|
|
149
|
+
const authHeader = (): Record<string, string> =>
|
|
150
|
+
options.token ? { authorization: `Bearer ${options.token}` } : {};
|
|
151
|
+
|
|
152
|
+
const failDetail = async (path: string, res: Response): Promise<Error> => {
|
|
153
|
+
const detail = await res.text().catch(() => "");
|
|
154
|
+
return new Error(
|
|
155
|
+
`Ryu primitive call ${path} failed: ${res.status} ${res.statusText}${
|
|
156
|
+
detail ? ` — ${detail}` : ""
|
|
157
|
+
}`
|
|
158
|
+
);
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
const post = async (path: string, body: unknown): Promise<unknown> => {
|
|
162
|
+
const res = await doFetch(`${base}${path}`, {
|
|
163
|
+
method: "POST",
|
|
164
|
+
headers: { "content-type": "application/json", ...authHeader() },
|
|
165
|
+
body: JSON.stringify(body ?? {}),
|
|
166
|
+
});
|
|
167
|
+
if (!res.ok) {
|
|
168
|
+
throw await failDetail(path, res);
|
|
169
|
+
}
|
|
170
|
+
const text = await res.text();
|
|
171
|
+
return text ? (JSON.parse(text) as unknown) : undefined;
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
// `/api/voice/transcribe` — Core's Axum handler requires a multipart upload
|
|
175
|
+
// with a `file` field, NOT a JSON body. Convert the caller's data: URL into a
|
|
176
|
+
// `file` part (letting fetch set the multipart boundary) and return the text.
|
|
177
|
+
const transcribeDirect = async (body: unknown): Promise<string> => {
|
|
178
|
+
const input = (body ?? {}) as { audio?: string; filename?: string };
|
|
179
|
+
if (!input.audio) {
|
|
180
|
+
throw new Error(
|
|
181
|
+
"stt.transcribe requires an `audio` data: URL (the recorded audio)"
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
const { bytes, mediaType } = dataUrlToBytes(input.audio);
|
|
185
|
+
const form = new FormData();
|
|
186
|
+
form.append(
|
|
187
|
+
"file",
|
|
188
|
+
new Blob([bytes], { type: mediaType || "audio/wav" }),
|
|
189
|
+
input.filename ?? "recording.wav"
|
|
190
|
+
);
|
|
191
|
+
// No JSON content-type here — FormData sets its own multipart boundary.
|
|
192
|
+
const res = await doFetch(`${base}/api/voice/transcribe`, {
|
|
193
|
+
method: "POST",
|
|
194
|
+
headers: authHeader(),
|
|
195
|
+
body: form,
|
|
196
|
+
});
|
|
197
|
+
if (!res.ok) {
|
|
198
|
+
throw await failDetail("/api/voice/transcribe", res);
|
|
199
|
+
}
|
|
200
|
+
const parsed = (await res.json()) as { text?: string };
|
|
201
|
+
return (parsed.text ?? "").trim();
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
// `/api/voice/speak` — Core streams raw `audio/wav` bytes back (not JSON, not a
|
|
205
|
+
// data: URL). Convert the response to a renderable data: URL, as the desktop
|
|
206
|
+
// host's rpc.ts does, so the shipped type contract ("returns a data: URL") holds.
|
|
207
|
+
const speakDirect = async (body: unknown): Promise<string> => {
|
|
208
|
+
const res = await doFetch(`${base}/api/voice/speak`, {
|
|
209
|
+
method: "POST",
|
|
210
|
+
headers: { "content-type": "application/json", ...authHeader() },
|
|
211
|
+
body: JSON.stringify(body ?? {}),
|
|
212
|
+
});
|
|
213
|
+
if (!res.ok) {
|
|
214
|
+
throw await failDetail("/api/voice/speak", res);
|
|
215
|
+
}
|
|
216
|
+
const bytes = new Uint8Array(await res.arrayBuffer());
|
|
217
|
+
const mediaType = res.headers.get("content-type") || "audio/wav";
|
|
218
|
+
return bytesToDataUrl(bytes, mediaType);
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
// `/api/images/generate` — Core returns an OpenAI-style envelope
|
|
222
|
+
// (`{ data: [{ url? , b64_json? }] }`, `apps/core/src/server/media.rs`), NOT a
|
|
223
|
+
// bare array of strings. Unwrap each item to a renderable URL: a direct `url`
|
|
224
|
+
// if present, else a `data:image/png;base64,<b64_json>` URL — so the shipped
|
|
225
|
+
// `Promise<string[]>` contract holds.
|
|
226
|
+
const generateImageDirect = async (body: unknown): Promise<string[]> => {
|
|
227
|
+
const res = await doFetch(`${base}/api/images/generate`, {
|
|
228
|
+
method: "POST",
|
|
229
|
+
headers: { "content-type": "application/json", ...authHeader() },
|
|
230
|
+
body: JSON.stringify(body ?? {}),
|
|
231
|
+
});
|
|
232
|
+
if (!res.ok) {
|
|
233
|
+
throw await failDetail("/api/images/generate", res);
|
|
234
|
+
}
|
|
235
|
+
const parsed = (await res.json()) as {
|
|
236
|
+
data?: Array<{ url?: string; b64_json?: string }>;
|
|
237
|
+
};
|
|
238
|
+
return (parsed.data ?? []).map((item) =>
|
|
239
|
+
item.url
|
|
240
|
+
? item.url
|
|
241
|
+
: `data:image/png;base64,${item.b64_json ?? ""}`
|
|
242
|
+
);
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
return {
|
|
246
|
+
bridge(method, args) {
|
|
247
|
+
if (!options.pluginId) {
|
|
248
|
+
return Promise.reject(
|
|
249
|
+
new Error(
|
|
250
|
+
`bridge primitive "${method}" requires a pluginId (the /api/plugins/:id/host caller identity)`
|
|
251
|
+
)
|
|
252
|
+
);
|
|
253
|
+
}
|
|
254
|
+
return post(`/api/plugins/${options.pluginId}/host`, { method, args });
|
|
255
|
+
},
|
|
256
|
+
direct(path, body) {
|
|
257
|
+
if (path === "/api/voice/transcribe") {
|
|
258
|
+
return transcribeDirect(body);
|
|
259
|
+
}
|
|
260
|
+
if (path === "/api/voice/speak") {
|
|
261
|
+
return speakDirect(body);
|
|
262
|
+
}
|
|
263
|
+
if (path === "/api/images/generate") {
|
|
264
|
+
return generateImageDirect(body);
|
|
265
|
+
}
|
|
266
|
+
return post(path, body);
|
|
267
|
+
},
|
|
268
|
+
capability(cap, body) {
|
|
269
|
+
return post(`/api/host/capability/${cap}`, body);
|
|
270
|
+
},
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// ── Primitive → transport binding (the single drift-point) ─────────────────────
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* How one primitive method reaches Core. This is the SDK's single mirror of the
|
|
278
|
+
* `rpc.ts` `METHOD_CAPABILITY` / `GRANT_CAPABILITY` maps — keep it in lockstep
|
|
279
|
+
* with that canonical source. `bridge`/`direct` families exist today;
|
|
280
|
+
* `broker` families are `@requires-grant` (declared capability edge).
|
|
281
|
+
*/
|
|
282
|
+
export type PrimitiveBinding =
|
|
283
|
+
| {
|
|
284
|
+
readonly transport: "bridge";
|
|
285
|
+
/** Exact `rpc.ts` method key (e.g. `"model.complete"`). */
|
|
286
|
+
readonly method: string;
|
|
287
|
+
/** Gateway grant that unlocks it (`GRANT_CAPABILITY` inverse). */
|
|
288
|
+
readonly grant: string;
|
|
289
|
+
}
|
|
290
|
+
| {
|
|
291
|
+
readonly transport: "direct";
|
|
292
|
+
/** Core data-path endpoint the host calls directly. */
|
|
293
|
+
readonly path: string;
|
|
294
|
+
/** Gateway grant that unlocks it. */
|
|
295
|
+
readonly grant: string;
|
|
296
|
+
}
|
|
297
|
+
| {
|
|
298
|
+
readonly transport: "broker";
|
|
299
|
+
/** Abstract capability name (the broker `:cap` segment + the
|
|
300
|
+
* `requires.capabilities` edge the caller must declare). */
|
|
301
|
+
readonly capability: string;
|
|
302
|
+
};
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* The binding for every primitive method, keyed `"<namespace>.<method>"`.
|
|
306
|
+
* Mirrors `rpc.ts`; the ONLY place primitive→endpoint knowledge lives.
|
|
307
|
+
*/
|
|
308
|
+
export const PRIMITIVE_BINDINGS: Record<string, PrimitiveBinding> = {
|
|
309
|
+
// Bridge families (existing `PluginHookBridge`).
|
|
310
|
+
"engines.complete": {
|
|
311
|
+
transport: "bridge",
|
|
312
|
+
method: "model.complete",
|
|
313
|
+
grant: "hook:side-model",
|
|
314
|
+
},
|
|
315
|
+
// Host-direct media data-path (the host holds the node token; returns data: URLs).
|
|
316
|
+
"image.generate": {
|
|
317
|
+
transport: "direct",
|
|
318
|
+
path: "/api/images/generate",
|
|
319
|
+
grant: "media:generate",
|
|
320
|
+
},
|
|
321
|
+
"tts.speak": {
|
|
322
|
+
transport: "direct",
|
|
323
|
+
path: "/api/voice/speak",
|
|
324
|
+
grant: "media:generate",
|
|
325
|
+
},
|
|
326
|
+
"stt.transcribe": {
|
|
327
|
+
transport: "direct",
|
|
328
|
+
path: "/api/voice/transcribe",
|
|
329
|
+
grant: "media:transcribe",
|
|
330
|
+
},
|
|
331
|
+
// Broker capabilities — no rpc family yet (@requires-grant).
|
|
332
|
+
"rag.retrieve": { transport: "broker", capability: "rag" },
|
|
333
|
+
"rag.embed": { transport: "broker", capability: "rag" },
|
|
334
|
+
"rag.rerank": { transport: "broker", capability: "rag" },
|
|
335
|
+
"memory.recall": { transport: "broker", capability: "memory" },
|
|
336
|
+
"memory.store": { transport: "broker", capability: "memory" },
|
|
337
|
+
"realtime.broadcast": { transport: "broker", capability: "realtime" },
|
|
338
|
+
"realtime.subscribe": { transport: "broker", capability: "realtime" },
|
|
339
|
+
"durable.checkpoint": { transport: "broker", capability: "durable" },
|
|
340
|
+
"durable.resume": { transport: "broker", capability: "durable" },
|
|
341
|
+
"engines.embed": { transport: "broker", capability: "engines" },
|
|
342
|
+
};
|
|
343
|
+
|
|
344
|
+
// ── Typed primitive clients (shapes mirror the crate traits) ───────────────────
|
|
345
|
+
|
|
346
|
+
/** One retrieved chunk (`crates/ryu-rag` `RagChunk`). */
|
|
347
|
+
export interface RagChunk {
|
|
348
|
+
id: string;
|
|
349
|
+
metadata?: Record<string, unknown>;
|
|
350
|
+
score: number;
|
|
351
|
+
source?: string;
|
|
352
|
+
text: string;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/** One rerank result (`crates/ryu-rag` reranker trait). */
|
|
356
|
+
export interface RagRerankResult {
|
|
357
|
+
document: string;
|
|
358
|
+
index: number;
|
|
359
|
+
score: number;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/** RAG primitive — retrieval, embedding, reranking (`crates/ryu-rag`). */
|
|
363
|
+
export interface RagClient {
|
|
364
|
+
/** Embed text into vectors. `@requires-grant rag`. */
|
|
365
|
+
embed(input: {
|
|
366
|
+
input: string | string[];
|
|
367
|
+
model?: string;
|
|
368
|
+
}): Promise<number[][]>;
|
|
369
|
+
/** Rerank `documents` against `query`. `@requires-grant rag`. */
|
|
370
|
+
rerank(input: {
|
|
371
|
+
query: string;
|
|
372
|
+
documents: string[];
|
|
373
|
+
topK?: number;
|
|
374
|
+
model?: string;
|
|
375
|
+
}): Promise<RagRerankResult[]>;
|
|
376
|
+
/** Vector/GraphRAG retrieval for `query`. `@requires-grant rag`. */
|
|
377
|
+
retrieve(input: {
|
|
378
|
+
query: string;
|
|
379
|
+
topK?: number;
|
|
380
|
+
spaceId?: string;
|
|
381
|
+
filter?: Record<string, unknown>;
|
|
382
|
+
}): Promise<RagChunk[]>;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/** One recalled memory (`crates/ryu-memory` `MemoryItem`). */
|
|
386
|
+
export interface MemoryItem {
|
|
387
|
+
category?: string;
|
|
388
|
+
content: string;
|
|
389
|
+
id: string;
|
|
390
|
+
importance?: number;
|
|
391
|
+
level?: string;
|
|
392
|
+
score?: number;
|
|
393
|
+
tags?: string[];
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/** Memory primitive — recall + store (`crates/ryu-memory`). */
|
|
397
|
+
export interface MemoryClient {
|
|
398
|
+
/** Semantic recall across the readable scope levels. `@requires-grant memory`. */
|
|
399
|
+
recall(input: {
|
|
400
|
+
query: string;
|
|
401
|
+
levels?: string[];
|
|
402
|
+
limit?: number;
|
|
403
|
+
}): Promise<MemoryItem[]>;
|
|
404
|
+
/** Persist a memory. `@requires-grant memory`. */
|
|
405
|
+
store(input: {
|
|
406
|
+
content: string;
|
|
407
|
+
level?: string;
|
|
408
|
+
category?: string;
|
|
409
|
+
importance?: number;
|
|
410
|
+
tags?: string[];
|
|
411
|
+
}): Promise<{ id: string }>;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
/**
|
|
415
|
+
* A realtime subscription handle. The broker is a unary POST, so `subscribe`
|
|
416
|
+
* cannot stream today — it returns a handle (the crate's typed event contract
|
|
417
|
+
* grows a live channel later). Honest shape over a promised-but-unbacked stream.
|
|
418
|
+
*/
|
|
419
|
+
export interface RealtimeSubscription {
|
|
420
|
+
room: string;
|
|
421
|
+
subscriptionId: string;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
/** Realtime primitive — typed room events (`crates/ryu-realtime`). */
|
|
425
|
+
export interface RealtimeClient {
|
|
426
|
+
/** Broadcast a typed event to a room. `@requires-grant realtime`. */
|
|
427
|
+
broadcast(input: {
|
|
428
|
+
room: string;
|
|
429
|
+
event: string;
|
|
430
|
+
payload?: unknown;
|
|
431
|
+
}): Promise<void>;
|
|
432
|
+
/** Open a subscription handle for a room. `@requires-grant realtime`. */
|
|
433
|
+
subscribe(input: { room: string }): Promise<RealtimeSubscription>;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
/** Durable primitive — checkpoint + resume (`crates/ryu-durable`). */
|
|
437
|
+
export interface DurableClient {
|
|
438
|
+
/** Persist a checkpoint; returns a resume token. `@requires-grant durable`. */
|
|
439
|
+
checkpoint(input: {
|
|
440
|
+
key: string;
|
|
441
|
+
state: unknown;
|
|
442
|
+
}): Promise<{ token: string }>;
|
|
443
|
+
/** Resume from a checkpoint token (`null` when unknown). `@requires-grant durable`. */
|
|
444
|
+
resume(input: { token: string }): Promise<{ state: unknown } | null>;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
/** Engines primitive — completion + embedding (`crates/ryu-engines`). */
|
|
448
|
+
export interface EnginesClient {
|
|
449
|
+
/**
|
|
450
|
+
* Tool-less one-shot completion (bridge `model.complete` → `host.sideModel`,
|
|
451
|
+
* Gateway-routed). Grant `hook:side-model`.
|
|
452
|
+
*/
|
|
453
|
+
complete(input: {
|
|
454
|
+
prompt: string;
|
|
455
|
+
system?: string;
|
|
456
|
+
model?: string;
|
|
457
|
+
modelPrefKey?: string;
|
|
458
|
+
effort?: string;
|
|
459
|
+
}): Promise<string>;
|
|
460
|
+
/** Embed text into vectors. `@requires-grant engines`. */
|
|
461
|
+
embed(input: {
|
|
462
|
+
input: string | string[];
|
|
463
|
+
model?: string;
|
|
464
|
+
}): Promise<number[][]>;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
/** TTS primitive — speech synthesis (`crates/ryu-tts`). */
|
|
468
|
+
export interface TtsClient {
|
|
469
|
+
/**
|
|
470
|
+
* Synthesize speech (host-direct `/api/voice/speak`, Gateway-governed).
|
|
471
|
+
* Returns a renderable `data:` audio URL. Grant `media:generate`.
|
|
472
|
+
*/
|
|
473
|
+
speak(input: {
|
|
474
|
+
text: string;
|
|
475
|
+
engine?: string;
|
|
476
|
+
voice?: string;
|
|
477
|
+
speed?: number;
|
|
478
|
+
language?: string;
|
|
479
|
+
}): Promise<string>;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
/** STT primitive — transcription (`crates/ryu-stt`). */
|
|
483
|
+
export interface SttClient {
|
|
484
|
+
/**
|
|
485
|
+
* Transcribe an audio `data:` URL (host-direct `/api/voice/transcribe`).
|
|
486
|
+
* Returns the text. Grant `media:transcribe`.
|
|
487
|
+
*/
|
|
488
|
+
transcribe(input: { audio: string; filename?: string }): Promise<string>;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
/** Image primitive — generation (`crates/ryu-image`). */
|
|
492
|
+
export interface ImageClient {
|
|
493
|
+
/**
|
|
494
|
+
* Generate image(s) from a prompt (host-direct `/api/images/generate`,
|
|
495
|
+
* Gateway-governed). Returns renderable `data:` URLs. Grant `media:generate`.
|
|
496
|
+
*/
|
|
497
|
+
generate(input: {
|
|
498
|
+
prompt: string;
|
|
499
|
+
count?: number;
|
|
500
|
+
size?: string;
|
|
501
|
+
provider?: string;
|
|
502
|
+
model?: string;
|
|
503
|
+
}): Promise<string[]>;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
/**
|
|
507
|
+
* The composable primitive bundle mounted on {@link RunnableContext}. Every
|
|
508
|
+
* client routes through the Gateway (bridge/direct/broker all reach a governed
|
|
509
|
+
* Core node) — the same "what runs vs what is allowed" split as `ctx.gateway`.
|
|
510
|
+
*/
|
|
511
|
+
export interface RyuPrimitives {
|
|
512
|
+
durable: DurableClient;
|
|
513
|
+
engines: EnginesClient;
|
|
514
|
+
image: ImageClient;
|
|
515
|
+
memory: MemoryClient;
|
|
516
|
+
rag: RagClient;
|
|
517
|
+
realtime: RealtimeClient;
|
|
518
|
+
stt: SttClient;
|
|
519
|
+
tts: TtsClient;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
// ── Factory ────────────────────────────────────────────────────────────────
|
|
523
|
+
|
|
524
|
+
/** Route a broker-backed primitive call. `op` discriminates the provider verb. */
|
|
525
|
+
function brokerCall(
|
|
526
|
+
transport: PrimitiveTransport,
|
|
527
|
+
cap: string,
|
|
528
|
+
op: string,
|
|
529
|
+
input: unknown
|
|
530
|
+
): Promise<unknown> {
|
|
531
|
+
return transport.capability(cap, { op, input });
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
/**
|
|
535
|
+
* Build the typed {@link RyuPrimitives} bundle over a {@link PrimitiveTransport}.
|
|
536
|
+
* Each method is a thin wrapper: bridge/direct families forward to the existing
|
|
537
|
+
* endpoints; broker families POST to `/api/host/capability/:cap` (`@requires-grant`).
|
|
538
|
+
*/
|
|
539
|
+
export function createPrimitives(transport: PrimitiveTransport): RyuPrimitives {
|
|
540
|
+
return {
|
|
541
|
+
rag: {
|
|
542
|
+
retrieve: (input) =>
|
|
543
|
+
brokerCall(transport, "rag", "retrieve", input) as Promise<RagChunk[]>,
|
|
544
|
+
embed: (input) =>
|
|
545
|
+
brokerCall(transport, "rag", "embed", input) as Promise<number[][]>,
|
|
546
|
+
rerank: (input) =>
|
|
547
|
+
brokerCall(transport, "rag", "rerank", input) as Promise<
|
|
548
|
+
RagRerankResult[]
|
|
549
|
+
>,
|
|
550
|
+
},
|
|
551
|
+
memory: {
|
|
552
|
+
recall: (input) =>
|
|
553
|
+
brokerCall(transport, "memory", "recall", input) as Promise<
|
|
554
|
+
MemoryItem[]
|
|
555
|
+
>,
|
|
556
|
+
store: (input) =>
|
|
557
|
+
brokerCall(transport, "memory", "store", input) as Promise<{
|
|
558
|
+
id: string;
|
|
559
|
+
}>,
|
|
560
|
+
},
|
|
561
|
+
realtime: {
|
|
562
|
+
broadcast: (input) =>
|
|
563
|
+
brokerCall(transport, "realtime", "broadcast", input).then(
|
|
564
|
+
() => undefined
|
|
565
|
+
),
|
|
566
|
+
subscribe: (input) =>
|
|
567
|
+
brokerCall(
|
|
568
|
+
transport,
|
|
569
|
+
"realtime",
|
|
570
|
+
"subscribe",
|
|
571
|
+
input
|
|
572
|
+
) as Promise<RealtimeSubscription>,
|
|
573
|
+
},
|
|
574
|
+
durable: {
|
|
575
|
+
checkpoint: (input) =>
|
|
576
|
+
brokerCall(transport, "durable", "checkpoint", input) as Promise<{
|
|
577
|
+
token: string;
|
|
578
|
+
}>,
|
|
579
|
+
resume: (input) =>
|
|
580
|
+
brokerCall(transport, "durable", "resume", input) as Promise<{
|
|
581
|
+
state: unknown;
|
|
582
|
+
} | null>,
|
|
583
|
+
},
|
|
584
|
+
engines: {
|
|
585
|
+
// Bridge family: model.complete → host.sideModel (wire keys are snake_case).
|
|
586
|
+
complete: (input) =>
|
|
587
|
+
transport.bridge("model.complete", {
|
|
588
|
+
prompt: input.prompt,
|
|
589
|
+
system: input.system,
|
|
590
|
+
model: input.model,
|
|
591
|
+
model_pref_key: input.modelPrefKey,
|
|
592
|
+
effort: input.effort,
|
|
593
|
+
}) as Promise<string>,
|
|
594
|
+
embed: (input) =>
|
|
595
|
+
brokerCall(transport, "engines", "embed", input) as Promise<number[][]>,
|
|
596
|
+
},
|
|
597
|
+
tts: {
|
|
598
|
+
speak: (input) =>
|
|
599
|
+
transport.direct("/api/voice/speak", input) as Promise<string>,
|
|
600
|
+
},
|
|
601
|
+
stt: {
|
|
602
|
+
transcribe: (input) =>
|
|
603
|
+
transport.direct("/api/voice/transcribe", input) as Promise<string>,
|
|
604
|
+
},
|
|
605
|
+
image: {
|
|
606
|
+
generate: (input) =>
|
|
607
|
+
transport.direct("/api/images/generate", input) as Promise<string[]>,
|
|
608
|
+
},
|
|
609
|
+
};
|
|
610
|
+
}
|