runbios-mcp 0.2.8-dev.210 → 0.2.8-dev.213

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.
@@ -0,0 +1,679 @@
1
+ import { BiosClient } from "../api-client.js";
2
+ import { sessionLaunchGates } from "../config.js";
3
+ import { consumeApprovalToken, issueApprovalToken, verifyApprovalToken, } from "../assistant/approval.js";
4
+ import { AssistantUpstreamError, streamAssistantModel, } from "../assistant/model.js";
5
+ import { createAssistantToolRuntime, openAITool, rankTools, toolRisk, } from "../assistant/tool-runtime.js";
6
+ const DEFAULT_MODEL = "bios-adaptive";
7
+ const MAX_MESSAGES = 1_000;
8
+ const MAX_TOOL_ROUNDS = 8;
9
+ const MAX_TOOL_CALLS = 12;
10
+ const MAX_TOOL_RESULT_CHARS = 64_000;
11
+ const EXCLUDED_ASSISTANT_TOOLS = new Set(["get_platform_guide", "chat_with_inference", "upload_dataset"]);
12
+ const SEARCH_TOOL_NAME = "search_platform_tools";
13
+ class AssistantRequestError extends Error {
14
+ status;
15
+ code;
16
+ constructor(status, code, message) {
17
+ super(message);
18
+ this.status = status;
19
+ this.code = code;
20
+ }
21
+ }
22
+ function jsonError(res, status, code, message) {
23
+ res.status(status).json({ error: { code, message } });
24
+ }
25
+ function contentParts(value) {
26
+ if (!Array.isArray(value) || value.length === 0 || value.length > 32)
27
+ return null;
28
+ const parts = [];
29
+ for (const raw of value) {
30
+ if (!raw || typeof raw !== "object")
31
+ return null;
32
+ const part = raw;
33
+ if (part.type === "text" && typeof part.text === "string") {
34
+ parts.push({ type: "text", text: part.text });
35
+ continue;
36
+ }
37
+ if (part.type === "image_url" && part.image_url && typeof part.image_url === "object") {
38
+ const url = part.image_url.url;
39
+ if (typeof url !== "string" || !(url.startsWith("https://") || url.startsWith("data:image/")))
40
+ return null;
41
+ parts.push({ type: "image_url", image_url: { url } });
42
+ continue;
43
+ }
44
+ if (part.type === "video_url" && part.video_url && typeof part.video_url === "object") {
45
+ const url = part.video_url.url;
46
+ if (typeof url !== "string" || !(url.startsWith("https://") || url.startsWith("data:video/")))
47
+ return null;
48
+ parts.push({ type: "video_url", video_url: { url } });
49
+ continue;
50
+ }
51
+ return null;
52
+ }
53
+ return parts;
54
+ }
55
+ function toolCalls(value) {
56
+ if (!Array.isArray(value) || value.length === 0 || value.length > 16)
57
+ return null;
58
+ const calls = [];
59
+ for (const raw of value) {
60
+ if (!raw || typeof raw !== "object")
61
+ return null;
62
+ const call = raw;
63
+ const fn = call.function && typeof call.function === "object"
64
+ ? call.function
65
+ : null;
66
+ if (typeof call.id !== "string" || call.id.length < 1 || call.id.length > 256
67
+ || !fn || typeof fn.name !== "string" || !/^[A-Za-z0-9_-]{1,128}$/.test(fn.name)
68
+ || typeof fn.arguments !== "string")
69
+ return null;
70
+ calls.push({ id: call.id, type: "function", function: { name: fn.name, arguments: fn.arguments } });
71
+ }
72
+ return calls;
73
+ }
74
+ export function normalizeAssistantMessages(value) {
75
+ if (!Array.isArray(value) || value.length === 0 || value.length > MAX_MESSAGES) {
76
+ throw new AssistantRequestError(400, "invalid_messages", `messages must contain between 1 and ${MAX_MESSAGES} entries.`);
77
+ }
78
+ const messages = [];
79
+ for (const raw of value) {
80
+ if (!raw || typeof raw !== "object")
81
+ throw new AssistantRequestError(400, "invalid_messages", "Every message must be an object.");
82
+ const message = raw;
83
+ if (message.role === "user") {
84
+ if (typeof message.content === "string")
85
+ messages.push({ role: "user", content: message.content });
86
+ else {
87
+ const parts = contentParts(message.content);
88
+ if (!parts)
89
+ throw new AssistantRequestError(400, "invalid_messages", "User content parts must be text, image_url, or video_url values.");
90
+ messages.push({ role: "user", content: parts });
91
+ }
92
+ continue;
93
+ }
94
+ if (message.role === "assistant") {
95
+ if (message.content !== null && typeof message.content !== "string") {
96
+ throw new AssistantRequestError(400, "invalid_messages", "Assistant content must be text or null.");
97
+ }
98
+ const calls = message.tool_calls === undefined ? undefined : toolCalls(message.tool_calls);
99
+ if (message.tool_calls !== undefined && !calls)
100
+ throw new AssistantRequestError(400, "invalid_messages", "Assistant tool calls are malformed.");
101
+ messages.push({ role: "assistant", content: message.content, ...(calls ? { tool_calls: calls } : {}) });
102
+ continue;
103
+ }
104
+ if (message.role === "tool") {
105
+ if (typeof message.content !== "string" || typeof message.tool_call_id !== "string" || !message.tool_call_id) {
106
+ throw new AssistantRequestError(400, "invalid_messages", "Tool messages require text content and a tool_call_id.");
107
+ }
108
+ messages.push({ role: "tool", content: message.content, tool_call_id: message.tool_call_id });
109
+ continue;
110
+ }
111
+ throw new AssistantRequestError(400, "invalid_messages", "Only user, assistant, and tool messages are accepted.");
112
+ }
113
+ return messages;
114
+ }
115
+ function mediaCounts(messages) {
116
+ let images = 0;
117
+ let videos = 0;
118
+ for (const message of messages) {
119
+ if (!Array.isArray(message.content))
120
+ continue;
121
+ for (const part of message.content) {
122
+ if (part.type === "image_url")
123
+ images++;
124
+ if (part.type === "video_url")
125
+ videos++;
126
+ }
127
+ }
128
+ return { images, videos };
129
+ }
130
+ function latestUserText(messages) {
131
+ for (let index = messages.length - 1; index >= 0; index--) {
132
+ const message = messages[index];
133
+ if (message.role !== "user")
134
+ continue;
135
+ if (typeof message.content === "string")
136
+ return message.content;
137
+ if (!Array.isArray(message.content))
138
+ return "";
139
+ return message.content.filter((part) => part.type === "text").map((part) => part.text).join("\n");
140
+ }
141
+ return "";
142
+ }
143
+ function systemPrompt(toolsEnabled) {
144
+ return [
145
+ "You are Run BiOS Assistant. Answer general questions normally and help the signed-in user operate the Run BiOS platform.",
146
+ "Every platform action is restricted to the authenticated organization and workspace. Never invent, infer, or substitute an account or resource ID.",
147
+ "Resource references use @dataset:<id>, @training:<id>, @deployment:<id>, @checkpoint:<id>, and @integration:<id>. Resolve ambiguous resources with list/search tools instead of guessing.",
148
+ toolsEnabled
149
+ ? `Only claim access to tools provided in this request. If the needed tool is not present, call ${SEARCH_TOOL_NAME}; never claim a hidden or unavailable capability.`
150
+ : "No platform tools are available in this turn. Do not claim that an action was performed; explain that platform actions require both a tool-capable model and authorized tools in the current workspace.",
151
+ "Use preflight and status tools before paid operations. State concrete prices, GPU choices, queue terms, and irreversible effects from tool results only.",
152
+ "Ask a concise normal-chat question when required information is missing. Do not call a mutation with guessed values.",
153
+ "A tool request is not success. Report an operation as complete only after its tool result confirms it, and include returned resource IDs.",
154
+ "Web search and third-party connectors are not available unless a corresponding tool is explicitly present. Do not pretend to browse or access email.",
155
+ "Use clear GitHub-flavored Markdown. Tables, fenced code, links, and LaTeX are supported.",
156
+ ].join("\n");
157
+ }
158
+ function resolveEffort(model, requested) {
159
+ const mode = model.reasoning_mode ?? "none";
160
+ if (mode === "none") {
161
+ if (requested && requested !== "none")
162
+ throw new AssistantRequestError(400, "unsupported_effort", "This model does not expose reasoning effort controls.");
163
+ return undefined;
164
+ }
165
+ const advertised = (model.available_efforts ?? []).map((value) => value.toLowerCase());
166
+ const ladder = advertised.length > 0 ? advertised : ["low", "medium", "high"];
167
+ const fallback = model.default_effort && ladder.includes(model.default_effort.toLowerCase())
168
+ ? model.default_effort.toLowerCase()
169
+ : ladder.includes("medium") ? "medium" : ladder[Math.floor((ladder.length - 1) / 2)];
170
+ const effort = (requested || fallback).toLowerCase();
171
+ if (effort === "none" && mode === "optional")
172
+ return "none";
173
+ if (!ladder.includes(effort))
174
+ throw new AssistantRequestError(400, "unsupported_effort", `This model supports: ${ladder.join(", ")}.`);
175
+ return effort;
176
+ }
177
+ function maxTokens(model, effort) {
178
+ const thinking = { minimal: 1_024, low: 4_096, medium: 8_192, high: 16_384, xhigh: 24_576, max: 32_768 };
179
+ const requested = (thinking[effort ?? ""] ?? 0) + 8_192;
180
+ const advertised = Number(model.output_tokens?.max ?? 0);
181
+ if (advertised > 0)
182
+ return Math.max(1, Math.min(advertised, requested || 8_192));
183
+ return requested || 8_192;
184
+ }
185
+ function parseArguments(call) {
186
+ try {
187
+ const value = JSON.parse(call.function.arguments || "{}");
188
+ return value && typeof value === "object" && !Array.isArray(value) ? value : null;
189
+ }
190
+ catch {
191
+ return null;
192
+ }
193
+ }
194
+ function truncateToolResult(value) {
195
+ if (value.length <= MAX_TOOL_RESULT_CHARS)
196
+ return value;
197
+ return `${value.slice(0, MAX_TOOL_RESULT_CHARS)}\n\n[Result truncated by Run BiOS Assistant after ${MAX_TOOL_RESULT_CHARS} characters.]`;
198
+ }
199
+ function resourceKind(toolName, key) {
200
+ const normalized = key.toLowerCase();
201
+ if (normalized.includes("dataset"))
202
+ return "dataset";
203
+ if (normalized.includes("checkpoint"))
204
+ return "checkpoint";
205
+ if (normalized.includes("training") || normalized === "job_id")
206
+ return "training";
207
+ if (normalized.includes("deployment") || normalized.includes("inference"))
208
+ return "deployment";
209
+ if (normalized.includes("integration"))
210
+ return "integration";
211
+ if (normalized.includes("booking") || normalized === "handle")
212
+ return "booking";
213
+ if (normalized !== "id")
214
+ return null;
215
+ if (toolName.includes("dataset"))
216
+ return "dataset";
217
+ if (toolName.includes("checkpoint"))
218
+ return "checkpoint";
219
+ if (toolName.includes("training"))
220
+ return "training";
221
+ if (toolName.includes("inference") || toolName.includes("deployment"))
222
+ return "deployment";
223
+ if (toolName.includes("integration"))
224
+ return "integration";
225
+ if (toolName.includes("booking"))
226
+ return "booking";
227
+ return null;
228
+ }
229
+ export function extractResourceReferences(toolName, text) {
230
+ let value;
231
+ try {
232
+ value = JSON.parse(text);
233
+ }
234
+ catch {
235
+ return [];
236
+ }
237
+ const found = new Map();
238
+ const visit = (node, depth) => {
239
+ if (depth > 6 || found.size >= 24 || !node)
240
+ return;
241
+ if (Array.isArray(node)) {
242
+ for (const child of node)
243
+ visit(child, depth + 1);
244
+ return;
245
+ }
246
+ if (typeof node !== "object")
247
+ return;
248
+ const object = node;
249
+ const label = [object.name, object.display_name, object.label].find((item) => typeof item === "string");
250
+ for (const [key, child] of Object.entries(object)) {
251
+ const kind = resourceKind(toolName, key);
252
+ if (kind && typeof child === "string" && child.length > 0 && child.length <= 512) {
253
+ found.set(`${kind}:${child}`, { kind, id: child, ...(label ? { label } : {}) });
254
+ }
255
+ visit(child, depth + 1);
256
+ }
257
+ };
258
+ visit(value, 0);
259
+ return [...found.values()];
260
+ }
261
+ function unresolvedToolCalls(messages) {
262
+ const calls = new Map();
263
+ const answered = new Set();
264
+ for (const message of messages) {
265
+ for (const call of message.tool_calls ?? [])
266
+ calls.set(call.id, call);
267
+ if (message.role === "tool" && message.tool_call_id)
268
+ answered.add(message.tool_call_id);
269
+ }
270
+ for (const id of answered)
271
+ calls.delete(id);
272
+ return calls;
273
+ }
274
+ function approvalMatchesTranscript(payload, messages) {
275
+ const unresolved = unresolvedToolCalls(messages);
276
+ return payload.calls.every((approved) => {
277
+ const call = unresolved.get(approved.id);
278
+ const args = call ? parseArguments(call) : null;
279
+ return !!call
280
+ && call.function.name === approved.name
281
+ && !!args
282
+ && JSON.stringify(args) === JSON.stringify(approved.arguments)
283
+ && toolRisk(approved.name) === approved.risk;
284
+ });
285
+ }
286
+ function addUsage(total, next) {
287
+ if (!next)
288
+ return;
289
+ total.prompt_tokens = (total.prompt_tokens ?? 0) + (next.prompt_tokens ?? 0);
290
+ total.completion_tokens = (total.completion_tokens ?? 0) + (next.completion_tokens ?? 0);
291
+ total.total_tokens = (total.total_tokens ?? 0) + (next.total_tokens ?? 0);
292
+ }
293
+ function searchTool() {
294
+ return {
295
+ type: "function",
296
+ function: {
297
+ name: SEARCH_TOOL_NAME,
298
+ description: "Find additional Run BiOS platform tools that are authorized for this user. Use this when the current tools do not cover the requested platform operation.",
299
+ parameters: {
300
+ type: "object",
301
+ properties: { query: { type: "string", description: "The capability or operation to find." } },
302
+ required: ["query"],
303
+ additionalProperties: false,
304
+ },
305
+ },
306
+ };
307
+ }
308
+ function sse(res, event, data) {
309
+ if (res.writableEnded)
310
+ return;
311
+ res.write(`event: ${event}\ndata: ${JSON.stringify(data)}\n\n`);
312
+ }
313
+ async function runTool(runtime, toolMap, call, args, res) {
314
+ sse(res, "tool.start", { id: call.id, name: call.function.name, arguments: args });
315
+ let ok = false;
316
+ let text;
317
+ if (!toolMap.has(call.function.name)) {
318
+ text = JSON.stringify({ code: "TOOL_UNAVAILABLE", message: "This tool is not available to this user in the current workspace." });
319
+ }
320
+ else {
321
+ try {
322
+ const result = await runtime.call(call.function.name, args);
323
+ ok = result.ok;
324
+ text = truncateToolResult(result.text);
325
+ }
326
+ catch (error) {
327
+ text = JSON.stringify({
328
+ code: "TOOL_FAILED",
329
+ message: error instanceof Error ? error.message : "The platform tool failed.",
330
+ });
331
+ }
332
+ }
333
+ const resources = extractResourceReferences(call.function.name, text);
334
+ sse(res, "tool.result", { id: call.id, name: call.function.name, ok, content: text, resources });
335
+ return { role: "tool", tool_call_id: call.id, content: text };
336
+ }
337
+ function setupStream(res) {
338
+ res.status(200);
339
+ res.setHeader("Content-Type", "text/event-stream; charset=utf-8");
340
+ res.setHeader("Cache-Control", "no-cache, no-transform");
341
+ res.setHeader("Connection", "keep-alive");
342
+ res.setHeader("X-Accel-Buffering", "no");
343
+ res.flushHeaders();
344
+ }
345
+ export function createAssistantChatHandler(deps) {
346
+ return async function assistantChat(req, res) {
347
+ if (req.header("x-gateway-auth") !== deps.cfg.internalServiceKey
348
+ || req.header("x-auth-type") !== "jwt"
349
+ || !req.header("x-user-id")) {
350
+ jsonError(res, 401, "unauthorized", "A valid console session is required.");
351
+ return;
352
+ }
353
+ const authorization = req.header("authorization") ?? "";
354
+ const userId = req.header("x-user-id") ?? "";
355
+ const workspaceId = req.header("x-workspace-id") ?? "";
356
+ const orgId = req.header("x-org-id") ?? "";
357
+ if (!/^Bearer\s+\S+$/i.test(authorization) || !workspaceId || !orgId) {
358
+ jsonError(res, 400, "workspace_required", "Choose an organization and workspace before using the assistant.");
359
+ return;
360
+ }
361
+ let body;
362
+ let messages;
363
+ let approvalPayload = null;
364
+ try {
365
+ body = (req.body ?? {});
366
+ messages = normalizeAssistantMessages(body.messages);
367
+ const model = (body.model || DEFAULT_MODEL).trim();
368
+ if (!/^[A-Za-z0-9._:/-]{1,200}$/.test(model))
369
+ throw new AssistantRequestError(400, "invalid_model", "Choose a valid serverless model.");
370
+ body.model = model;
371
+ if (body.approval) {
372
+ if (body.approval.decision !== "approve" && body.approval.decision !== "reject") {
373
+ throw new AssistantRequestError(400, "invalid_approval", "Choose approve or reject for the pending action.");
374
+ }
375
+ approvalPayload = verifyApprovalToken(body.approval.token ?? "", deps.cfg.internalServiceKey, { userId, workspaceId, orgId });
376
+ if (!approvalPayload || !approvalMatchesTranscript(approvalPayload, messages)) {
377
+ throw new AssistantRequestError(409, "approval_invalid", "This approval is expired, already used, or no longer matches the pending action.");
378
+ }
379
+ }
380
+ else if (!messages.some((message) => message.role === "user")) {
381
+ throw new AssistantRequestError(400, "user_message_required", "Send a user message to start the assistant.");
382
+ }
383
+ }
384
+ catch (error) {
385
+ if (error instanceof AssistantRequestError)
386
+ jsonError(res, error.status, error.code, error.message);
387
+ else
388
+ jsonError(res, 400, "invalid_request", "The assistant request is malformed.");
389
+ return;
390
+ }
391
+ const client = new BiosClient({
392
+ baseUrl: `http://${deps.cfg.apiGatewayHost}:${deps.cfg.apiGatewayPort}`,
393
+ authHeaders: { Authorization: authorization },
394
+ orgId,
395
+ workspaceId,
396
+ userAgent: "bios-assistant/1.0",
397
+ });
398
+ let modelDetail;
399
+ let resolvedEffort;
400
+ let runtime = null;
401
+ try {
402
+ modelDetail = await client.api(`/api/serverless/catalog/models/${encodeURIComponent(body.model)}`);
403
+ if (!modelDetail?.slug)
404
+ throw new Error("model unavailable");
405
+ const counts = mediaCounts(messages);
406
+ if (counts.images > 0 && modelDetail.supports_vision !== true) {
407
+ throw new AssistantRequestError(400, "vision_unsupported", "The selected model does not support image input.");
408
+ }
409
+ if (counts.videos > 0 && modelDetail.supports_video !== true) {
410
+ throw new AssistantRequestError(400, "video_unsupported", "The selected model does not support video input.");
411
+ }
412
+ if (counts.images > 10 || counts.videos > 8) {
413
+ throw new AssistantRequestError(400, "too_many_attachments", "Use at most 10 images and 8 videos in one conversation request.");
414
+ }
415
+ resolvedEffort = resolveEffort(modelDetail, body.effort);
416
+ if (approvalPayload && modelDetail.supports_tools !== true) {
417
+ throw new AssistantRequestError(409, "approval_model_changed", "Switch back to a tool-capable model or cancel the pending action.");
418
+ }
419
+ if (modelDetail.supports_tools === true) {
420
+ try {
421
+ const [identity, gates] = await Promise.all([
422
+ client.api("/api/api-keys/introspect"),
423
+ sessionLaunchGates(orgId, () => client.api(`/api/organizations/${encodeURIComponent(orgId)}/features`)),
424
+ ]);
425
+ const allowedTools = new Set((identity.allowed_mcp_tools ?? []).filter((name) => !EXCLUDED_ASSISTANT_TOOLS.has(name)));
426
+ runtime = await createAssistantToolRuntime({
427
+ client,
428
+ launchGates: gates,
429
+ allowedTools,
430
+ hooks: {
431
+ onToolCall: (info) => deps.audit.emit("assistant_tool_call", {
432
+ tool: info.tool,
433
+ ok: info.ok,
434
+ ms: info.ms,
435
+ user_id: userId,
436
+ workspace_id: workspaceId,
437
+ ...(info.error ? { error: info.error } : {}),
438
+ }),
439
+ },
440
+ });
441
+ if (runtime.tools.length === 0) {
442
+ await runtime.close();
443
+ runtime = null;
444
+ }
445
+ }
446
+ catch {
447
+ runtime = null;
448
+ deps.audit.emit("assistant_tool_visibility_failed_closed", { user_id: userId, workspace_id: workspaceId });
449
+ if (approvalPayload) {
450
+ throw new AssistantRequestError(503, "tool_access_unavailable", "Tool access could not be verified. Nothing was changed; retry shortly.");
451
+ }
452
+ }
453
+ }
454
+ }
455
+ catch (error) {
456
+ if (error instanceof AssistantRequestError)
457
+ jsonError(res, error.status, error.code, error.message);
458
+ else
459
+ jsonError(res, 400, "model_unavailable", "This serverless model is not available in the selected workspace.");
460
+ return;
461
+ }
462
+ const controller = new AbortController();
463
+ req.once("aborted", () => controller.abort());
464
+ res.once("close", () => { if (!res.writableEnded)
465
+ controller.abort(); });
466
+ setupStream(res);
467
+ const heartbeat = setInterval(() => { if (!res.writableEnded)
468
+ res.write(": keepalive\n\n"); }, 15_000);
469
+ heartbeat.unref();
470
+ const appended = [];
471
+ const transcript = [
472
+ { role: "system", content: systemPrompt(runtime !== null) },
473
+ ...messages,
474
+ ];
475
+ const usage = {};
476
+ const toolMap = new Map((runtime?.tools ?? []).map((tool) => [tool.name, tool]));
477
+ const activeTools = new Map();
478
+ for (const tool of rankTools(runtime?.tools ?? [], latestUserText(messages), 24))
479
+ activeTools.set(tool.name, tool);
480
+ const signatures = new Set();
481
+ const toolCallIds = new Set(messages.flatMap((message) => (message.tool_calls ?? []).map((call) => call.id)));
482
+ let toolCallsMade = 0;
483
+ let rounds = 0;
484
+ let pendingApproval = null;
485
+ const appendToolMessage = (message) => {
486
+ transcript.push(message);
487
+ appended.push(message);
488
+ };
489
+ try {
490
+ if (approvalPayload && body.approval) {
491
+ const consumed = await consumeApprovalToken(deps.store, approvalPayload, body.approval.decision);
492
+ if (!consumed)
493
+ throw new AssistantRequestError(409, "approval_used", "This approval has already been used or expired.");
494
+ deps.audit.emit("assistant_approval_decision", {
495
+ decision: body.approval.decision,
496
+ tools: approvalPayload.calls.map((call) => call.name),
497
+ user_id: userId,
498
+ workspace_id: workspaceId,
499
+ });
500
+ for (const approved of approvalPayload.calls) {
501
+ const wireCall = {
502
+ id: approved.id,
503
+ type: "function",
504
+ function: { name: approved.name, arguments: JSON.stringify(approved.arguments) },
505
+ };
506
+ if (body.approval.decision === "reject") {
507
+ const result = JSON.stringify({ code: "USER_DECLINED", message: "The user declined this action. Nothing was changed." });
508
+ sse(res, "tool.result", { id: approved.id, name: approved.name, ok: false, content: result, resources: [] });
509
+ appendToolMessage({ role: "tool", tool_call_id: approved.id, content: result });
510
+ }
511
+ else {
512
+ appendToolMessage(await runTool(runtime, toolMap, wireCall, approved.arguments, res));
513
+ }
514
+ }
515
+ }
516
+ for (; rounds < MAX_TOOL_ROUNDS; rounds++) {
517
+ const offered = new Set(activeTools.keys());
518
+ const modelTools = runtime
519
+ ? [searchTool(), ...[...activeTools.values()].map(openAITool)]
520
+ : [];
521
+ let transformSent = false;
522
+ sse(res, "assistant.start", { round: rounds + 1 });
523
+ const modelResult = await streamAssistantModel({
524
+ baseUrl: `http://${deps.cfg.apiGatewayHost}:${deps.cfg.apiGatewayPort}`,
525
+ authorization,
526
+ orgId,
527
+ workspaceId,
528
+ model: modelDetail.slug,
529
+ effort: resolvedEffort,
530
+ maxTokens: maxTokens(modelDetail, resolvedEffort),
531
+ messages: transcript,
532
+ tools: modelTools,
533
+ signal: controller.signal,
534
+ fetchImpl: deps.fetchImpl,
535
+ onTextDelta: (text) => sse(res, "assistant.delta", { round: rounds + 1, text }),
536
+ onContextTransform: (value) => {
537
+ if (transformSent)
538
+ return;
539
+ transformSent = true;
540
+ sse(res, "context.compacted", value);
541
+ },
542
+ });
543
+ for (const [index, call] of (modelResult.message.tool_calls ?? []).entries()) {
544
+ const base = call.id || `call_${rounds + 1}_${index}`;
545
+ let unique = base;
546
+ let suffix = 1;
547
+ while (toolCallIds.has(unique))
548
+ unique = `${base}_${rounds + 1}_${suffix++}`;
549
+ call.id = unique;
550
+ toolCallIds.add(unique);
551
+ }
552
+ addUsage(usage, modelResult.usage);
553
+ transcript.push(modelResult.message);
554
+ appended.push(modelResult.message);
555
+ sse(res, "assistant.finish", {
556
+ round: rounds + 1,
557
+ message: modelResult.message,
558
+ finish_reason: modelResult.finishReason,
559
+ usage: modelResult.usage,
560
+ });
561
+ const calls = modelResult.message.tool_calls ?? [];
562
+ if (calls.length === 0 || !runtime)
563
+ break;
564
+ const protectedCalls = [];
565
+ for (const call of calls) {
566
+ toolCallsMade++;
567
+ const args = parseArguments(call);
568
+ if (!args) {
569
+ const result = JSON.stringify({ code: "INVALID_TOOL_ARGUMENTS", message: "The model produced invalid JSON arguments. No tool was called." });
570
+ sse(res, "tool.result", { id: call.id, name: call.function.name, ok: false, content: result, resources: [] });
571
+ appendToolMessage({ role: "tool", tool_call_id: call.id, content: result });
572
+ continue;
573
+ }
574
+ if (toolCallsMade > MAX_TOOL_CALLS) {
575
+ const result = JSON.stringify({ code: "TOOL_LIMIT_REACHED", message: "The assistant reached the per-request tool-call limit. No additional tool was called." });
576
+ sse(res, "tool.result", { id: call.id, name: call.function.name, ok: false, content: result, resources: [] });
577
+ appendToolMessage({ role: "tool", tool_call_id: call.id, content: result });
578
+ continue;
579
+ }
580
+ const signature = `${call.function.name}:${JSON.stringify(args)}`;
581
+ if (signatures.has(signature)) {
582
+ const result = JSON.stringify({ code: "DUPLICATE_TOOL_CALL", message: "The same tool call already ran in this request. Read its prior result instead of repeating it." });
583
+ sse(res, "tool.result", { id: call.id, name: call.function.name, ok: false, content: result, resources: [] });
584
+ appendToolMessage({ role: "tool", tool_call_id: call.id, content: result });
585
+ continue;
586
+ }
587
+ signatures.add(signature);
588
+ if (call.function.name === SEARCH_TOOL_NAME) {
589
+ const query = typeof args.query === "string" ? args.query : "";
590
+ const matches = rankTools(runtime.tools, query, 12);
591
+ for (const match of matches)
592
+ activeTools.set(match.name, match);
593
+ const result = JSON.stringify({
594
+ tools: matches.map((tool) => ({ name: tool.name, description: tool.description })),
595
+ instruction: "These authorized tools will be available on the next model turn. Use an exact returned name and its provided schema.",
596
+ });
597
+ sse(res, "tool.result", { id: call.id, name: call.function.name, ok: true, content: result, resources: [] });
598
+ appendToolMessage({ role: "tool", tool_call_id: call.id, content: result });
599
+ continue;
600
+ }
601
+ if (!offered.has(call.function.name)) {
602
+ const result = JSON.stringify({ code: "TOOL_NOT_OFFERED", message: `Call ${SEARCH_TOOL_NAME} to discover this capability before using it.` });
603
+ sse(res, "tool.result", { id: call.id, name: call.function.name, ok: false, content: result, resources: [] });
604
+ appendToolMessage({ role: "tool", tool_call_id: call.id, content: result });
605
+ continue;
606
+ }
607
+ const risk = toolRisk(call.function.name);
608
+ if (risk !== "read") {
609
+ protectedCalls.push({ id: call.id, name: call.function.name, arguments: args, risk });
610
+ continue;
611
+ }
612
+ appendToolMessage(await runTool(runtime, toolMap, call, args, res));
613
+ }
614
+ if (protectedCalls.length > 0) {
615
+ const approval = await issueApprovalToken(deps.store, deps.cfg.internalServiceKey, {
616
+ userId,
617
+ workspaceId,
618
+ orgId,
619
+ calls: protectedCalls,
620
+ });
621
+ pendingApproval = {
622
+ token: approval.token,
623
+ expires_at: new Date(approval.expiresAt).toISOString(),
624
+ calls: protectedCalls,
625
+ };
626
+ deps.audit.emit("assistant_approval_requested", {
627
+ tools: protectedCalls.map((call) => call.name),
628
+ risks: protectedCalls.map((call) => call.risk),
629
+ user_id: userId,
630
+ workspace_id: workspaceId,
631
+ });
632
+ sse(res, "approval.required", pendingApproval);
633
+ break;
634
+ }
635
+ }
636
+ if (rounds >= MAX_TOOL_ROUNDS && !pendingApproval) {
637
+ sse(res, "error", {
638
+ status: 409,
639
+ code: "tool_loop_limit",
640
+ message: "The assistant stopped after too many tool rounds. Review the tool results and continue with a narrower request.",
641
+ });
642
+ }
643
+ sse(res, "usage", usage);
644
+ sse(res, "done", { messages: appended, usage, pending_approval: pendingApproval });
645
+ deps.audit.emit("assistant_chat_completed", {
646
+ model: modelDetail.slug,
647
+ rounds: Math.min(rounds + 1, MAX_TOOL_ROUNDS),
648
+ tool_calls: toolCallsMade,
649
+ approval_required: pendingApproval !== null,
650
+ prompt_tokens: usage.prompt_tokens ?? 0,
651
+ completion_tokens: usage.completion_tokens ?? 0,
652
+ user_id: userId,
653
+ workspace_id: workspaceId,
654
+ });
655
+ }
656
+ catch (error) {
657
+ if (!controller.signal.aborted) {
658
+ const status = error instanceof AssistantUpstreamError || error instanceof AssistantRequestError ? error.status : 500;
659
+ const code = error instanceof AssistantUpstreamError || error instanceof AssistantRequestError ? error.code : "assistant_error";
660
+ const message = error instanceof Error ? error.message : "The assistant request failed.";
661
+ sse(res, "error", {
662
+ status,
663
+ code,
664
+ message,
665
+ ...(status === 402 || code === "insufficient_funds" ? { action: "add_funds" } : {}),
666
+ });
667
+ sse(res, "done", { messages: appended, usage, pending_approval: null });
668
+ }
669
+ }
670
+ finally {
671
+ clearInterval(heartbeat);
672
+ if (runtime)
673
+ await runtime.close();
674
+ if (!res.writableEnded)
675
+ res.end();
676
+ }
677
+ };
678
+ }
679
+ //# sourceMappingURL=assistant.js.map