@adminforth/agent 1.51.1 → 1.52.1

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.
Files changed (58) hide show
  1. package/agent/middleware/apiBasedTools.ts +31 -25
  2. package/agent/runtime/AgentRuntime.ts +24 -3
  3. package/agent/systemPrompt.ts +3 -6
  4. package/agent/turn/TurnLifecycleService.ts +18 -0
  5. package/agent/turn/TurnStreamConsumer.ts +11 -3
  6. package/agent/turn/turnTypes.ts +9 -1
  7. package/agentEvents.ts +5 -0
  8. package/agentTurnService.ts +158 -12
  9. package/apiBasedTools.ts +7 -0
  10. package/build.log +3 -2
  11. package/custom/ChatFooter.vue +3 -2
  12. package/custom/chat.ts +1 -1
  13. package/custom/composables/agentStore/useAgentChat.ts +169 -6
  14. package/custom/composables/agentStore/useAgentSessions.ts +3 -1
  15. package/custom/composables/useAgentStore.ts +87 -0
  16. package/custom/conversation_area/MessageRenderer.vue +6 -1
  17. package/custom/conversation_area/ProcessingTimeline.vue +1 -0
  18. package/custom/conversation_area/ToolApprovalRenderer.vue +98 -0
  19. package/custom/conversation_area/ToolRenderer.vue +3 -2
  20. package/custom/conversation_area/ToolsGroup.vue +5 -1
  21. package/custom/skills/mutate_data/SKILL.md +10 -36
  22. package/custom/types.ts +5 -1
  23. package/dist/agent/middleware/apiBasedTools.js +26 -25
  24. package/dist/agent/runtime/AgentRuntime.d.ts +1 -1
  25. package/dist/agent/runtime/AgentRuntime.js +18 -3
  26. package/dist/agent/systemPrompt.js +3 -6
  27. package/dist/agent/turn/TurnLifecycleService.d.ts +8 -1
  28. package/dist/agent/turn/TurnLifecycleService.js +17 -1
  29. package/dist/agent/turn/TurnStreamConsumer.d.ts +2 -1
  30. package/dist/agent/turn/TurnStreamConsumer.js +14 -8
  31. package/dist/agent/turn/turnTypes.d.ts +14 -1
  32. package/dist/agentEvents.d.ts +4 -0
  33. package/dist/agentTurnService.d.ts +1 -0
  34. package/dist/agentTurnService.js +132 -14
  35. package/dist/apiBasedTools.d.ts +5 -0
  36. package/dist/apiBasedTools.js +1 -0
  37. package/dist/custom/ChatFooter.vue +3 -2
  38. package/dist/custom/chat.ts +1 -1
  39. package/dist/custom/composables/agentStore/useAgentChat.ts +169 -6
  40. package/dist/custom/composables/agentStore/useAgentSessions.ts +3 -1
  41. package/dist/custom/composables/useAgentStore.ts +87 -0
  42. package/dist/custom/conversation_area/MessageRenderer.vue +6 -1
  43. package/dist/custom/conversation_area/ProcessingTimeline.vue +1 -0
  44. package/dist/custom/conversation_area/ToolApprovalRenderer.vue +98 -0
  45. package/dist/custom/conversation_area/ToolRenderer.vue +3 -2
  46. package/dist/custom/conversation_area/ToolsGroup.vue +5 -1
  47. package/dist/custom/skills/mutate_data/SKILL.md +10 -36
  48. package/dist/custom/types.ts +5 -1
  49. package/dist/endpoints/core.js +28 -0
  50. package/dist/index.js +1 -1
  51. package/dist/sessionStore.d.ts +1 -0
  52. package/dist/sessionStore.js +6 -0
  53. package/dist/surfaces/web-sse/createSseEventEmitter.js +13 -0
  54. package/endpoints/core.ts +30 -0
  55. package/index.ts +1 -1
  56. package/package.json +3 -6
  57. package/sessionStore.ts +11 -0
  58. package/surfaces/web-sse/createSseEventEmitter.ts +14 -0
@@ -1,4 +1,5 @@
1
1
  import { ToolMessage } from "@langchain/core/messages";
