@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,671 @@
|
|
|
1
|
+
import {
|
|
2
|
+
assertAllowedEgressUrl,
|
|
3
|
+
defineModel,
|
|
4
|
+
resolveGatewayToken,
|
|
5
|
+
resolveGatewayUrl
|
|
6
|
+
} from "./chunk-ODFEUVPW.js";
|
|
7
|
+
|
|
8
|
+
// src/runnable/primitives.ts
|
|
9
|
+
function dataUrlToBytes(dataUrl) {
|
|
10
|
+
const match = /^data:([^;,]*)(;base64)?,([\s\S]*)$/.exec(dataUrl);
|
|
11
|
+
if (!match) {
|
|
12
|
+
throw new Error(
|
|
13
|
+
"stt.transcribe expects an `audio` value that is a data: URL (data:<mime>;base64,<data>)"
|
|
14
|
+
);
|
|
15
|
+
}
|
|
16
|
+
const mediaType = match[1] || "application/octet-stream";
|
|
17
|
+
const isBase64 = Boolean(match[2]);
|
|
18
|
+
const payload = match[3] ?? "";
|
|
19
|
+
if (isBase64) {
|
|
20
|
+
const binary = atob(payload);
|
|
21
|
+
const bytes = new Uint8Array(binary.length);
|
|
22
|
+
for (let i = 0; i < binary.length; i++) {
|
|
23
|
+
bytes[i] = binary.charCodeAt(i);
|
|
24
|
+
}
|
|
25
|
+
return { bytes, mediaType };
|
|
26
|
+
}
|
|
27
|
+
return { bytes: new TextEncoder().encode(decodeURIComponent(payload)), mediaType };
|
|
28
|
+
}
|
|
29
|
+
function bytesToDataUrl(bytes, mediaType) {
|
|
30
|
+
let binary = "";
|
|
31
|
+
const chunk = 32768;
|
|
32
|
+
for (let i = 0; i < bytes.length; i += chunk) {
|
|
33
|
+
binary += String.fromCharCode(...bytes.subarray(i, i + chunk));
|
|
34
|
+
}
|
|
35
|
+
return `data:${mediaType};base64,${btoa(binary)}`;
|
|
36
|
+
}
|
|
37
|
+
function httpPrimitiveTransport(options) {
|
|
38
|
+
const base = options.nodeUrl.replace(/\/+$/, "");
|
|
39
|
+
assertAllowedEgressUrl(base);
|
|
40
|
+
const doFetch = options.fetchImpl ?? fetch;
|
|
41
|
+
const authHeader = () => options.token ? { authorization: `Bearer ${options.token}` } : {};
|
|
42
|
+
const failDetail = async (path, res) => {
|
|
43
|
+
const detail = await res.text().catch(() => "");
|
|
44
|
+
return new Error(
|
|
45
|
+
`Ryu primitive call ${path} failed: ${res.status} ${res.statusText}${detail ? ` \u2014 ${detail}` : ""}`
|
|
46
|
+
);
|
|
47
|
+
};
|
|
48
|
+
const post = async (path, body) => {
|
|
49
|
+
const res = await doFetch(`${base}${path}`, {
|
|
50
|
+
method: "POST",
|
|
51
|
+
headers: { "content-type": "application/json", ...authHeader() },
|
|
52
|
+
body: JSON.stringify(body ?? {})
|
|
53
|
+
});
|
|
54
|
+
if (!res.ok) {
|
|
55
|
+
throw await failDetail(path, res);
|
|
56
|
+
}
|
|
57
|
+
const text = await res.text();
|
|
58
|
+
return text ? JSON.parse(text) : void 0;
|
|
59
|
+
};
|
|
60
|
+
const transcribeDirect = async (body) => {
|
|
61
|
+
const input = body ?? {};
|
|
62
|
+
if (!input.audio) {
|
|
63
|
+
throw new Error(
|
|
64
|
+
"stt.transcribe requires an `audio` data: URL (the recorded audio)"
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
const { bytes, mediaType } = dataUrlToBytes(input.audio);
|
|
68
|
+
const form = new FormData();
|
|
69
|
+
form.append(
|
|
70
|
+
"file",
|
|
71
|
+
new Blob([bytes], { type: mediaType || "audio/wav" }),
|
|
72
|
+
input.filename ?? "recording.wav"
|
|
73
|
+
);
|
|
74
|
+
const res = await doFetch(`${base}/api/voice/transcribe`, {
|
|
75
|
+
method: "POST",
|
|
76
|
+
headers: authHeader(),
|
|
77
|
+
body: form
|
|
78
|
+
});
|
|
79
|
+
if (!res.ok) {
|
|
80
|
+
throw await failDetail("/api/voice/transcribe", res);
|
|
81
|
+
}
|
|
82
|
+
const parsed = await res.json();
|
|
83
|
+
return (parsed.text ?? "").trim();
|
|
84
|
+
};
|
|
85
|
+
const speakDirect = async (body) => {
|
|
86
|
+
const res = await doFetch(`${base}/api/voice/speak`, {
|
|
87
|
+
method: "POST",
|
|
88
|
+
headers: { "content-type": "application/json", ...authHeader() },
|
|
89
|
+
body: JSON.stringify(body ?? {})
|
|
90
|
+
});
|
|
91
|
+
if (!res.ok) {
|
|
92
|
+
throw await failDetail("/api/voice/speak", res);
|
|
93
|
+
}
|
|
94
|
+
const bytes = new Uint8Array(await res.arrayBuffer());
|
|
95
|
+
const mediaType = res.headers.get("content-type") || "audio/wav";
|
|
96
|
+
return bytesToDataUrl(bytes, mediaType);
|
|
97
|
+
};
|
|
98
|
+
const generateImageDirect = async (body) => {
|
|
99
|
+
const res = await doFetch(`${base}/api/images/generate`, {
|
|
100
|
+
method: "POST",
|
|
101
|
+
headers: { "content-type": "application/json", ...authHeader() },
|
|
102
|
+
body: JSON.stringify(body ?? {})
|
|
103
|
+
});
|
|
104
|
+
if (!res.ok) {
|
|
105
|
+
throw await failDetail("/api/images/generate", res);
|
|
106
|
+
}
|
|
107
|
+
const parsed = await res.json();
|
|
108
|
+
return (parsed.data ?? []).map(
|
|
109
|
+
(item) => item.url ? item.url : `data:image/png;base64,${item.b64_json ?? ""}`
|
|
110
|
+
);
|
|
111
|
+
};
|
|
112
|
+
return {
|
|
113
|
+
bridge(method, args) {
|
|
114
|
+
if (!options.pluginId) {
|
|
115
|
+
return Promise.reject(
|
|
116
|
+
new Error(
|
|
117
|
+
`bridge primitive "${method}" requires a pluginId (the /api/plugins/:id/host caller identity)`
|
|
118
|
+
)
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
return post(`/api/plugins/${options.pluginId}/host`, { method, args });
|
|
122
|
+
},
|
|
123
|
+
direct(path, body) {
|
|
124
|
+
if (path === "/api/voice/transcribe") {
|
|
125
|
+
return transcribeDirect(body);
|
|
126
|
+
}
|
|
127
|
+
if (path === "/api/voice/speak") {
|
|
128
|
+
return speakDirect(body);
|
|
129
|
+
}
|
|
130
|
+
if (path === "/api/images/generate") {
|
|
131
|
+
return generateImageDirect(body);
|
|
132
|
+
}
|
|
133
|
+
return post(path, body);
|
|
134
|
+
},
|
|
135
|
+
capability(cap, body) {
|
|
136
|
+
return post(`/api/host/capability/${cap}`, body);
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
var PRIMITIVE_BINDINGS = {
|
|
141
|
+
// Bridge families (existing `PluginHookBridge`).
|
|
142
|
+
"engines.complete": {
|
|
143
|
+
transport: "bridge",
|
|
144
|
+
method: "model.complete",
|
|
145
|
+
grant: "hook:side-model"
|
|
146
|
+
},
|
|
147
|
+
// Host-direct media data-path (the host holds the node token; returns data: URLs).
|
|
148
|
+
"image.generate": {
|
|
149
|
+
transport: "direct",
|
|
150
|
+
path: "/api/images/generate",
|
|
151
|
+
grant: "media:generate"
|
|
152
|
+
},
|
|
153
|
+
"tts.speak": {
|
|
154
|
+
transport: "direct",
|
|
155
|
+
path: "/api/voice/speak",
|
|
156
|
+
grant: "media:generate"
|
|
157
|
+
},
|
|
158
|
+
"stt.transcribe": {
|
|
159
|
+
transport: "direct",
|
|
160
|
+
path: "/api/voice/transcribe",
|
|
161
|
+
grant: "media:transcribe"
|
|
162
|
+
},
|
|
163
|
+
// Broker capabilities — no rpc family yet (@requires-grant).
|
|
164
|
+
"rag.retrieve": { transport: "broker", capability: "rag" },
|
|
165
|
+
"rag.embed": { transport: "broker", capability: "rag" },
|
|
166
|
+
"rag.rerank": { transport: "broker", capability: "rag" },
|
|
167
|
+
"memory.recall": { transport: "broker", capability: "memory" },
|
|
168
|
+
"memory.store": { transport: "broker", capability: "memory" },
|
|
169
|
+
"realtime.broadcast": { transport: "broker", capability: "realtime" },
|
|
170
|
+
"realtime.subscribe": { transport: "broker", capability: "realtime" },
|
|
171
|
+
"durable.checkpoint": { transport: "broker", capability: "durable" },
|
|
172
|
+
"durable.resume": { transport: "broker", capability: "durable" },
|
|
173
|
+
"engines.embed": { transport: "broker", capability: "engines" }
|
|
174
|
+
};
|
|
175
|
+
function brokerCall(transport, cap, op, input) {
|
|
176
|
+
return transport.capability(cap, { op, input });
|
|
177
|
+
}
|
|
178
|
+
function createPrimitives(transport) {
|
|
179
|
+
return {
|
|
180
|
+
rag: {
|
|
181
|
+
retrieve: (input) => brokerCall(transport, "rag", "retrieve", input),
|
|
182
|
+
embed: (input) => brokerCall(transport, "rag", "embed", input),
|
|
183
|
+
rerank: (input) => brokerCall(transport, "rag", "rerank", input)
|
|
184
|
+
},
|
|
185
|
+
memory: {
|
|
186
|
+
recall: (input) => brokerCall(transport, "memory", "recall", input),
|
|
187
|
+
store: (input) => brokerCall(transport, "memory", "store", input)
|
|
188
|
+
},
|
|
189
|
+
realtime: {
|
|
190
|
+
broadcast: (input) => brokerCall(transport, "realtime", "broadcast", input).then(
|
|
191
|
+
() => void 0
|
|
192
|
+
),
|
|
193
|
+
subscribe: (input) => brokerCall(
|
|
194
|
+
transport,
|
|
195
|
+
"realtime",
|
|
196
|
+
"subscribe",
|
|
197
|
+
input
|
|
198
|
+
)
|
|
199
|
+
},
|
|
200
|
+
durable: {
|
|
201
|
+
checkpoint: (input) => brokerCall(transport, "durable", "checkpoint", input),
|
|
202
|
+
resume: (input) => brokerCall(transport, "durable", "resume", input)
|
|
203
|
+
},
|
|
204
|
+
engines: {
|
|
205
|
+
// Bridge family: model.complete → host.sideModel (wire keys are snake_case).
|
|
206
|
+
complete: (input) => transport.bridge("model.complete", {
|
|
207
|
+
prompt: input.prompt,
|
|
208
|
+
system: input.system,
|
|
209
|
+
model: input.model,
|
|
210
|
+
model_pref_key: input.modelPrefKey,
|
|
211
|
+
effort: input.effort
|
|
212
|
+
}),
|
|
213
|
+
embed: (input) => brokerCall(transport, "engines", "embed", input)
|
|
214
|
+
},
|
|
215
|
+
tts: {
|
|
216
|
+
speak: (input) => transport.direct("/api/voice/speak", input)
|
|
217
|
+
},
|
|
218
|
+
stt: {
|
|
219
|
+
transcribe: (input) => transport.direct("/api/voice/transcribe", input)
|
|
220
|
+
},
|
|
221
|
+
image: {
|
|
222
|
+
generate: (input) => transport.direct("/api/images/generate", input)
|
|
223
|
+
}
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// src/agent/model-call.ts
|
|
228
|
+
var CHAT_COMPLETIONS_PATH = "/v1/chat/completions";
|
|
229
|
+
function normalizeBaseUrl(baseUrl) {
|
|
230
|
+
return baseUrl.endsWith("/") ? baseUrl.slice(0, -1) : baseUrl;
|
|
231
|
+
}
|
|
232
|
+
async function callModelWithTools(options) {
|
|
233
|
+
const base = normalizeBaseUrl(options.baseUrl);
|
|
234
|
+
assertAllowedEgressUrl(base);
|
|
235
|
+
const body = {
|
|
236
|
+
model: options.model,
|
|
237
|
+
messages: options.messages
|
|
238
|
+
};
|
|
239
|
+
if (options.tools && options.tools.length > 0) {
|
|
240
|
+
body.tools = options.tools;
|
|
241
|
+
if (options.toolChoice) {
|
|
242
|
+
body.tool_choice = options.toolChoice;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
const headers = {
|
|
246
|
+
"content-type": "application/json",
|
|
247
|
+
// Force the gateway's plain-completion branch so our own tool_calls are
|
|
248
|
+
// returned verbatim on Composio-on managed nodes.
|
|
249
|
+
"x-ryu-raw-tools": "on"
|
|
250
|
+
};
|
|
251
|
+
if (options.token) {
|
|
252
|
+
headers.authorization = `Bearer ${options.token}`;
|
|
253
|
+
}
|
|
254
|
+
const res = await fetch(`${base}${CHAT_COMPLETIONS_PATH}`, {
|
|
255
|
+
method: "POST",
|
|
256
|
+
headers,
|
|
257
|
+
body: JSON.stringify(body),
|
|
258
|
+
signal: options.signal
|
|
259
|
+
});
|
|
260
|
+
if (!res.ok) {
|
|
261
|
+
const text = await res.text().catch(() => "");
|
|
262
|
+
throw new Error(
|
|
263
|
+
`[ryu-sdk] gateway ${res.status} ${res.statusText} at ${base}${CHAT_COMPLETIONS_PATH}${text ? `: ${text}` : ""}`
|
|
264
|
+
);
|
|
265
|
+
}
|
|
266
|
+
const json = await res.json();
|
|
267
|
+
const choice = json.choices?.[0];
|
|
268
|
+
const rawMessage = choice?.message;
|
|
269
|
+
const message = {
|
|
270
|
+
role: "assistant",
|
|
271
|
+
content: rawMessage?.content ?? null,
|
|
272
|
+
...rawMessage?.tool_calls && rawMessage.tool_calls.length > 0 ? { tool_calls: rawMessage.tool_calls } : {}
|
|
273
|
+
};
|
|
274
|
+
const usage = json.usage ? {
|
|
275
|
+
promptTokens: json.usage.prompt_tokens ?? 0,
|
|
276
|
+
completionTokens: json.usage.completion_tokens ?? 0,
|
|
277
|
+
totalTokens: json.usage.total_tokens ?? 0
|
|
278
|
+
} : void 0;
|
|
279
|
+
return {
|
|
280
|
+
message,
|
|
281
|
+
finishReason: choice?.finish_reason ?? null,
|
|
282
|
+
usage
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// src/agent/tools.ts
|
|
287
|
+
function ryuTool(id, opts = {}) {
|
|
288
|
+
return {
|
|
289
|
+
kind: "remote",
|
|
290
|
+
id,
|
|
291
|
+
description: opts.description,
|
|
292
|
+
parameters: opts.parameters
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
function isLocalTool(tool) {
|
|
296
|
+
return tool.kind === "tool";
|
|
297
|
+
}
|
|
298
|
+
var PERMISSIVE_OBJECT_SCHEMA = {
|
|
299
|
+
type: "object",
|
|
300
|
+
additionalProperties: true
|
|
301
|
+
};
|
|
302
|
+
function normalize(baseUrl) {
|
|
303
|
+
return baseUrl.endsWith("/") ? baseUrl.slice(0, -1) : baseUrl;
|
|
304
|
+
}
|
|
305
|
+
function authHeaders(token) {
|
|
306
|
+
const headers = {
|
|
307
|
+
"content-type": "application/json"
|
|
308
|
+
};
|
|
309
|
+
if (token) {
|
|
310
|
+
headers.authorization = `Bearer ${token}`;
|
|
311
|
+
}
|
|
312
|
+
return headers;
|
|
313
|
+
}
|
|
314
|
+
async function resolveToolDefs(tools, ctx) {
|
|
315
|
+
const defs = [];
|
|
316
|
+
for (const [name, tool] of Object.entries(tools)) {
|
|
317
|
+
if (isLocalTool(tool)) {
|
|
318
|
+
defs.push({
|
|
319
|
+
type: "function",
|
|
320
|
+
function: {
|
|
321
|
+
name,
|
|
322
|
+
description: tool.name,
|
|
323
|
+
parameters: tool.schema
|
|
324
|
+
}
|
|
325
|
+
});
|
|
326
|
+
continue;
|
|
327
|
+
}
|
|
328
|
+
let description = tool.description;
|
|
329
|
+
if (!description) {
|
|
330
|
+
description = await describeRemoteTool(tool.id, ctx);
|
|
331
|
+
}
|
|
332
|
+
defs.push({
|
|
333
|
+
type: "function",
|
|
334
|
+
function: {
|
|
335
|
+
name,
|
|
336
|
+
description: description ?? tool.id,
|
|
337
|
+
parameters: tool.parameters ?? PERMISSIVE_OBJECT_SCHEMA
|
|
338
|
+
}
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
return defs;
|
|
342
|
+
}
|
|
343
|
+
async function describeRemoteTool(id, ctx) {
|
|
344
|
+
const url = `${normalize(ctx.coreBaseUrl)}/api/tools/describe?id=${encodeURIComponent(id)}`;
|
|
345
|
+
try {
|
|
346
|
+
const res = await fetch(url, {
|
|
347
|
+
headers: authHeaders(ctx.coreToken),
|
|
348
|
+
signal: ctx.signal
|
|
349
|
+
});
|
|
350
|
+
if (!res.ok) {
|
|
351
|
+
return void 0;
|
|
352
|
+
}
|
|
353
|
+
const json = await res.json();
|
|
354
|
+
return json.description || json.name || void 0;
|
|
355
|
+
} catch {
|
|
356
|
+
return void 0;
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
async function executeTool(name, argsJson, tools, ctx) {
|
|
360
|
+
const tool = tools[name];
|
|
361
|
+
if (!tool) {
|
|
362
|
+
throw new Error(`[ryu-sdk] model called unknown tool "${name}"`);
|
|
363
|
+
}
|
|
364
|
+
const args = parseArgs(argsJson, name);
|
|
365
|
+
if (isLocalTool(tool)) {
|
|
366
|
+
const output = await tool.run(
|
|
367
|
+
args,
|
|
368
|
+
ctx.runnableContext
|
|
369
|
+
);
|
|
370
|
+
return { output };
|
|
371
|
+
}
|
|
372
|
+
if (!ctx.agentId) {
|
|
373
|
+
throw new Error(
|
|
374
|
+
`[ryu-sdk] remote tool "${name}" (${tool.id}) requires an agentId \u2014 set it on the Agent config so Core can govern the call`
|
|
375
|
+
);
|
|
376
|
+
}
|
|
377
|
+
const url = `${normalize(ctx.coreBaseUrl)}/api/mcp/tools/call`;
|
|
378
|
+
const res = await fetch(url, {
|
|
379
|
+
method: "POST",
|
|
380
|
+
headers: authHeaders(ctx.coreToken),
|
|
381
|
+
signal: ctx.signal,
|
|
382
|
+
body: JSON.stringify({
|
|
383
|
+
tool: tool.id,
|
|
384
|
+
arguments: args,
|
|
385
|
+
agent_id: ctx.agentId,
|
|
386
|
+
...ctx.userId ? { user_id: ctx.userId } : {}
|
|
387
|
+
})
|
|
388
|
+
});
|
|
389
|
+
if (!res.ok) {
|
|
390
|
+
const text = await res.text().catch(() => "");
|
|
391
|
+
throw new Error(
|
|
392
|
+
`[ryu-sdk] Core tools/call ${res.status} for "${tool.id}"${text ? `: ${text}` : ""}`
|
|
393
|
+
);
|
|
394
|
+
}
|
|
395
|
+
const json = await res.json();
|
|
396
|
+
if (json.ok === false) {
|
|
397
|
+
throw new Error(
|
|
398
|
+
`[ryu-sdk] tool "${tool.id}" failed: ${json.error ?? "unknown error"}`
|
|
399
|
+
);
|
|
400
|
+
}
|
|
401
|
+
return { output: json.output };
|
|
402
|
+
}
|
|
403
|
+
function parseArgs(argsJson, name) {
|
|
404
|
+
const trimmed = (argsJson ?? "").trim();
|
|
405
|
+
if (trimmed === "") {
|
|
406
|
+
return {};
|
|
407
|
+
}
|
|
408
|
+
try {
|
|
409
|
+
return JSON.parse(trimmed);
|
|
410
|
+
} catch {
|
|
411
|
+
throw new Error(
|
|
412
|
+
`[ryu-sdk] tool "${name}" arguments were not valid JSON: ${argsJson}`
|
|
413
|
+
);
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
var ELICITATION_KEY = "__ryu_elicitation__";
|
|
417
|
+
function detectElicitation(output) {
|
|
418
|
+
if (typeof output !== "object" || output === null) {
|
|
419
|
+
return null;
|
|
420
|
+
}
|
|
421
|
+
const envelope = output[ELICITATION_KEY];
|
|
422
|
+
if (typeof envelope !== "object" || envelope === null) {
|
|
423
|
+
return null;
|
|
424
|
+
}
|
|
425
|
+
return envelope;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
// src/agent/loop.ts
|
|
429
|
+
function safeParse(json) {
|
|
430
|
+
try {
|
|
431
|
+
return JSON.parse(json);
|
|
432
|
+
} catch {
|
|
433
|
+
return json;
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
function addUsage(a, b) {
|
|
437
|
+
if (!(a || b)) {
|
|
438
|
+
return void 0;
|
|
439
|
+
}
|
|
440
|
+
return {
|
|
441
|
+
promptTokens: (a?.promptTokens ?? 0) + (b?.promptTokens ?? 0),
|
|
442
|
+
completionTokens: (a?.completionTokens ?? 0) + (b?.completionTokens ?? 0),
|
|
443
|
+
totalTokens: (a?.totalTokens ?? 0) + (b?.totalTokens ?? 0)
|
|
444
|
+
};
|
|
445
|
+
}
|
|
446
|
+
function findElicitation(output) {
|
|
447
|
+
const direct = detectElicitation(output);
|
|
448
|
+
if (direct) {
|
|
449
|
+
return direct;
|
|
450
|
+
}
|
|
451
|
+
if (typeof output === "string") {
|
|
452
|
+
return detectElicitation(safeParse(output));
|
|
453
|
+
}
|
|
454
|
+
return null;
|
|
455
|
+
}
|
|
456
|
+
async function* runAgentLoop(config) {
|
|
457
|
+
const messages = [...config.messages];
|
|
458
|
+
let toolDefs;
|
|
459
|
+
try {
|
|
460
|
+
toolDefs = await resolveToolDefs(config.tools, config.toolCtx);
|
|
461
|
+
} catch (err) {
|
|
462
|
+
yield { type: "error", message: describeError(err) };
|
|
463
|
+
return;
|
|
464
|
+
}
|
|
465
|
+
let usage;
|
|
466
|
+
let lastText = "";
|
|
467
|
+
for (let step = 1; step <= config.maxSteps; step++) {
|
|
468
|
+
let result;
|
|
469
|
+
try {
|
|
470
|
+
result = await callModelWithTools({
|
|
471
|
+
baseUrl: config.gatewayBaseUrl,
|
|
472
|
+
token: config.gatewayToken,
|
|
473
|
+
model: config.model,
|
|
474
|
+
messages,
|
|
475
|
+
tools: toolDefs.length > 0 ? toolDefs : void 0,
|
|
476
|
+
toolChoice: toolDefs.length > 0 ? "auto" : void 0,
|
|
477
|
+
signal: config.signal
|
|
478
|
+
});
|
|
479
|
+
} catch (err) {
|
|
480
|
+
yield { type: "error", message: describeError(err) };
|
|
481
|
+
return;
|
|
482
|
+
}
|
|
483
|
+
usage = addUsage(usage, result.usage);
|
|
484
|
+
const assistant = result.message;
|
|
485
|
+
messages.push(assistant);
|
|
486
|
+
if (assistant.content) {
|
|
487
|
+
lastText = assistant.content;
|
|
488
|
+
yield { type: "text", content: assistant.content };
|
|
489
|
+
}
|
|
490
|
+
const toolCalls = assistant.tool_calls ?? [];
|
|
491
|
+
if (toolCalls.length === 0) {
|
|
492
|
+
yield { type: "result", text: lastText, steps: step, usage };
|
|
493
|
+
return;
|
|
494
|
+
}
|
|
495
|
+
const paused = yield* runToolCalls(toolCalls, messages, config);
|
|
496
|
+
if (paused) {
|
|
497
|
+
return;
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
yield { type: "result", text: lastText, steps: config.maxSteps, usage };
|
|
501
|
+
}
|
|
502
|
+
async function* runToolCalls(toolCalls, messages, config) {
|
|
503
|
+
for (const call of toolCalls) {
|
|
504
|
+
const name = call.function.name;
|
|
505
|
+
const input = safeParse(call.function.arguments);
|
|
506
|
+
yield { type: "tool_call", id: call.id, name, input };
|
|
507
|
+
let output;
|
|
508
|
+
try {
|
|
509
|
+
const res = await executeTool(
|
|
510
|
+
name,
|
|
511
|
+
call.function.arguments,
|
|
512
|
+
config.tools,
|
|
513
|
+
config.toolCtx
|
|
514
|
+
);
|
|
515
|
+
output = res.output;
|
|
516
|
+
} catch (err) {
|
|
517
|
+
const errPayload = { error: describeError(err) };
|
|
518
|
+
messages.push({
|
|
519
|
+
role: "tool",
|
|
520
|
+
tool_call_id: call.id,
|
|
521
|
+
content: JSON.stringify(errPayload)
|
|
522
|
+
});
|
|
523
|
+
yield { type: "tool_result", id: call.id, name, output: errPayload };
|
|
524
|
+
continue;
|
|
525
|
+
}
|
|
526
|
+
const elicitation = findElicitation(output);
|
|
527
|
+
if (elicitation) {
|
|
528
|
+
yield {
|
|
529
|
+
type: "auth_required",
|
|
530
|
+
tool: name,
|
|
531
|
+
url: elicitation.url,
|
|
532
|
+
message: elicitation.message
|
|
533
|
+
};
|
|
534
|
+
return true;
|
|
535
|
+
}
|
|
536
|
+
messages.push({
|
|
537
|
+
role: "tool",
|
|
538
|
+
tool_call_id: call.id,
|
|
539
|
+
content: typeof output === "string" ? output : JSON.stringify(output)
|
|
540
|
+
});
|
|
541
|
+
yield { type: "tool_result", id: call.id, name, output };
|
|
542
|
+
}
|
|
543
|
+
return false;
|
|
544
|
+
}
|
|
545
|
+
function describeError(err) {
|
|
546
|
+
return err instanceof Error ? err.message : String(err);
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
// src/agent/agent.ts
|
|
550
|
+
var DEFAULT_MAX_STEPS = 10;
|
|
551
|
+
var DEFAULT_CORE_URL = "http://127.0.0.1:7980";
|
|
552
|
+
function env(key) {
|
|
553
|
+
const value = globalThis.process?.env?.[key];
|
|
554
|
+
return value && value !== "" ? value : void 0;
|
|
555
|
+
}
|
|
556
|
+
function lazyGatewayClient(model, baseUrl, token) {
|
|
557
|
+
let client = null;
|
|
558
|
+
const get = () => {
|
|
559
|
+
client ??= defineModel(model, { baseUrl, token });
|
|
560
|
+
return client;
|
|
561
|
+
};
|
|
562
|
+
return {
|
|
563
|
+
chat: (messages) => get().chat(messages),
|
|
564
|
+
stream: (messages) => get().stream(messages)
|
|
565
|
+
};
|
|
566
|
+
}
|
|
567
|
+
var Agent = class {
|
|
568
|
+
config;
|
|
569
|
+
constructor(config) {
|
|
570
|
+
this.config = config;
|
|
571
|
+
}
|
|
572
|
+
/** Assemble the loop config for a given prompt. */
|
|
573
|
+
buildLoopConfig(prompt, signal) {
|
|
574
|
+
const gatewayBaseUrl = this.config.node?.baseUrl ?? resolveGatewayUrl();
|
|
575
|
+
const gatewayToken = this.config.node?.token ?? resolveGatewayToken();
|
|
576
|
+
const coreBaseUrl = this.config.core?.baseUrl ?? env("RYU_CORE_URL") ?? DEFAULT_CORE_URL;
|
|
577
|
+
const coreToken = this.config.core?.token ?? env("RYU_TOKEN");
|
|
578
|
+
const messages = [];
|
|
579
|
+
if (this.config.instructions) {
|
|
580
|
+
messages.push({ role: "system", content: this.config.instructions });
|
|
581
|
+
}
|
|
582
|
+
messages.push({ role: "user", content: prompt });
|
|
583
|
+
const toolCtx = {
|
|
584
|
+
agentId: this.config.agentId,
|
|
585
|
+
coreBaseUrl,
|
|
586
|
+
coreToken,
|
|
587
|
+
userId: this.config.userId,
|
|
588
|
+
signal,
|
|
589
|
+
runnableContext: {
|
|
590
|
+
gateway: lazyGatewayClient(
|
|
591
|
+
this.config.model,
|
|
592
|
+
gatewayBaseUrl,
|
|
593
|
+
gatewayToken
|
|
594
|
+
),
|
|
595
|
+
signal,
|
|
596
|
+
// Mount the composable primitive surface only when a plugin id is
|
|
597
|
+
// present — the bridge families authenticate it, so we never attach a
|
|
598
|
+
// half-wired transport (advisor guidance / §6b).
|
|
599
|
+
...this.config.pluginId ? createPrimitives(
|
|
600
|
+
httpPrimitiveTransport({
|
|
601
|
+
nodeUrl: coreBaseUrl,
|
|
602
|
+
token: coreToken,
|
|
603
|
+
pluginId: this.config.pluginId
|
|
604
|
+
})
|
|
605
|
+
) : {}
|
|
606
|
+
}
|
|
607
|
+
};
|
|
608
|
+
return {
|
|
609
|
+
model: this.config.model,
|
|
610
|
+
gatewayBaseUrl,
|
|
611
|
+
gatewayToken,
|
|
612
|
+
messages,
|
|
613
|
+
tools: this.config.tools ?? {},
|
|
614
|
+
toolCtx,
|
|
615
|
+
maxSteps: this.config.maxSteps ?? DEFAULT_MAX_STEPS,
|
|
616
|
+
signal
|
|
617
|
+
};
|
|
618
|
+
}
|
|
619
|
+
/** Stream loop events (text / tool_call / tool_result / auth_required / …). */
|
|
620
|
+
stream(prompt, signal) {
|
|
621
|
+
return runAgentLoop(this.buildLoopConfig(prompt, signal));
|
|
622
|
+
}
|
|
623
|
+
/** Run to completion and return the final text, step count, and usage. */
|
|
624
|
+
async generate(prompt, signal) {
|
|
625
|
+
let text = "";
|
|
626
|
+
let steps = 0;
|
|
627
|
+
let usage;
|
|
628
|
+
let authRequired;
|
|
629
|
+
for await (const event of this.stream(prompt, signal)) {
|
|
630
|
+
if (event.type === "result") {
|
|
631
|
+
text = event.text;
|
|
632
|
+
steps = event.steps;
|
|
633
|
+
usage = event.usage;
|
|
634
|
+
} else if (event.type === "auth_required") {
|
|
635
|
+
authRequired = event;
|
|
636
|
+
} else if (event.type === "error") {
|
|
637
|
+
throw new Error(
|
|
638
|
+
`[ryu-sdk] agent "${this.config.name}": ${event.message}`
|
|
639
|
+
);
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
return { text, steps, usage, authRequired };
|
|
643
|
+
}
|
|
644
|
+
};
|
|
645
|
+
function createAgent(config) {
|
|
646
|
+
return new Agent(config);
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
// src/agent/query.ts
|
|
650
|
+
function query(input) {
|
|
651
|
+
const agent = new Agent({
|
|
652
|
+
name: input.options.name ?? "agent",
|
|
653
|
+
...input.options
|
|
654
|
+
});
|
|
655
|
+
return agent.stream(input.prompt);
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
export {
|
|
659
|
+
httpPrimitiveTransport,
|
|
660
|
+
PRIMITIVE_BINDINGS,
|
|
661
|
+
createPrimitives,
|
|
662
|
+
callModelWithTools,
|
|
663
|
+
ryuTool,
|
|
664
|
+
resolveToolDefs,
|
|
665
|
+
executeTool,
|
|
666
|
+
detectElicitation,
|
|
667
|
+
runAgentLoop,
|
|
668
|
+
Agent,
|
|
669
|
+
createAgent,
|
|
670
|
+
query
|
|
671
|
+
};
|