@inkeep/agents-api 0.0.0-dev-20260122003353 → 0.0.0-dev-20260122004923

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.
@@ -15,6 +15,7 @@ import { IncrementalStreamParser } from "../services/IncrementalStreamParser.js"
15
15
  import { MidGenerationCompressor } from "../services/MidGenerationCompressor.js";
16
16
  import { pendingToolApprovalManager } from "../services/PendingToolApprovalManager.js";
17
17
  import { ResponseFormatter } from "../services/ResponseFormatter.js";
18
+ import { toolApprovalUiBus } from "../services/ToolApprovalUiBus.js";
18
19
  import { generateToolId } from "../utils/agent-operations.js";
19
20
  import { jsonSchemaToZod } from "../utils/data-component-schema.js";
20
21
  import { ArtifactCreateSchema, ArtifactReferenceSchema } from "../utils/artifact-component-schema.js";
@@ -223,6 +224,12 @@ var Agent = class {
223
224
  execute: async (args, context$1) => {
224
225
  const startTime = Date.now();
225
226
  const toolCallId = context$1?.toolCallId || generateToolId();
227
+ const streamHelper = this.getStreamingHelper();
228
+ const chunkString = (s, size = 16) => {
229
+ const out = [];
230
+ for (let i = 0; i < s.length; i += size) out.push(s.slice(i, i + size));
231
+ return out;
232
+ };
226
233
  const activeSpan = trace.getActiveSpan();
227
234
  if (activeSpan) {
228
235
  const attributes = {
@@ -237,9 +244,26 @@ var Agent = class {
237
244
  if (options?.mcpServerName) attributes["ai.toolCall.mcpServerName"] = options.mcpServerName;
238
245
  activeSpan.setAttributes(attributes);
239
246
  }
240
- const isInternalTool = toolName.includes("save_tool_result") || toolName.includes("thinking_complete") || toolName.startsWith("transfer_to_");
247
+ const isInternalToolForUi = toolName.includes("save_tool_result") || toolName.includes("thinking_complete") || toolName.startsWith("transfer_to_") || toolName.startsWith("delegate_to_");
241
248
  const needsApproval = options?.needsApproval || false;
242
- if (streamRequestId && !isInternalTool) {
249
+ if (streamRequestId && streamHelper && !isInternalToolForUi) {
250
+ const inputText = JSON.stringify(args ?? {});
251
+ await streamHelper.writeToolInputStart({
252
+ toolCallId,
253
+ toolName
254
+ });
255
+ for (const part of chunkString(inputText, 16)) await streamHelper.writeToolInputDelta({
256
+ toolCallId,
257
+ inputTextDelta: part
258
+ });
259
+ await streamHelper.writeToolInputAvailable({
260
+ toolCallId,
261
+ toolName,
262
+ input: args ?? {},
263
+ providerMetadata: context$1?.providerMetadata
264
+ });
265
+ }
266
+ if (streamRequestId && !isInternalToolForUi) {
243
267
  const toolCallData = {
244
268
  toolName,
245
269
  input: args,
@@ -256,7 +280,7 @@ var Agent = class {
256
280
  const result = await originalExecute(args, context$1);
257
281
  const duration = Date.now() - startTime;
258
282
  const toolResultConversationId = this.getToolResultConversationId();
259
- if (streamRequestId && !isInternalTool && toolResultConversationId) try {
283
+ if (streamRequestId && !isInternalToolForUi && toolResultConversationId) try {
260
284
  const messagePayload = {
261
285
  id: generateId(),
262
286
  tenantId: this.config.tenantId,
@@ -284,7 +308,7 @@ var Agent = class {
284
308
  conversationId: toolResultConversationId
285
309
  }, "Failed to store tool result in conversation history");
286
310
  }
287
- if (streamRequestId && !isInternalTool) agentSessionManager.recordEvent(streamRequestId, "tool_result", this.config.id, {
311
+ if (streamRequestId && !isInternalToolForUi) agentSessionManager.recordEvent(streamRequestId, "tool_result", this.config.id, {
288
312
  toolName,
289
313
  output: result,
290
314
  toolCallId,
@@ -292,11 +316,17 @@ var Agent = class {
292
316
  relationshipId,
293
317
  needsApproval
294
318
  });
319
+ const isDeniedResult = !!result && typeof result === "object" && "__inkeepToolDenied" in result && result.__inkeepToolDenied === true;
320
+ if (streamRequestId && streamHelper && !isInternalToolForUi) if (isDeniedResult) await streamHelper.writeToolOutputDenied({ toolCallId });
321
+ else await streamHelper.writeToolOutputAvailable({
322
+ toolCallId,
323
+ output: result
324
+ });
295
325
  return result;
296
326
  } catch (error) {
297
327
  const duration = Date.now() - startTime;
298
328
  const errorMessage = error instanceof Error ? error.message : "Unknown error";
299
- if (streamRequestId && !isInternalTool) agentSessionManager.recordEvent(streamRequestId, "tool_result", this.config.id, {
329
+ if (streamRequestId && !isInternalToolForUi) agentSessionManager.recordEvent(streamRequestId, "tool_result", this.config.id, {
300
330
  toolName,
301
331
  output: null,
302
332
  toolCallId,
@@ -305,6 +335,10 @@ var Agent = class {
305
335
  relationshipId,
306
336
  needsApproval
307
337
  });
338
+ if (streamRequestId && streamHelper && !isInternalToolForUi) await streamHelper.writeToolOutputError({
339
+ toolCallId,
340
+ error: errorMessage
341
+ });
308
342
  throw error;
309
343
  }
310
344
  }
@@ -374,7 +408,7 @@ var Agent = class {
374
408
  const sessionWrappedTool = tool({
375
409
  description: originalTool.description,
376
410
  inputSchema: originalTool.inputSchema,
377
- execute: async (args, { toolCallId }) => {
411
+ execute: async (args, { toolCallId, providerMetadata }) => {
378
412
  let processedArgs;
379
413
  try {
380
414
  processedArgs = parseEmbeddedJson(args);
@@ -412,22 +446,52 @@ var Agent = class {
412
446
  requestSpan.setStatus({ code: SpanStatusCode.OK });
413
447
  requestSpan.end();
414
448
  });
415
- const approvalResult = await pendingToolApprovalManager.waitForApproval(toolCallId, toolName, args, this.conversationId || "unknown", this.config.id);
416
- if (!approvalResult.approved) return tracer.startActiveSpan("tool.approval_denied", { attributes: {
417
- "tool.name": toolName,
418
- "tool.callId": toolCallId,
419
- "subAgent.id": this.config.id,
420
- "subAgent.name": this.config.name
421
- } }, (denialSpan) => {
422
- logger.info({
423
- toolName,
424
- toolCallId,
425
- reason: approvalResult.reason
426
- }, "Tool execution denied by user");
427
- denialSpan.setStatus({ code: SpanStatusCode.OK });
428
- denialSpan.end();
429
- return `User denied approval to run this tool: ${approvalResult.reason}`;
449
+ const streamHelper = this.getStreamingHelper();
450
+ if (streamHelper) await streamHelper.writeToolApprovalRequest({
451
+ approvalId: `aitxt-${toolCallId}`,
452
+ toolCallId
430
453
  });
454
+ else if (this.isDelegatedAgent) {
455
+ const streamRequestId$1 = this.getStreamRequestId();
456
+ if (streamRequestId$1) await toolApprovalUiBus.publish(streamRequestId$1, {
457
+ type: "approval-needed",
458
+ toolCallId,
459
+ toolName,
460
+ input: finalArgs,
461
+ providerMetadata,
462
+ approvalId: `aitxt-${toolCallId}`
463
+ });
464
+ }
465
+ const approvalResult = await pendingToolApprovalManager.waitForApproval(toolCallId, toolName, args, this.conversationId || "unknown", this.config.id);
466
+ if (!approvalResult.approved) {
467
+ if (!streamHelper && this.isDelegatedAgent) {
468
+ const streamRequestId$1 = this.getStreamRequestId();
469
+ if (streamRequestId$1) await toolApprovalUiBus.publish(streamRequestId$1, {
470
+ type: "approval-resolved",
471
+ toolCallId,
472
+ approved: false
473
+ });
474
+ }
475
+ return tracer.startActiveSpan("tool.approval_denied", { attributes: {
476
+ "tool.name": toolName,
477
+ "tool.callId": toolCallId,
478
+ "subAgent.id": this.config.id,
479
+ "subAgent.name": this.config.name
480
+ } }, (denialSpan) => {
481
+ logger.info({
482
+ toolName,
483
+ toolCallId,
484
+ reason: approvalResult.reason
485
+ }, "Tool execution denied by user");
486
+ denialSpan.setStatus({ code: SpanStatusCode.OK });
487
+ denialSpan.end();
488
+ return {
489
+ __inkeepToolDenied: true,
490
+ toolCallId,
491
+ reason: approvalResult.reason
492
+ };
493
+ });
494
+ }
431
495
  tracer.startActiveSpan("tool.approval_approved", { attributes: {
432
496
  "tool.name": toolName,
433
497
  "tool.callId": toolCallId,
@@ -441,6 +505,14 @@ var Agent = class {
441
505
  approvedSpan.setStatus({ code: SpanStatusCode.OK });
442
506
  approvedSpan.end();
443
507
  });
508
+ if (!streamHelper && this.isDelegatedAgent) {
509
+ const streamRequestId$1 = this.getStreamRequestId();
510
+ if (streamRequestId$1) await toolApprovalUiBus.publish(streamRequestId$1, {
511
+ type: "approval-resolved",
512
+ toolCallId,
513
+ approved: true
514
+ });
515
+ }
444
516
  }
445
517
  logger.debug({
446
518
  toolName,
@@ -500,10 +572,7 @@ var Agent = class {
500
572
  result: enhancedResult,
501
573
  timestamp: Date.now()
502
574
  });
503
- return {
504
- result: enhancedResult,
505
- toolCallId
506
- };
575
+ return enhancedResult;
507
576
  } catch (error) {
508
577
  logger.error({
509
578
  toolName,
@@ -795,10 +864,7 @@ var Agent = class {
795
864
  result,
796
865
  timestamp: Date.now()
797
866
  });
798
- return {
799
- result,
800
- toolCallId
801
- };
867
+ return result;
802
868
  } catch (error) {
803
869
  logger.error({
804
870
  toolName: functionToolDef.name,
@@ -3,6 +3,7 @@ import runDbClient_default from "../../../data/db/runDbClient.js";
3
3
  import { contextValidationMiddleware } from "../context/validation.js";
4
4
  import { handleContextResolution } from "../context/context.js";
5
5
  import "../context/index.js";
6
+ import { toolApprovalUiBus } from "../services/ToolApprovalUiBus.js";
6
7
  import { errorOp } from "../utils/agent-operations.js";
7
8
  import { createSSEStreamHelper } from "../utils/stream-helpers.js";
8
9
  import { ExecutionHandler } from "../handlers/executionHandler.js";
@@ -222,9 +223,50 @@ app.openapi(chatCompletionsRoute, async (c) => {
222
223
  "database.operation": "insert"
223
224
  });
224
225
  return streamSSE(c, async (stream$1) => {
226
+ const chunkString = (s, size = 16) => {
227
+ const out = [];
228
+ for (let i = 0; i < s.length; i += size) out.push(s.slice(i, i + size));
229
+ return out;
230
+ };
231
+ let unsubscribe;
225
232
  try {
226
233
  const sseHelper = createSSEStreamHelper(stream$1, requestId, timestamp);
227
234
  await sseHelper.writeRole();
235
+ const seenToolCalls = /* @__PURE__ */ new Set();
236
+ const seenOutputs = /* @__PURE__ */ new Set();
237
+ unsubscribe = toolApprovalUiBus.subscribe(requestId, async (event) => {
238
+ if (event.type === "approval-needed") {
239
+ if (seenToolCalls.has(event.toolCallId)) return;
240
+ seenToolCalls.add(event.toolCallId);
241
+ await sseHelper.writeToolInputStart({
242
+ toolCallId: event.toolCallId,
243
+ toolName: event.toolName
244
+ });
245
+ const inputText = JSON.stringify(event.input ?? {});
246
+ for (const part of chunkString(inputText, 16)) await sseHelper.writeToolInputDelta({
247
+ toolCallId: event.toolCallId,
248
+ inputTextDelta: part
249
+ });
250
+ await sseHelper.writeToolInputAvailable({
251
+ toolCallId: event.toolCallId,
252
+ toolName: event.toolName,
253
+ input: event.input ?? {},
254
+ providerMetadata: event.providerMetadata
255
+ });
256
+ await sseHelper.writeToolApprovalRequest({
257
+ approvalId: event.approvalId,
258
+ toolCallId: event.toolCallId
259
+ });
260
+ } else if (event.type === "approval-resolved") {
261
+ if (seenOutputs.has(event.toolCallId)) return;
262
+ seenOutputs.add(event.toolCallId);
263
+ if (event.approved) await sseHelper.writeToolOutputAvailable({
264
+ toolCallId: event.toolCallId,
265
+ output: { status: "approved" }
266
+ });
267
+ else await sseHelper.writeToolOutputDenied({ toolCallId: event.toolCallId });
268
+ }
269
+ });
228
270
  logger.info({ subAgentId }, "Starting execution");
229
271
  const emitOperations = c.req.header("x-emit-operations") === "true";
230
272
  const forwardedHeaders = {};
@@ -275,6 +317,10 @@ app.openapi(chatCompletionsRoute, async (c) => {
275
317
  } catch (streamError) {
276
318
  logger.error({ streamError }, "Failed to write error to stream");
277
319
  }
320
+ } finally {
321
+ try {
322
+ unsubscribe?.();
323
+ } catch (_e) {}
278
324
  }
279
325
  });
280
326
  });
@@ -4,6 +4,7 @@ import { contextValidationMiddleware } from "../context/validation.js";
4
4
  import { handleContextResolution } from "../context/context.js";
5
5
  import "../context/index.js";
6
6
  import { pendingToolApprovalManager } from "../services/PendingToolApprovalManager.js";
7
+ import { toolApprovalUiBus } from "../services/ToolApprovalUiBus.js";
7
8
  import { errorOp } from "../utils/agent-operations.js";
8
9
  import { createBufferingStreamHelper, createVercelStreamHelper } from "../utils/stream-helpers.js";
9
10
  import { ExecutionHandler } from "../handlers/executionHandler.js";
@@ -34,16 +35,31 @@ const chatDataStreamRoute = createRoute({
34
35
  "tool"
35
36
  ]),
36
37
  content: z.any(),
37
- parts: z.array(z.object({
38
- type: z.union([z.enum([
39
- "text",
40
- "image",
41
- "audio",
42
- "video",
43
- "file"
44
- ]), z.string().regex(/^data-/, "Type must start with \"data-\"")]),
45
- text: z.string().optional()
46
- })).optional()
38
+ parts: z.array(z.union([
39
+ z.object({
40
+ type: z.union([z.enum([
41
+ "text",
42
+ "image",
43
+ "audio",
44
+ "video",
45
+ "file"
46
+ ]), z.string().regex(/^data-/, "Type must start with \"data-\"")]),
47
+ text: z.string().optional()
48
+ }),
49
+ z.object({
50
+ type: z.string().regex(/^tool-/, "Type must start with \"tool-\""),
51
+ toolCallId: z.string(),
52
+ state: z.literal("approval-responded"),
53
+ approval: z.object({
54
+ id: z.string(),
55
+ approved: z.boolean(),
56
+ reason: z.string().optional()
57
+ }),
58
+ input: z.any().optional(),
59
+ callProviderMetadata: z.any().optional()
60
+ }),
61
+ z.object({ type: z.literal("step-start") })
62
+ ])).optional()
47
63
  })),
48
64
  id: z.string().optional(),
49
65
  conversationId: z.string().optional(),
@@ -74,7 +90,39 @@ app.openapi(chatDataStreamRoute, async (c) => {
74
90
  agentId
75
91
  }, "Extracted chatDataStream parameters");
76
92
  const body = c.get("requestBody") || {};
77
- const conversationId = body.conversationId || getConversationId();
93
+ const approvalPart = (body.messages || []).flatMap((m) => m?.parts || []).find((p) => p?.state === "approval-responded" && typeof p?.toolCallId === "string");
94
+ const isApprovalResponse = !!approvalPart;
95
+ const conversationId = isApprovalResponse ? body.conversationId : body.conversationId || getConversationId();
96
+ if (isApprovalResponse) {
97
+ if (!conversationId) return c.json({
98
+ success: false,
99
+ error: "conversationId is required for approval response"
100
+ }, 400);
101
+ const toolCallId = approvalPart.toolCallId;
102
+ const approved = !!approvalPart.approval?.approved;
103
+ const reason = approvalPart.approval?.reason;
104
+ if (!await getConversation(runDbClient_default)({
105
+ scopes: {
106
+ tenantId,
107
+ projectId
108
+ },
109
+ conversationId
110
+ })) return c.json({
111
+ success: false,
112
+ error: "Conversation not found"
113
+ }, 404);
114
+ if (!(approved ? pendingToolApprovalManager.approveToolCall(toolCallId) : pendingToolApprovalManager.denyToolCall(toolCallId, reason))) return c.json({
115
+ success: true,
116
+ toolCallId,
117
+ approved,
118
+ alreadyProcessed: true
119
+ });
120
+ return c.json({
121
+ success: true,
122
+ toolCallId,
123
+ approved
124
+ });
125
+ }
78
126
  const targetTenantId = c.req.header("x-target-tenant-id");
79
127
  const targetProjectId = c.req.header("x-target-project-id");
80
128
  const targetAgentId = c.req.header("x-target-agent-id");
@@ -227,16 +275,58 @@ app.openapi(chatDataStreamRoute, async (c) => {
227
275
  }
228
276
  const dataStream = createUIMessageStream({ execute: async ({ writer }) => {
229
277
  const streamHelper = createVercelStreamHelper(writer);
278
+ let unsubscribe;
230
279
  try {
231
280
  const emitOperations = c.req.header("x-emit-operations") === "true";
232
281
  const executionHandler = new ExecutionHandler();
233
282
  const datasetRunId = c.req.header("x-inkeep-dataset-run-id");
283
+ const requestId = `chatds-${Date.now()}`;
284
+ const chunkString = (s, size = 16) => {
285
+ const out = [];
286
+ for (let i = 0; i < s.length; i += size) out.push(s.slice(i, i + size));
287
+ return out;
288
+ };
289
+ const seenToolCalls = /* @__PURE__ */ new Set();
290
+ const seenOutputs = /* @__PURE__ */ new Set();
291
+ unsubscribe = toolApprovalUiBus.subscribe(requestId, async (event) => {
292
+ if (event.type === "approval-needed") {
293
+ if (seenToolCalls.has(event.toolCallId)) return;
294
+ seenToolCalls.add(event.toolCallId);
295
+ await streamHelper.writeToolInputStart({
296
+ toolCallId: event.toolCallId,
297
+ toolName: event.toolName
298
+ });
299
+ const inputText = JSON.stringify(event.input ?? {});
300
+ for (const part of chunkString(inputText, 16)) await streamHelper.writeToolInputDelta({
301
+ toolCallId: event.toolCallId,
302
+ inputTextDelta: part
303
+ });
304
+ await streamHelper.writeToolInputAvailable({
305
+ toolCallId: event.toolCallId,
306
+ toolName: event.toolName,
307
+ input: event.input ?? {},
308
+ providerMetadata: event.providerMetadata
309
+ });
310
+ await streamHelper.writeToolApprovalRequest({
311
+ approvalId: event.approvalId,
312
+ toolCallId: event.toolCallId
313
+ });
314
+ } else if (event.type === "approval-resolved") {
315
+ if (seenOutputs.has(event.toolCallId)) return;
316
+ seenOutputs.add(event.toolCallId);
317
+ if (event.approved) await streamHelper.writeToolOutputAvailable({
318
+ toolCallId: event.toolCallId,
319
+ output: { status: "approved" }
320
+ });
321
+ else await streamHelper.writeToolOutputDenied({ toolCallId: event.toolCallId });
322
+ }
323
+ });
234
324
  if (!(await executionHandler.execute({
235
325
  executionContext,
236
326
  conversationId,
237
327
  userMessage: userText,
238
328
  initialAgentId: subAgentId,
239
- requestId: `chatds-${Date.now()}`,
329
+ requestId,
240
330
  sseHelper: streamHelper,
241
331
  emitOperations,
242
332
  datasetRunId: datasetRunId || void 0,
@@ -246,6 +336,9 @@ app.openapi(chatDataStreamRoute, async (c) => {
246
336
  logger.error({ err }, "Streaming error");
247
337
  await streamHelper.writeOperation(errorOp("Internal server error", "system"));
248
338
  } finally {
339
+ try {
340
+ unsubscribe?.();
341
+ } catch (_e) {}
249
342
  if ("cleanup" in streamHelper && typeof streamHelper.cleanup === "function") streamHelper.cleanup();
250
343
  }
251
344
  } });
@@ -0,0 +1,28 @@
1
+ //#region src/domains/run/services/ToolApprovalUiBus.d.ts
2
+ type ToolApprovalUiEvent = {
3
+ type: 'approval-needed';
4
+ toolCallId: string;
5
+ toolName: string;
6
+ input: any;
7
+ providerMetadata?: any;
8
+ approvalId: string;
9
+ } | {
10
+ type: 'approval-resolved';
11
+ toolCallId: string;
12
+ approved: boolean;
13
+ };
14
+ type Listener = (event: ToolApprovalUiEvent) => void | Promise<void>;
15
+ /**
16
+ * In-process event bus keyed by streamRequestId.
17
+ *
18
+ * Used to propagate approval UI events from delegated agents (who must not write to stream)
19
+ * up to the user-facing request handler, which can stream tool UI parts to the client.
20
+ */
21
+ declare class ToolApprovalUiBus {
22
+ private listeners;
23
+ subscribe(streamRequestId: string, listener: Listener): () => void;
24
+ publish(streamRequestId: string, event: ToolApprovalUiEvent): Promise<void>;
25
+ }
26
+ declare const toolApprovalUiBus: ToolApprovalUiBus;
27
+ //#endregion
28
+ export { ToolApprovalUiBus, ToolApprovalUiEvent, toolApprovalUiBus };
@@ -0,0 +1,44 @@
1
+ import { getLogger } from "../../../logger.js";
2
+
3
+ //#region src/domains/run/services/ToolApprovalUiBus.ts
4
+ const logger = getLogger("ToolApprovalUiBus");
5
+ /**
6
+ * In-process event bus keyed by streamRequestId.
7
+ *
8
+ * Used to propagate approval UI events from delegated agents (who must not write to stream)
9
+ * up to the user-facing request handler, which can stream tool UI parts to the client.
10
+ */
11
+ var ToolApprovalUiBus = class {
12
+ listeners = /* @__PURE__ */ new Map();
13
+ subscribe(streamRequestId, listener) {
14
+ if (!streamRequestId) return () => {};
15
+ const set = this.listeners.get(streamRequestId) ?? /* @__PURE__ */ new Set();
16
+ set.add(listener);
17
+ this.listeners.set(streamRequestId, set);
18
+ return () => {
19
+ const existing = this.listeners.get(streamRequestId);
20
+ if (!existing) return;
21
+ existing.delete(listener);
22
+ if (existing.size === 0) this.listeners.delete(streamRequestId);
23
+ };
24
+ }
25
+ async publish(streamRequestId, event) {
26
+ if (!streamRequestId) return;
27
+ const set = this.listeners.get(streamRequestId);
28
+ if (!set || set.size === 0) return;
29
+ for (const listener of set) try {
30
+ await listener(event);
31
+ } catch (error) {
32
+ logger.warn({
33
+ streamRequestId,
34
+ eventType: event.type,
35
+ toolCallId: event.toolCallId,
36
+ error: error instanceof Error ? error.message : String(error)
37
+ }, "ToolApprovalUiBus listener failed");
38
+ }
39
+ }
40
+ };
41
+ const toolApprovalUiBus = new ToolApprovalUiBus();
42
+
43
+ //#endregion
44
+ export { ToolApprovalUiBus, toolApprovalUiBus };