2
+ import { isGraphInterrupt } from "@langchain/langgraph";
2
3
  import { createMiddleware } from "langchain";
3
4
  import { logger, type AdminUser, type IAdminForth } from "adminforth";
4
5
  import {
@@ -13,6 +14,7 @@ import { ALWAYS_AVAILABLE_API_TOOL_NAMES } from "../tools/index.js";
13
14
  import { createApiTool } from "../tools/apiTool.js";
14
15
  import type { AgentEventEmitter } from "../../agentEvents.js";
15
16
  import type { SequenceDebugCollector } from "./sequenceDebug.js";
17
+ import { isAbortError } from "../../errors.js";
16
18
 
17
19
  function getEnabledApiToolNames(messages: unknown[]) {
18
20
  const enabledToolNames = new Set<string>();
@@ -82,8 +84,14 @@ export function createApiBasedToolsMiddleware(
82
84
  async wrapToolCall(request, handler) {
83
85
  const startedAt = Date.now();
84
86
  const toolInput = JSON.stringify(request.toolCall.args ?? {});
85
- const { adminUser, emit, sequenceDebugSink, userTimeZone } = request.runtime.context as {
87
+ if (!request.toolCall.id) {
88
+ throw new Error(`Tool call "${request.toolCall.name}" has no id.`);
89
+ }
90
+
91
+ const toolCallId = request.toolCall.id;
92
+ const { adminUser, abortSignal, emit, sequenceDebugSink, userTimeZone } = request.runtime.context as {
86
93
  adminUser: AdminUser;
94
+ abortSignal?: AbortSignal;
87
95
  emit?: AgentEventEmitter;
88
96
  sequenceDebugSink: SequenceDebugCollector;
89
97
  userTimeZone: string;
@@ -113,7 +121,7 @@ export function createApiBasedToolsMiddleware(
113
121
  }
114
122
  const toolCallTracker = createToolCallTracker({
115
123
  emit: emitToolCall,
116
- toolCallId: request.toolCall.id,
124
+ toolCallId,
117
125
  toolName: request.toolCall.name,
118
126
  toolInfo,
119
127
  input: toolArgs,
@@ -125,39 +133,37 @@ export function createApiBasedToolsMiddleware(
125
133
  );
126
134
 
127
135
  try {
128
- let result;
129
-
130
- if (request.tool) {
131
- result = await handler(request);
132
- } else {
133
- const enabledApiToolNames = getEnabledApiToolNames(request.state.messages);
134
136
 
135
- if (enabledApiToolNames.has(request.toolCall.name)) {
136
- result = await handler({
137
+ const result = getEnabledApiToolNames(request.state.messages).has(request.toolCall.name)
138
+ ? await handler({
137
139
  ...request,
138
140
  tool: dynamicTools[request.toolCall.name],
139
- });
140
- } else {
141
- result = new ToolMessage({
142
- content: `Tool "${request.toolCall.name}" is not loaded. Call fetch_tool_schema first.`,
143
- tool_call_id: request.toolCall.id ?? "",
144
- name: request.toolCall.name,
145
- status: "error",
146
- });
147
- }
148
- }
141
+ })
142
+ : await handler(request);
149
143
 
150
144
  toolCallTracker.finishSuccess(result);
151
145
  return result;
152
146
  } catch (error) {
153
- const errorDetails =
154
- error instanceof Error ? error.stack ?? error.message : String(error);
147
+ if (
148
+ isGraphInterrupt(error)
149
+ || abortSignal?.aborted
150
+ || isAbortError(error)
151
+ ) {
152
+ throw error;
153
+ }
154
+
155
+ const message = error instanceof Error ? error.message : String(error);
155
156
 
156
157
  logger.error(
157
- `Tool "${request.toolCall.name}" failed after ${Date.now() - startedAt}ms with input: ${toolInput}\n${errorDetails}`,
158
+ `Error calling tool "${request.toolCall.name}": ${error instanceof Error ? error.stack ?? error.message : String(error)}`,
158
159
  );
159
- toolCallTracker.finishError(error);
160
- throw error;
160
+ toolCallTracker.finishError(`Error: ${message}`);
161
+ return new ToolMessage({
162
+ name: request.toolCall.name,
163
+ tool_call_id: toolCallId,
164
+ status: "error",
165
+ content: `Error: ${message}`,
166
+ })
161
167
  } finally {
162
168
  logger.info(
163
169
  `Tool "${request.toolCall.name}" finished in ${Date.now() - startedAt}ms`,
@@ -1,5 +1,5 @@
1
1
  import type { IAdminForth } from "adminforth";
2
- import { createAgent, summarizationMiddleware } from "langchain";
2
+ import { createAgent, summarizationMiddleware, humanInTheLoopMiddleware } from "langchain";
3
3
  import type { BaseCheckpointSaver } from "@langchain/langgraph";
4
4
  import { createApiBasedToolsMiddleware } from "../middleware/apiBasedTools.js";
5
5
  import { createSequenceDebugMiddleware } from "../middleware/sequenceDebug.js";
@@ -7,6 +7,22 @@ import { createAgentLlmMetricsLogger } from "../simpleAgent.js";
7
7
  import type { AgentToolProvider } from "../tools/AgentToolProvider.js";
8
8
  import type { AgentRuntimeRunInput } from "../turn/turnTypes.js";
9
9
  import { contextSchema, toLangchainAgentContext } from "./AgentContext.js";
10
+ import type { ApiBasedTool } from "../../apiBasedTools.js";
11
+
12
+ function createHumanInTheLoopInterrupts(
13
+ apiBasedTools: Record<string, ApiBasedTool>,
14
+ ): Record<string, { allowedDecisions: ("approve" | "reject" | "edit")[] }> {
15
+ return Object.fromEntries(
16
+ Object.entries(apiBasedTools)
17
+ .filter(([, apiBasedTool]) => apiBasedTool.agent?.isDangerous === true)
18
+ .map(([toolName]) => [
19
+ toolName,
20
+ {
21
+ allowedDecisions: ["approve", "reject"],
22
+ },
23
+ ]),
24
+ );
25
+ }
10
26
 
11
27
  export type AgentRuntimeOptions = {
12
28
  name: string;
@@ -29,8 +45,13 @@ export class AgentRuntime {
29
45
  const sequenceDebugMiddleware = createSequenceDebugMiddleware(
30
46
  input.observability.sequenceDebugSink,
31
47
  );
48
+ const hitlMiddleware = humanInTheLoopMiddleware({
49
+ interruptOn: createHumanInTheLoopInterrupts(apiBasedTools),
50
+ descriptionPrefix: "Tool execution pending approval",
51
+ });
32
52
  const middleware = [
33
53
  apiBasedToolsMiddleware,
54
+ hitlMiddleware,
34
55
  ...(input.models.modelMiddleware ?? []),
35
56
  sequenceDebugMiddleware,
36
57
  summarizationMiddleware({
@@ -49,8 +70,8 @@ export class AgentRuntime {
49
70
  middleware,
50
71
  });
51
72
 
52
- return agent.stream({ messages: input.messages } as any, {
53
- streamMode: "messages",
73
+ return agent.stream(input.input as any, {
74
+ streamMode: ["messages", "updates"],
54
75
  recursionLimit: 100,
55
76
  callbacks: [createAgentLlmMetricsLogger()],
56
77
  signal: input.context.abortSignal,
@@ -25,11 +25,8 @@ export const DEFAULT_AGENT_SYSTEM_PROMPT = [
25
25
  "Do not add extra explanations or suggestions unless the user asks.",
26
26
  "Adapt to the user's tone and style of speaking, mirroring their vibe and wording.",
27
27
  "if the user speaks casually, you should respond casually too",
28
- "Never mutate data without user confirmation for a clearly described mutation plan.",
29
- "One confirmation may cover one mutation or one explicitly described batch/sequence of related mutations.",
30
- "If the confirmed plan has multiple steps, you may execute the whole confirmed plan without asking again between those steps.",
31
- "If the plan changes, expands, or you want to do anything beyond the confirmed plan, ask for confirmation again.",
32
- "Do not reuse an old confirmation for a new mutation plan.",
28
+ "Before calling a dangerous tool, briefly describe the exact action, target, and important changes in chat.",
29
+ "Do not ask the user for textual confirmation; dangerous tools are approved by the runtime approval UI.",
33
30
  ].join(" ");
34
31
 
35
32
  export function appendCustomSystemPrompt(
@@ -124,7 +121,7 @@ export async function buildAgentSystemPrompt(
124
121
  "If the user wants to fetch records, load fetch_data first. If the user wants analytics or charts, load analyze_data first.",
125
122
  "Only call fetch_tool_schema for tool names that are explicitly mentioned in a fetched skill and are not already available as base tools.",
126
123
  "If a fetched skill lists a non-base tool you need, call fetch_tool_schema for it immediately instead of telling the user the tool is unavailable.",
127
- "For example: for record creation load mutate_data, read its tool list, call fetch_tool_schema for create_record, and then use create_record after confirmation.",
124
+ "For example: for record creation load mutate_data, read its tool list, call fetch_tool_schema for create_record, describe the planned record, and then use create_record.",
128
125
  "When fetch_tool_schema succeeds, that tool becomes available on the next step.",
129
126
  "All admin links must be root-relative and start with '/'.",
130
127
  "Build record links as '/resource/{resourceId}/show/{primary key}'. Never use bare 'resource/{resourceId}/show/{primary key}' without the leading slash.",
@@ -1,4 +1,5 @@
1
1
  import type { AgentSessionStore } from "../../sessionStore.js";
2
+ import type { PluginOptions } from "../../types.js";
2
3
  import type { BaseAgentTurnInput } from "./turnTypes.js";
3
4
  import { TurnPersistenceService } from "./TurnPersistenceService.js";
4
5
 
@@ -6,6 +7,7 @@ export class TurnLifecycleService {
6
7
  constructor(
7
8
  private readonly sessionStore: AgentSessionStore,
8
9
  private readonly persistence: TurnPersistenceService,
10
+ private readonly options: PluginOptions,
9
11
  ) {}
10
12
 
11
13
  async start(input: BaseAgentTurnInput) {
@@ -19,6 +21,22 @@ export class TurnLifecycleService {
19
21
  };
20
22
  }
21
23
 
24
+ async resume(input: BaseAgentTurnInput) {
25
+ const latestTurn = await this.sessionStore.getLatestTurn(input.sessionId);
26
+
27
+ if (!latestTurn) {
28
+ throw new Error(`No agent turn found for session "${input.sessionId}".`);
29
+ }
30
+
31
+ return {
32
+ turnId: latestTurn[this.options.turnResource.idField],
33
+ previousUserMessages: await this.sessionStore.getPreviousUserMessages(input.sessionId),
34
+ initialResponse: latestTurn[this.options.turnResource.responseField] === "not_finished"
35
+ ? ""
36
+ : String(latestTurn[this.options.turnResource.responseField]),
37
+ };
38
+ }
39
+
22
40
  async finish(input: {
23
41
  turnId: string;
24
42
  responseText: string;
@@ -3,19 +3,27 @@ import { VegaLiteStreamBuffer } from "./VegaLiteStreamBuffer.js";
3
3
 
4
4
  export class TurnStreamConsumer {
5
5
  async consume(input: {
6
- stream: AsyncIterable<[any, any]>;
6
+ stream: AsyncIterable<["messages", [any, any]] | ["updates", Record<string, any>]>;
7
7
  abortSignal?: AbortSignal;
8
8
  emit?: AgentEventEmitter;
9
+ onInterrupt?: (interrupt: unknown) => void | Promise<void>;
9
10
  }) {
10
11
  let fullResponse = "";
11
12
  const textBuffer = new VegaLiteStreamBuffer();
12
13
 
13
- for await (const rawChunk of input.stream) {
14
+ for await (const [mode, chunk] of input.stream) {
14
15
  if (input.abortSignal?.aborted) {
15
16
  throw new DOMException("This operation was aborted", "AbortError");
16
17
  }
17
18
 
18
- const [token, metadata] = rawChunk;
19
+ if (mode === "updates") {
20
+ if ("__interrupt__" in chunk) {
21
+ await input.onInterrupt?.(chunk.__interrupt__);
22
+ }
23
+ continue;
24
+ }
25
+
26
+ const [token, metadata] = chunk;
19
27
  const nodeName =
20
28
  typeof metadata?.langgraph_node === "string"
21
29
  ? metadata.langgraph_node
@@ -1,5 +1,6 @@
1
1
  import type { AdminUser, AudioAdapter } from "adminforth";
2
2
  import type { Messages } from "@langchain/langgraph";
3
+ import type { Command } from "@langchain/langgraph";
3
4
  import type { AgentChatModel, AgentMiddleware } from "../simpleAgent.js";
4
5
  import type { SequenceDebugCollector } from "../middleware/sequenceDebug.js";
5
6
  import type { PreviousUserMessage } from "../languageDetect.js";
@@ -20,6 +21,7 @@ export type BaseAgentTurnInput = {
20
21
 
21
22
  export type TextAgentTurnInput = BaseAgentTurnInput & {
22
23
  emit: AgentEventEmitter;
24
+ approvalDecision?: "approve" | "reject";
23
25
  failureLogMessage?: string;
24
26
  abortLogMessage?: string;
25
27
  };
@@ -60,6 +62,11 @@ export type PreparedAgentTurn = {
60
62
  modeName?: string | null;
61
63
  context: AgentTurnContext;
62
64
  observability: AgentTurnObservability;
65
+ resume?: {
66
+ decision: "approve" | "reject";
67
+ interrupts?: { id: string; count: number }[];
68
+ };
69
+ initialResponse?: string;
63
70
  };
64
71
 
65
72
  export type AgentTurnModels = {
@@ -70,13 +77,14 @@ export type AgentTurnModels = {
70
77
 
71
78
  export type AgentRuntimeRunInput = {
72
79
  models: AgentTurnModels;
73
- messages: Messages;
80
+ input: { messages: Messages } | Command;
74
81
  context: AgentTurnContext;
75
82
  observability: AgentTurnObservability;
76
83
  };
77
84
 
78
85
  export type RunAndPersistAgentResponseInput = BaseAgentTurnInput & {
79
86
  emit?: AgentEventEmitter;
87
+ approvalDecision?: "approve" | "reject";
80
88
  failureLogMessage: string;
81
89
  abortLogMessage: string;
82
90
  };
package/agentEvents.ts CHANGED
@@ -22,6 +22,11 @@ export type AgentEvent =
22
22
  phase: "start" | "end";
23
23
  label: string;
24
24
  }
25
+ | {
26
+ type: "interrupt";
27
+ sessionId: string;
28
+ interrupt: unknown;
29
+ }
25
30
  | {
26
31
  type: "open-page";
27
32
  targetPath: string;
@@ -1,5 +1,6 @@
1
1
  import { logger } from "adminforth";
2
2
  import { randomUUID } from "crypto";
3
+ import { Command } from "@langchain/langgraph";
3
4
  import { AgentModelFactory } from "./agent/models/AgentModelFactory.js";
4
5
  import { AgentModeResolver } from "./agent/models/AgentModeResolver.js";
5
6
  import { createSequenceDebugCollector } from "./agent/middleware/sequenceDebug.js";
@@ -25,7 +26,94 @@ export type {
25
26
  RunAndPersistAgentResponseResult,
26
27
  } from "./agent/turn/turnTypes.js";
27
28
 
29
+ function getApprovalDecision(input: BaseAgentTurnInput) {
30
+ return "approvalDecision" in input
31
+ && (input.approvalDecision === "approve" || input.approvalDecision === "reject")
32
+ ? input.approvalDecision
33
+ : undefined;
34
+ }
35
+
36
+ function getInterruptItems(interrupt: unknown): unknown[] {
37
+ return Array.isArray(interrupt) ? interrupt : [interrupt];
38
+ }
39
+
40
+ function getHitlInterrupts(interrupt: unknown): { id: string; count: number }[] {
41
+ return getInterruptItems(interrupt).flatMap((item) => {
42
+ const value = item && typeof item === "object" && "value" in item
43
+ ? (item as { value: unknown }).value
44
+ : item;
45
+ const actionRequests = value && typeof value === "object"
46
+ ? (value as { actionRequests?: unknown }).actionRequests
47
+ : undefined;
48
+ const interruptId = item && typeof item === "object"
49
+ ? (item as { id?: unknown }).id
50
+ : undefined;
51
+
52
+ return typeof interruptId === "string" && Array.isArray(actionRequests)
53
+ ? [{ id: interruptId, count: actionRequests.length }]
54
+ : [];
55
+ });
56
+ }
57
+
58
+ function buildHitlDecision(decision: "approve" | "reject", prompt?: string) {
59
+ if (decision === "approve") {
60
+ return { type: "approve" as const };
61
+ }
62
+
63
+ return {
64
+ type: "reject" as const,
65
+ message: prompt
66
+ ? `User rejected the pending tool execution and sent a new instruction instead: ${prompt}`
67
+ : "User rejected executing this tool",
68
+ };
69
+ }
70
+
71
+ function buildHitlResumeValue(input: {
72
+ decision: "approve" | "reject";
73
+ count: number;
74
+ prompt?: string;
75
+ }) {
76
+ return {
77
+ decisions: Array.from({ length: input.count }, () => (
78
+ buildHitlDecision(input.decision, input.prompt)
79
+ )),
80
+ };
81
+ }
82
+
83
+ function buildLangGraphResume(input: {
84
+ decision: "approve" | "reject";
85
+ interrupts?: { id: string; count: number }[];
86
+ prompt?: string;
87
+ }) {
88
+ const interrupts = input.interrupts ?? [];
89
+
90
+ if (interrupts.length === 0) {
91
+ throw new Error("No pending approval interrupt found for resume.");
92
+ }
93
+
94
+ if (interrupts.length === 1) {
95
+ return buildHitlResumeValue({
96
+ decision: input.decision,
97
+ count: interrupts[0].count,
98
+ prompt: input.prompt,
99
+ });
100
+ }
101
+
102
+ return Object.fromEntries(
103
+ interrupts.map((interrupt) => [
104
+ interrupt.id,
105
+ buildHitlResumeValue({
106
+ decision: input.decision,
107
+ count: interrupt.count,
108
+ prompt: input.prompt,
109
+ }),
110
+ ]),
111
+ );
112
+ }
113
+
28
114
  export class AgentTurnService {
115
+ private readonly pendingInterrupts = new Map<string, { id: string; count: number }[]>();
116
+
29
117
  constructor(
30
118
  private readonly lifecycle: TurnLifecycleService,
31
119
  private readonly contextBuilder: TurnContextBuilder,
@@ -38,23 +126,40 @@ export class AgentTurnService {
38
126
 
39
127
  private async prepareTurn(input: BaseAgentTurnInput): Promise<PreparedAgentTurn> {
40
128
  const sequenceDebugCollector = createSequenceDebugCollector();
41
- const { turnId, previousUserMessages } = await this.lifecycle.start(input);
129
+ const approvalDecision = getApprovalDecision(input);
130
+ const shouldResume = Boolean(approvalDecision);
131
+ const pendingInterrupts = this.pendingInterrupts.get(input.sessionId);
132
+ if (shouldResume && (!pendingInterrupts || pendingInterrupts.length === 0)) {
133
+ throw new Error(`No pending approval interrupt found for session "${input.sessionId}".`);
134
+ }
135
+ const lifecycleTurn = shouldResume
136
+ ? await this.lifecycle.resume(input)
137
+ : await this.lifecycle.start(input);
42
138
  const context = await this.contextBuilder.build({
43
139
  base: input,
44
- turnId,
140
+ turnId: lifecycleTurn.turnId,
45
141
  });
46
142
 
47
143
  return {
48
144
  prompt: input.prompt,
49
145
  sessionId: input.sessionId,
50
- turnId,
51
- previousUserMessages,
146
+ turnId: lifecycleTurn.turnId,
147
+ previousUserMessages: lifecycleTurn.previousUserMessages,
52
148
  modeName: input.modeName,
53
149
  context,
54
150
  observability: {
55
151
  emit: undefined,
56
152
  sequenceDebugSink: sequenceDebugCollector,
57
153
  },
154
+ resume: shouldResume
155
+ ? {
156
+ decision: approvalDecision!,
157
+ interrupts: pendingInterrupts,
158
+ }
159
+ : undefined,
160
+ initialResponse: shouldResume && "initialResponse" in lifecycleTurn
161
+ ? (lifecycleTurn as { initialResponse?: string }).initialResponse
162
+ : undefined,
58
163
  };
59
164
  }
60
165
 
@@ -73,16 +178,56 @@ export class AgentTurnService {
73
178
  ]);
74
179
  const stream = await this.runtime.stream({
75
180
  models,
76
- messages,
181
+ input: input.resume
182
+ ? new Command({
183
+ resume: buildLangGraphResume({
184
+ decision: input.resume.decision,
185
+ interrupts: input.resume.interrupts,
186
+ prompt: input.prompt,
187
+ }),
188
+ })
189
+ : { messages },
77
190
  context: input.context,
78
191
  observability: input.observability,
79
192
  });
80
193
 
81
- return this.streamConsumer.consume({
82
- stream: stream as AsyncIterable<[any, any]>,
83
- abortSignal: input.context.abortSignal,
84
- emit: input.observability.emit,
85
- });
194
+ let interrupted = false;
195
+ try {
196
+ return await this.streamConsumer.consume({
197
+ stream: stream as AsyncIterable<["messages", [any, any]] | ["updates", Record<string, any>]>,
198
+ abortSignal: input.context.abortSignal,
199
+ emit: input.observability.emit,
200
+ onInterrupt: async (interrupt) => {
201
+ interrupted = true;
202
+ const interrupts = getHitlInterrupts(interrupt);
203
+ const pendingInterrupts = this.pendingInterrupts.get(input.sessionId) ?? [];
204
+ const mergedInterrupts = new Map(
205
+ pendingInterrupts.map((pendingInterrupt) => [
206
+ pendingInterrupt.id,
207
+ pendingInterrupt.count,
208
+ ]),
209
+ );
210
+
211
+ for (const pendingInterrupt of interrupts) {
212
+ mergedInterrupts.set(pendingInterrupt.id, pendingInterrupt.count);
213
+ }
214
+
215
+ this.pendingInterrupts.set(
216
+ input.sessionId,
217
+ [...mergedInterrupts.entries()].map(([id, count]) => ({ id, count })),
218
+ );
219
+ await input.observability.emit?.({
220
+ type: "interrupt",
221
+ sessionId: input.sessionId,
222
+ interrupt,
223
+ });
224
+ },
225
+ });
226
+ } finally {
227
+ if (!interrupted) {
228
+ this.pendingInterrupts.delete(input.sessionId);
229
+ }
230
+ }
86
231
  }
87
232
 
88
233
  async runAndPersistAgentResponse(
@@ -91,13 +236,13 @@ export class AgentTurnService {
91
236
  const preparedTurn = await this.prepareTurn(input);
92
237
  preparedTurn.observability.emit = input.emit;
93
238
 
94
- let fullResponse = "";
239
+ let fullResponse = preparedTurn.initialResponse ?? "";
95
240
  let aborted = false;
96
241
  let failed = false;
97
242
 
98
243
  try {
99
244
  const agentResponse = await this.runAgentTurn(preparedTurn);
100
- fullResponse = agentResponse.text;
245
+ fullResponse += agentResponse.text;
101
246
  } catch (error) {
102
247
  if (input.abortSignal?.aborted || isAbortError(error)) {
103
248
  aborted = true;
@@ -138,6 +283,7 @@ export class AgentTurnService {
138
283
  currentPage: input.currentPage,
139
284
  chatSurface: input.chatSurface,
140
285
  adminPublicOrigin: input.adminPublicOrigin,
286
+ approvalDecision: input.approvalDecision,
141
287
  abortSignal: input.abortSignal,
142
288
  adminUser: input.adminUser,
143
289
  emit: input.emit,
package/apiBasedTools.ts CHANGED
@@ -39,10 +39,15 @@ type GetResourceDataToolResponse = {
39
39
  type DateTimeColumnType = AdminForthDataTypes.DATETIME | AdminForthDataTypes.TIME;
40
40
  type RegisteredApiToolSchema = IRegisteredApiSchema & {
41
41
  handler: (input: unknown) => void | Promise<unknown>;
42
+ agent?: AgentToolMeta;
42
43
  };
43
44
 
44
45
  const DEFAULT_USER_TIME_ZONE = 'UTC';
45
46
 
47
+ type AgentToolMeta = {
48
+ isDangerous?: boolean;
49
+ };
50
+
46
51
  function hasRegisteredApiToolHandler(schema: IRegisteredApiSchema): schema is RegisteredApiToolSchema {
47
52
  return typeof (schema as { handler?: unknown }).handler === 'function';
48
53
  }
@@ -175,6 +180,7 @@ export type ApiBasedToolCallParams = {
175
180
  export type ApiBasedTool = {
176
181
  description?: string;
177
182
  input_schema?: unknown;
183
+ agent?: AgentToolMeta;
178
184
  call: (params?: ApiBasedToolCallParams) => Promise<string>;
179
185
  };
180
186
 
@@ -615,6 +621,7 @@ export function prepareApiBasedTools(
615
621
  apiBasedTools[toolName] = {
616
622
  description: schema.description,
617
623
  input_schema: schema.request_schema,
624
+ agent: schema.agent,
618
625
  call: async ({ adminUser, adminuser, abortSignal, inputs, userTimeZone, acceptLanguage } = {}) => {
619
626
  if (isHiddenResourceCall(hiddenResourceIdSet, inputs)) {
620
627
  return YAML.stringify({
package/build.log CHANGED
@@ -36,6 +36,7 @@ custom/conversation_area/ReasoningRenderer.vue
36
36
  custom/conversation_area/SystemMessageRenderer.vue
37
37
  custom/conversation_area/TextRenderer.vue
38
38
  custom/conversation_area/ThreeDotsAnimation.vue
39
+ custom/conversation_area/ToolApprovalRenderer.vue
39
40
  custom/conversation_area/ToolRenderer.vue
40
41
  custom/conversation_area/ToolsGroup.vue
41
42
  custom/incremark_code_renderers/
@@ -62,5 +63,5 @@ custom/speech_recognition_frontend/voiceActivityDetection.ts
62
63
  custom/speech_recognition_frontend/types/
63
64
  custom/speech_recognition_frontend/types/voice-activity-detection.d.ts
64
65
 
65
- sent 1,670,885 bytes received 921 bytes 3,343,612.00 bytes/sec
66
- total size is 1,666,790 speedup is 1.00
66
+ sent 1,682,812 bytes received 940 bytes 3,367,504.00 bytes/sec
67
+ total size is 1,678,584 speedup is 1.00
@@ -20,7 +20,8 @@
20
20
  'min-h-12 px-4 pt-4 rounded-xl w-full resize-none overflow-hidden text-lightInputText dark:text-darkInputText rounded-md bg-transparent text-sm bg-gray-50 dark:bg-gray-700 dark:border-gray-600 focus:outline-none',
21
21
  { '!text-base': coreStore.isIos }
22
22
  ]"
23
- :placeholder="agentStore.userMessagePlaceholder"
23
+ :placeholder="agentStore.hasPendingToolApproval ? 'Approve or reject the pending action to continue' : agentStore.userMessagePlaceholder"
24
+ :disabled="agentStore.isMessageInputBlocked"
24
25
  @keydown.enter.exact.prevent="sendMessage"
25
26
  />
26
27
  <div
@@ -70,7 +71,7 @@
70
71
  v-if="!agentStore.isResponseInProgress"
71
72
  class="absolute right-4 bottom-2 !p-0 h-9 w-9 transition-opacity duration-200"
72
73
  @click="sendMessage"
73
- :disabled="!agentStore.trimmedUserMessage || agentStore.isResponseInProgress"
74
+ :disabled="!agentStore.trimmedUserMessage || agentStore.isMessageInputBlocked"
74
75
  >
75
76
  <IconArrowUpOutline
76
77
  class="w-8 h-8 p-1
package/custom/chat.ts CHANGED
@@ -17,7 +17,7 @@ import type {
17
17
  ChatStatus,
18
18
  UIMessage,
19
19
  } from 'ai';
20
- import { AbstractChat, } from 'ai'
20
+ import { AbstractChat } from 'ai'
21
21
  import { Ref, ref } from 'vue';
22
22
 
23
23
  class VueChatState<