@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
package/src/cli/dev.ts
ADDED
|
@@ -0,0 +1,425 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `ryu dev <entry>` — local dev playground for Ryu Runnables.
|
|
3
|
+
*
|
|
4
|
+
* Loads a developer-authored Runnable module from `<entry>`, starts an
|
|
5
|
+
* interactive chat/run loop in the terminal, and routes every model call
|
|
6
|
+
* through the local gateway (via the ModelClient from unit-c). The loop
|
|
7
|
+
* mirrors AcpEvent categories — text, tool-call, tool-result, error — so
|
|
8
|
+
* what a developer sees locally matches what Core will surface once the
|
|
9
|
+
* app is wrapped as an engine.
|
|
10
|
+
*
|
|
11
|
+
* Gateway URL resolution (same order as ModelClient):
|
|
12
|
+
* 1. RYU_GATEWAY_URL env var
|
|
13
|
+
* 2. http://127.0.0.1:7981 (Core default)
|
|
14
|
+
*
|
|
15
|
+
* If the gateway is unreachable the command prints a clear error and exits
|
|
16
|
+
* non-zero. No silent provider fallback is ever attempted.
|
|
17
|
+
*
|
|
18
|
+
* Usage:
|
|
19
|
+
* bunx ryu dev <entry>
|
|
20
|
+
* bunx ryu dev ./my-agent.ts
|
|
21
|
+
* RYU_GATEWAY_URL=http://my-gateway:7981 bunx ryu dev ./my-agent.ts
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import { createInterface } from "node:readline";
|
|
25
|
+
import type { ChatMessage } from "../model/client.ts";
|
|
26
|
+
import { ModelClient } from "../model/client.ts";
|
|
27
|
+
import { resolveGatewayUrl } from "../model/gateway.ts";
|
|
28
|
+
|
|
29
|
+
// Top-level regex for absolute Windows/Unix path detection.
|
|
30
|
+
const RE_ABSOLUTE_PATH = /^[A-Za-z]:[\\/]/;
|
|
31
|
+
|
|
32
|
+
// ── Dev event types (mirrors AcpEvent in acp.rs) ─────────────────────────────
|
|
33
|
+
|
|
34
|
+
/** A streamed text chunk from the assistant. */
|
|
35
|
+
export interface DevEventText {
|
|
36
|
+
content: string;
|
|
37
|
+
type: "text";
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** A tool call the Runnable has initiated (mirrors AcpEvent::ToolCall). */
|
|
41
|
+
export interface DevEventToolCall {
|
|
42
|
+
id: string;
|
|
43
|
+
input: unknown;
|
|
44
|
+
kind: string;
|
|
45
|
+
title: string;
|
|
46
|
+
type: "tool_call";
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** A tool result/update (mirrors AcpEvent::ToolResult). */
|
|
50
|
+
export interface DevEventToolResult {
|
|
51
|
+
id: string;
|
|
52
|
+
output: unknown;
|
|
53
|
+
status: "completed" | "failed" | "in_progress" | "pending";
|
|
54
|
+
type: "tool_result";
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** A fatal error event — stream ends after this. */
|
|
58
|
+
export interface DevEventError {
|
|
59
|
+
message: string;
|
|
60
|
+
type: "error";
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Union of all playground event types. */
|
|
64
|
+
export type DevEvent =
|
|
65
|
+
| DevEventError
|
|
66
|
+
| DevEventText
|
|
67
|
+
| DevEventToolCall
|
|
68
|
+
| DevEventToolResult;
|
|
69
|
+
|
|
70
|
+
// ── Runnable contract ─────────────────────────────────────────────────────────
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* The Runnable interface a developer's module must export as `default` or
|
|
74
|
+
* named `runnable`. This is the input→run→output contract from the object
|
|
75
|
+
* model.
|
|
76
|
+
*
|
|
77
|
+
* The `run` generator yields `DevEvent` objects so the playground can stream
|
|
78
|
+
* assistant text, tool calls, and results to the terminal in real time.
|
|
79
|
+
*/
|
|
80
|
+
export interface Runnable {
|
|
81
|
+
/** Human-readable name shown in the playground header. */
|
|
82
|
+
name: string;
|
|
83
|
+
/**
|
|
84
|
+
* Execute one turn. Receives the conversation history and a ModelClient
|
|
85
|
+
* already pointed at the gateway. Yields `DevEvent` objects as the turn
|
|
86
|
+
* progresses.
|
|
87
|
+
*/
|
|
88
|
+
run(
|
|
89
|
+
messages: ChatMessage[],
|
|
90
|
+
model: ModelClient
|
|
91
|
+
): AsyncGenerator<DevEvent> | Generator<DevEvent>;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// ── Gateway reachability check ────────────────────────────────────────────────
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Probe the gateway at `baseUrl` with a HEAD request.
|
|
98
|
+
*
|
|
99
|
+
* Returns `true` when any HTTP response is received (even 4xx — the gateway
|
|
100
|
+
* is reachable), `false` when the request fails with a network error.
|
|
101
|
+
*/
|
|
102
|
+
export async function probeGateway(baseUrl: string): Promise<boolean> {
|
|
103
|
+
try {
|
|
104
|
+
await fetch(`${baseUrl}/health`, {
|
|
105
|
+
method: "HEAD",
|
|
106
|
+
signal: AbortSignal.timeout(3000),
|
|
107
|
+
});
|
|
108
|
+
return true;
|
|
109
|
+
} catch {
|
|
110
|
+
return false;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// ── Module loader ─────────────────────────────────────────────────────────────
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Dynamically import a Runnable from `entryPath`.
|
|
118
|
+
*
|
|
119
|
+
* The module must export either:
|
|
120
|
+
* - `default` — the Runnable object
|
|
121
|
+
* - `runnable` — the Runnable object
|
|
122
|
+
*
|
|
123
|
+
* Throws when neither export is found or when the loaded value does not look
|
|
124
|
+
* like a Runnable (i.e. has no `run` function).
|
|
125
|
+
*/
|
|
126
|
+
export async function loadRunnable(entryPath: string): Promise<Runnable> {
|
|
127
|
+
// Resolve to an absolute path so dynamic import works regardless of cwd.
|
|
128
|
+
const abs =
|
|
129
|
+
entryPath.startsWith("/") || RE_ABSOLUTE_PATH.test(entryPath)
|
|
130
|
+
? entryPath
|
|
131
|
+
: `${process.cwd()}/${entryPath}`;
|
|
132
|
+
|
|
133
|
+
const mod = (await import(abs)) as Record<string, unknown>;
|
|
134
|
+
|
|
135
|
+
const candidate: unknown = mod.default ?? mod.runnable;
|
|
136
|
+
|
|
137
|
+
if (
|
|
138
|
+
!candidate ||
|
|
139
|
+
typeof (candidate as Record<string, unknown>).run !== "function"
|
|
140
|
+
) {
|
|
141
|
+
throw new Error(
|
|
142
|
+
`[ryu dev] Module at "${entryPath}" must export a Runnable as "default" or "runnable". ` +
|
|
143
|
+
'A Runnable has a "run(messages, model)" generator method.'
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return candidate as Runnable;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// ── Turn renderer ─────────────────────────────────────────────────────────────
|
|
151
|
+
|
|
152
|
+
/** ANSI escape sequences for terminal colours. */
|
|
153
|
+
const ANSI = {
|
|
154
|
+
reset: "\x1b[0m",
|
|
155
|
+
bold: "\x1b[1m",
|
|
156
|
+
cyan: "\x1b[36m",
|
|
157
|
+
yellow: "\x1b[33m",
|
|
158
|
+
green: "\x1b[32m",
|
|
159
|
+
red: "\x1b[31m",
|
|
160
|
+
dim: "\x1b[2m",
|
|
161
|
+
} as const;
|
|
162
|
+
|
|
163
|
+
function printBanner(runnableName: string, gatewayUrl: string): void {
|
|
164
|
+
process.stdout.write(
|
|
165
|
+
[
|
|
166
|
+
"",
|
|
167
|
+
`${ANSI.bold}${ANSI.cyan}ryu dev${ANSI.reset} — local Runnable playground`,
|
|
168
|
+
`${ANSI.dim}runnable : ${runnableName}${ANSI.reset}`,
|
|
169
|
+
`${ANSI.dim}gateway : ${gatewayUrl}${ANSI.reset}`,
|
|
170
|
+
`${ANSI.dim}type "/quit" or Ctrl+C to exit${ANSI.reset}`,
|
|
171
|
+
"",
|
|
172
|
+
].join("\n")
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function printPrompt(): void {
|
|
177
|
+
process.stdout.write(`${ANSI.bold}> ${ANSI.reset}`);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function printAssistantStart(): void {
|
|
181
|
+
process.stdout.write(`\n${ANSI.green}assistant:${ANSI.reset} `);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function printAssistantEnd(): void {
|
|
185
|
+
process.stdout.write("\n");
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function printToolCall(event: DevEventToolCall): void {
|
|
189
|
+
process.stdout.write(
|
|
190
|
+
`\n${ANSI.yellow}tool-call${ANSI.reset} [${event.id}] ${event.title} (${event.kind})`
|
|
191
|
+
);
|
|
192
|
+
if (event.input !== null && event.input !== undefined) {
|
|
193
|
+
process.stdout.write(
|
|
194
|
+
` ${ANSI.dim}${JSON.stringify(event.input)}${ANSI.reset}`
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
process.stdout.write("\n");
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function statusAnsiColor(status: DevEventToolResult["status"]): string {
|
|
201
|
+
if (status === "completed") {
|
|
202
|
+
return ANSI.green;
|
|
203
|
+
}
|
|
204
|
+
if (status === "failed") {
|
|
205
|
+
return ANSI.red;
|
|
206
|
+
}
|
|
207
|
+
return ANSI.dim;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function printToolResult(event: DevEventToolResult): void {
|
|
211
|
+
const statusColor = statusAnsiColor(event.status);
|
|
212
|
+
process.stdout.write(
|
|
213
|
+
`${ANSI.dim}tool-result${ANSI.reset} [${event.id}] ${statusColor}${event.status}${ANSI.reset}`
|
|
214
|
+
);
|
|
215
|
+
if (event.output !== null && event.output !== undefined) {
|
|
216
|
+
process.stdout.write(
|
|
217
|
+
` ${ANSI.dim}${JSON.stringify(event.output)}${ANSI.reset}`
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
process.stdout.write("\n");
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function printError(message: string): void {
|
|
224
|
+
process.stderr.write(`\n${ANSI.red}error:${ANSI.reset} ${message}\n`);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// ── Turn runner ───────────────────────────────────────────────────────────────
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Run one turn against the Runnable and stream events to the terminal.
|
|
231
|
+
*
|
|
232
|
+
* Returns `false` when a fatal DevEventError is encountered (caller should
|
|
233
|
+
* offer the user a chance to retry), `true` on clean completion.
|
|
234
|
+
*/
|
|
235
|
+
export async function runTurn(
|
|
236
|
+
runnable: Runnable,
|
|
237
|
+
messages: ChatMessage[],
|
|
238
|
+
model: ModelClient
|
|
239
|
+
): Promise<boolean> {
|
|
240
|
+
printAssistantStart();
|
|
241
|
+
|
|
242
|
+
const gen = runnable.run(messages, model);
|
|
243
|
+
|
|
244
|
+
let hasError = false;
|
|
245
|
+
|
|
246
|
+
for await (const event of gen) {
|
|
247
|
+
switch (event.type) {
|
|
248
|
+
case "text": {
|
|
249
|
+
process.stdout.write(event.content);
|
|
250
|
+
break;
|
|
251
|
+
}
|
|
252
|
+
case "tool_call": {
|
|
253
|
+
printToolCall(event);
|
|
254
|
+
break;
|
|
255
|
+
}
|
|
256
|
+
case "tool_result": {
|
|
257
|
+
printToolResult(event);
|
|
258
|
+
break;
|
|
259
|
+
}
|
|
260
|
+
case "error": {
|
|
261
|
+
printAssistantEnd();
|
|
262
|
+
printError(event.message);
|
|
263
|
+
hasError = true;
|
|
264
|
+
break;
|
|
265
|
+
}
|
|
266
|
+
default:
|
|
267
|
+
break;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
if (!hasError) {
|
|
272
|
+
printAssistantEnd();
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
return !hasError;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// ── Interactive loop ──────────────────────────────────────────────────────────
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Run the interactive dev playground loop.
|
|
282
|
+
*
|
|
283
|
+
* Reads lines from stdin, passes each to `runnable.run`, streams events to
|
|
284
|
+
* stdout, and maintains conversation history across turns.
|
|
285
|
+
*
|
|
286
|
+
* Exits cleanly on Ctrl+C, EOF, or the `/quit` command.
|
|
287
|
+
*/
|
|
288
|
+
export async function runDevLoop(
|
|
289
|
+
runnable: Runnable,
|
|
290
|
+
model: ModelClient
|
|
291
|
+
): Promise<void> {
|
|
292
|
+
const history: ChatMessage[] = [];
|
|
293
|
+
|
|
294
|
+
const rl = createInterface({
|
|
295
|
+
input: process.stdin,
|
|
296
|
+
output: process.stdout,
|
|
297
|
+
terminal: false,
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
const linePromise = (): Promise<string | null> =>
|
|
301
|
+
new Promise((resolve) => {
|
|
302
|
+
rl.once("line", resolve);
|
|
303
|
+
rl.once("close", () => resolve(null));
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
printPrompt();
|
|
307
|
+
|
|
308
|
+
while (true) {
|
|
309
|
+
const line = await linePromise();
|
|
310
|
+
|
|
311
|
+
if (line === null) {
|
|
312
|
+
// EOF / Ctrl+C
|
|
313
|
+
process.stdout.write("\n");
|
|
314
|
+
break;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
const trimmed = line.trim();
|
|
318
|
+
|
|
319
|
+
if (trimmed === "") {
|
|
320
|
+
printPrompt();
|
|
321
|
+
continue;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
if (trimmed === "/quit" || trimmed === "/exit") {
|
|
325
|
+
process.stdout.write("bye\n");
|
|
326
|
+
break;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
history.push({ role: "user", content: trimmed });
|
|
330
|
+
|
|
331
|
+
let assistantReply = "";
|
|
332
|
+
printAssistantStart();
|
|
333
|
+
|
|
334
|
+
const gen = runnable.run([...history], model);
|
|
335
|
+
|
|
336
|
+
for await (const event of gen) {
|
|
337
|
+
switch (event.type) {
|
|
338
|
+
case "text": {
|
|
339
|
+
process.stdout.write(event.content);
|
|
340
|
+
assistantReply += event.content;
|
|
341
|
+
break;
|
|
342
|
+
}
|
|
343
|
+
case "tool_call": {
|
|
344
|
+
printToolCall(event);
|
|
345
|
+
break;
|
|
346
|
+
}
|
|
347
|
+
case "tool_result": {
|
|
348
|
+
printToolResult(event);
|
|
349
|
+
break;
|
|
350
|
+
}
|
|
351
|
+
case "error": {
|
|
352
|
+
printAssistantEnd();
|
|
353
|
+
printError(event.message);
|
|
354
|
+
// Remove the user message from history on error.
|
|
355
|
+
history.pop();
|
|
356
|
+
assistantReply = "";
|
|
357
|
+
break;
|
|
358
|
+
}
|
|
359
|
+
default:
|
|
360
|
+
break;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
printAssistantEnd();
|
|
365
|
+
|
|
366
|
+
if (assistantReply) {
|
|
367
|
+
history.push({ role: "assistant", content: assistantReply });
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
printPrompt();
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
rl.close();
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// ── commandDev ────────────────────────────────────────────────────────────────
|
|
377
|
+
|
|
378
|
+
/**
|
|
379
|
+
* Entry point for the `ryu dev <entry>` command.
|
|
380
|
+
*
|
|
381
|
+
* 1. Resolves and validates the gateway URL.
|
|
382
|
+
* 2. Probes gateway reachability — hard-exits on failure (no silent fallback).
|
|
383
|
+
* 3. Loads the Runnable module from `entryPath`.
|
|
384
|
+
* 4. Prints the banner and starts the interactive loop.
|
|
385
|
+
*/
|
|
386
|
+
export async function commandDev(entryPath: string): Promise<void> {
|
|
387
|
+
const gatewayUrl = resolveGatewayUrl();
|
|
388
|
+
|
|
389
|
+
// Probe gateway — fail-closed: no fallback.
|
|
390
|
+
process.stdout.write(`checking gateway at ${gatewayUrl} ...\n`);
|
|
391
|
+
const reachable = await probeGateway(gatewayUrl);
|
|
392
|
+
if (!reachable) {
|
|
393
|
+
process.stderr.write(
|
|
394
|
+
[
|
|
395
|
+
"",
|
|
396
|
+
`${ANSI.red}error:${ANSI.reset} gateway not reachable at ${gatewayUrl}`,
|
|
397
|
+
"",
|
|
398
|
+
"The ryu dev playground requires a running Ryu gateway.",
|
|
399
|
+
"Start the gateway with: ryu gateway start",
|
|
400
|
+
"Or set RYU_GATEWAY_URL to point at a remote gateway.",
|
|
401
|
+
"",
|
|
402
|
+
"No provider fallback is attempted. Fix the gateway connection and retry.",
|
|
403
|
+
"",
|
|
404
|
+
].join("\n")
|
|
405
|
+
);
|
|
406
|
+
process.exit(1);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
// Load the Runnable module.
|
|
410
|
+
let runnable: Runnable;
|
|
411
|
+
try {
|
|
412
|
+
runnable = await loadRunnable(entryPath);
|
|
413
|
+
} catch (err) {
|
|
414
|
+
process.stderr.write(`error: ${String(err)}\n`);
|
|
415
|
+
process.exit(1);
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// Build the gateway-mandatory model client (default model — configurable via
|
|
419
|
+
// the Runnable's own defineModel calls, but the client the loop passes is
|
|
420
|
+
// the default one so the playground itself can do health checks).
|
|
421
|
+
const model = new ModelClient("default", { baseUrl: gatewayUrl });
|
|
422
|
+
|
|
423
|
+
printBanner(runnable.name, gatewayUrl);
|
|
424
|
+
await runDevLoop(runnable, model);
|
|
425
|
+
}
|