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