@adminforth/agent 1.51.1 → 1.52.0

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 (50) 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/composables/agentStore/useAgentChat.ts +169 -6
  13. package/custom/composables/agentStore/useAgentSessions.ts +3 -1
  14. package/custom/composables/useAgentStore.ts +87 -0
  15. package/custom/conversation_area/MessageRenderer.vue +6 -1
  16. package/custom/conversation_area/ToolApprovalRenderer.vue +98 -0
  17. package/custom/skills/mutate_data/SKILL.md +10 -36
  18. package/custom/types.ts +4 -1
  19. package/dist/agent/middleware/apiBasedTools.js +26 -25
  20. package/dist/agent/runtime/AgentRuntime.d.ts +1 -1
  21. package/dist/agent/runtime/AgentRuntime.js +18 -3
  22. package/dist/agent/systemPrompt.js +3 -6
  23. package/dist/agent/turn/TurnLifecycleService.d.ts +8 -1
  24. package/dist/agent/turn/TurnLifecycleService.js +17 -1
  25. package/dist/agent/turn/TurnStreamConsumer.d.ts +2 -1
  26. package/dist/agent/turn/TurnStreamConsumer.js +14 -8
  27. package/dist/agent/turn/turnTypes.d.ts +14 -1
  28. package/dist/agentEvents.d.ts +4 -0
  29. package/dist/agentTurnService.d.ts +1 -0
  30. package/dist/agentTurnService.js +132 -14
  31. package/dist/apiBasedTools.d.ts +5 -0
  32. package/dist/apiBasedTools.js +1 -0
  33. package/dist/custom/ChatFooter.vue +3 -2
  34. package/dist/custom/composables/agentStore/useAgentChat.ts +169 -6
  35. package/dist/custom/composables/agentStore/useAgentSessions.ts +3 -1
  36. package/dist/custom/composables/useAgentStore.ts +87 -0
  37. package/dist/custom/conversation_area/MessageRenderer.vue +6 -1
  38. package/dist/custom/conversation_area/ToolApprovalRenderer.vue +98 -0
  39. package/dist/custom/skills/mutate_data/SKILL.md +10 -36
  40. package/dist/custom/types.ts +4 -1
  41. package/dist/endpoints/core.js +28 -0
  42. package/dist/index.js +1 -1
  43. package/dist/sessionStore.d.ts +1 -0
  44. package/dist/sessionStore.js +6 -0
  45. package/dist/surfaces/web-sse/createSseEventEmitter.js +13 -0
  46. package/endpoints/core.ts +30 -0
  47. package/index.ts +1 -1
  48. package/package.json +3 -6
  49. package/sessionStore.ts +11 -0
  50. package/surfaces/web-sse/createSseEventEmitter.ts +14 -0
@@ -8,12 +8,14 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
8
8
  });
9
9
  };
10
10
  import { ToolMessage } from "@langchain/core/messages";
11
+ import { isGraphInterrupt } from "@langchain/langgraph";
11
12
  import { createMiddleware } from "langchain";
12
13
  import { logger } from "adminforth";
13
14
  import { formatApiBasedToolCall, } from "../../apiBasedTools.js";
14
15
  import { createToolCallTracker, } from "../toolCallEvents.js";
15
16
  import { ALWAYS_AVAILABLE_API_TOOL_NAMES } from "../tools/index.js";
16
17
  import { createApiTool } from "../tools/apiTool.js";
18
+ import { isAbortError } from "../../errors.js";
17
19
  function getEnabledApiToolNames(messages) {
18
20
  const enabledToolNames = new Set();
19
21
  for (const message of messages) {
@@ -62,10 +64,14 @@ export function createApiBasedToolsMiddleware(apiBasedTools, adminforth) {
62
64
  },
63
65
  wrapToolCall(request, handler) {
64
66
  return __awaiter(this, void 0, void 0, function* () {
65
- var _a, _b, _c, _d;
67
+ var _a, _b, _c;
66
68
  const startedAt = Date.now();
67
69
  const toolInput = JSON.stringify((_a = request.toolCall.args) !== null && _a !== void 0 ? _a : {});
68
- const { adminUser, emit, sequenceDebugSink, userTimeZone } = request.runtime.context;
70
+ if (!request.toolCall.id) {
71
+ throw new Error(`Tool call "${request.toolCall.name}" has no id.`);
72
+ }
73
+ const toolCallId = request.toolCall.id;
74
+ const { adminUser, abortSignal, emit, sequenceDebugSink, userTimeZone } = request.runtime.context;
69
75
  const emitToolCall = (event) => {
70
76
  sequenceDebugSink.handleToolCallEvent(event);
71
77
  void (emit === null || emit === void 0 ? void 0 : emit({
@@ -92,7 +98,7 @@ export function createApiBasedToolsMiddleware(apiBasedTools, adminforth) {
92
98
  }
93
99
  const toolCallTracker = createToolCallTracker({
94
100
  emit: emitToolCall,
95
- toolCallId: request.toolCall.id,
101
+ toolCallId,
96
102
  toolName: request.toolCall.name,
97
103
  toolInfo,
98
104
  input: toolArgs,
@@ -101,32 +107,27 @@ export function createApiBasedToolsMiddleware(apiBasedTools, adminforth) {
101
107
  toolCallTracker.start();
102
108
  logger.info(`Invoking tool "${request.toolCall.name}" with input: ${toolInput}`);
103
109
  try {
104
- let result;
105
- if (request.tool) {
106
- result = yield handler(request);
107
- }
108
- else {
109
- const enabledApiToolNames = getEnabledApiToolNames(request.state.messages);
110
- if (enabledApiToolNames.has(request.toolCall.name)) {
111
- result = yield handler(Object.assign(Object.assign({}, request), { tool: dynamicTools[request.toolCall.name] }));
112
- }
113
- else {
114
- result = new ToolMessage({
115
- content: `Tool "${request.toolCall.name}" is not loaded. Call fetch_tool_schema first.`,
116
- tool_call_id: (_c = request.toolCall.id) !== null && _c !== void 0 ? _c : "",
117
- name: request.toolCall.name,
118
- status: "error",
119
- });
120
- }
121
- }
110
+ const result = getEnabledApiToolNames(request.state.messages).has(request.toolCall.name)
111
+ ? yield handler(Object.assign(Object.assign({}, request), { tool: dynamicTools[request.toolCall.name] }))
112
+ : yield handler(request);
122
113
  toolCallTracker.finishSuccess(result);
123
114
  return result;
124
115
  }
125
116
  catch (error) {
126
- const errorDetails = error instanceof Error ? (_d = error.stack) !== null && _d !== void 0 ? _d : error.message : String(error);
127
- logger.error(`Tool "${request.toolCall.name}" failed after ${Date.now() - startedAt}ms with input: ${toolInput}\n${errorDetails}`);
128
- toolCallTracker.finishError(error);
129
- throw error;
117
+ if (isGraphInterrupt(error)
118
+ || (abortSignal === null || abortSignal === void 0 ? void 0 : abortSignal.aborted)
119
+ || isAbortError(error)) {
120
+ throw error;
121
+ }
122
+ const message = error instanceof Error ? error.message : String(error);
123
+ logger.error(`Error calling tool "${request.toolCall.name}": ${error instanceof Error ? (_c = error.stack) !== null && _c !== void 0 ? _c : error.message : String(error)}`);
124
+ toolCallTracker.finishError(`Error: ${message}`);
125
+ return new ToolMessage({
126
+ name: request.toolCall.name,
127
+ tool_call_id: toolCallId,
128
+ status: "error",
129
+ content: `Error: ${message}`,
130
+ });
130
131
  }
131
132
  finally {
132
133
  logger.info(`Tool "${request.toolCall.name}" finished in ${Date.now() - startedAt}ms`);
@@ -11,5 +11,5 @@ export type AgentRuntimeOptions = {
11
11
  export declare class AgentRuntime {
12
12
  private readonly options;
13
13
  constructor(options: AgentRuntimeOptions);
14
- stream(input: AgentRuntimeRunInput): Promise<import("@langchain/core/utils/stream").IterableReadableStream<[import("langchain").BaseMessage<import("@langchain/core/messages").MessageStructure<import("@langchain/core/messages").MessageToolSet>, import("@langchain/core/messages").MessageType>, Record<string, any>]>>;
14
+ stream(input: AgentRuntimeRunInput): Promise<import("@langchain/core/utils/stream").IterableReadableStream<["messages", [import("langchain").BaseMessage<import("@langchain/core/messages").MessageStructure<import("@langchain/core/messages").MessageToolSet>, import("@langchain/core/messages").MessageType>, Record<string, any>]] | ["updates", Record<string, Omit<import("langchain").BuiltInState<import("@langchain/core/messages").MessageStructure<import("langchain").ToolsToMessageToolSet<readonly (import("@langchain/core/tools").ClientTool | import("@langchain/core/tools").ServerTool)[]>>>, "jumpTo">>]>>;
15
15
  }
@@ -7,11 +7,21 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
7
7
  step((generator = generator.apply(thisArg, _arguments || [])).next());
8
8
  });
9
9
  };
10
- import { createAgent, summarizationMiddleware } from "langchain";
10
+ import { createAgent, summarizationMiddleware, humanInTheLoopMiddleware } from "langchain";
11
11
  import { createApiBasedToolsMiddleware } from "../middleware/apiBasedTools.js";
12
12
  import { createSequenceDebugMiddleware } from "../middleware/sequenceDebug.js";
13
13
  import { createAgentLlmMetricsLogger } from "../simpleAgent.js";
14
14
  import { contextSchema, toLangchainAgentContext } from "./AgentContext.js";
15
+ function createHumanInTheLoopInterrupts(apiBasedTools) {
16
+ return Object.fromEntries(Object.entries(apiBasedTools)
17
+ .filter(([, apiBasedTool]) => { var _a; return ((_a = apiBasedTool.agent) === null || _a === void 0 ? void 0 : _a.isDangerous) === true; })
18
+ .map(([toolName]) => [
19
+ toolName,
20
+ {
21
+ allowedDecisions: ["approve", "reject"],
22
+ },
23
+ ]));
24
+ }
15
25
  export class AgentRuntime {
16
26
  constructor(options) {
17
27
  this.options = options;
@@ -24,8 +34,13 @@ export class AgentRuntime {
24
34
  const adminforth = this.options.getAdminforth();
25
35
  const apiBasedToolsMiddleware = createApiBasedToolsMiddleware(apiBasedTools, adminforth);
26
36
  const sequenceDebugMiddleware = createSequenceDebugMiddleware(input.observability.sequenceDebugSink);
37
+ const hitlMiddleware = humanInTheLoopMiddleware({
38
+ interruptOn: createHumanInTheLoopInterrupts(apiBasedTools),
39
+ descriptionPrefix: "Tool execution pending approval",
40
+ });
27
41
  const middleware = [
28
42
  apiBasedToolsMiddleware,
43
+ hitlMiddleware,
29
44
  ...((_a = input.models.modelMiddleware) !== null && _a !== void 0 ? _a : []),
30
45
  sequenceDebugMiddleware,
31
46
  summarizationMiddleware({
@@ -42,8 +57,8 @@ export class AgentRuntime {
42
57
  contextSchema,
43
58
  middleware,
44
59
  });
45
- return agent.stream({ messages: input.messages }, {
46
- streamMode: "messages",
60
+ return agent.stream(input.input, {
61
+ streamMode: ["messages", "updates"],
47
62
  recursionLimit: 100,
48
63
  callbacks: [createAgentLlmMetricsLogger()],
49
64
  signal: input.context.abortSignal,
@@ -24,11 +24,8 @@ export const DEFAULT_AGENT_SYSTEM_PROMPT = [
24
24
  "Do not add extra explanations or suggestions unless the user asks.",
25
25
  "Adapt to the user's tone and style of speaking, mirroring their vibe and wording.",
26
26
  "if the user speaks casually, you should respond casually too",
27
- "Never mutate data without user confirmation for a clearly described mutation plan.",
28
- "One confirmation may cover one mutation or one explicitly described batch/sequence of related mutations.",
29
- "If the confirmed plan has multiple steps, you may execute the whole confirmed plan without asking again between those steps.",
30
- "If the plan changes, expands, or you want to do anything beyond the confirmed plan, ask for confirmation again.",
31
- "Do not reuse an old confirmation for a new mutation plan.",
27
+ "Before calling a dangerous tool, briefly describe the exact action, target, and important changes in chat.",
28
+ "Do not ask the user for textual confirmation; dangerous tools are approved by the runtime approval UI.",
32
29
  ].join(" ");
33
30
  export function appendCustomSystemPrompt(systemPrompt, customSystemPrompt) {
34
31
  const normalizedCustomSystemPrompt = customSystemPrompt === null || customSystemPrompt === void 0 ? void 0 : customSystemPrompt.trim();
@@ -98,7 +95,7 @@ export function buildAgentSystemPrompt(adminforth_1) {
98
95
  "If the user wants to fetch records, load fetch_data first. If the user wants analytics or charts, load analyze_data first.",
99
96
  "Only call fetch_tool_schema for tool names that are explicitly mentioned in a fetched skill and are not already available as base tools.",
100
97
  "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.",
101
- "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.",
98
+ "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.",
102
99
  "When fetch_tool_schema succeeds, that tool becomes available on the next step.",
103
100
  "All admin links must be root-relative and start with '/'.",
104
101
  "Build record links as '/resource/{resourceId}/show/{primary key}'. Never use bare 'resource/{resourceId}/show/{primary key}' without the leading slash.",
@@ -1,14 +1,21 @@
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
  export declare class TurnLifecycleService {
5
6
  private readonly sessionStore;
6
7
  private readonly persistence;
7
- constructor(sessionStore: AgentSessionStore, persistence: TurnPersistenceService);
8
+ private readonly options;
9
+ constructor(sessionStore: AgentSessionStore, persistence: TurnPersistenceService, options: PluginOptions);
8
10
  start(input: BaseAgentTurnInput): Promise<{
9
11
  turnId: any;
10
12
  previousUserMessages: import("../languageDetect.js").PreviousUserMessage[];
11
13
  }>;
14
+ resume(input: BaseAgentTurnInput): Promise<{
15
+ turnId: any;
16
+ previousUserMessages: import("../languageDetect.js").PreviousUserMessage[];
17
+ initialResponse: string;
18
+ }>;
12
19
  finish(input: {
13
20
  turnId: string;
14
21
  responseText: string;
@@ -8,9 +8,10 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
8
8
  });
9
9
  };
10
10
  export class TurnLifecycleService {
11
- constructor(sessionStore, persistence) {
11
+ constructor(sessionStore, persistence, options) {
12
12
  this.sessionStore = sessionStore;
13
13
  this.persistence = persistence;
14
+ this.options = options;
14
15
  }
15
16
  start(input) {
16
17
  return __awaiter(this, void 0, void 0, function* () {
@@ -23,6 +24,21 @@ export class TurnLifecycleService {
23
24
  };
24
25
  });
25
26
  }
27
+ resume(input) {
28
+ return __awaiter(this, void 0, void 0, function* () {
29
+ const latestTurn = yield this.sessionStore.getLatestTurn(input.sessionId);
30
+ if (!latestTurn) {
31
+ throw new Error(`No agent turn found for session "${input.sessionId}".`);
32
+ }
33
+ return {
34
+ turnId: latestTurn[this.options.turnResource.idField],
35
+ previousUserMessages: yield this.sessionStore.getPreviousUserMessages(input.sessionId),
36
+ initialResponse: latestTurn[this.options.turnResource.responseField] === "not_finished"
37
+ ? ""
38
+ : String(latestTurn[this.options.turnResource.responseField]),
39
+ };
40
+ });
41
+ }
26
42
  finish(input) {
27
43
  return __awaiter(this, void 0, void 0, function* () {
28
44
  yield this.persistence.saveTurnResponse(input);
@@ -1,9 +1,10 @@
1
1
  import type { AgentEventEmitter } from "../../agentEvents.js";
2
2
  export declare class TurnStreamConsumer {
3
3
  consume(input: {
4
- stream: AsyncIterable<[any, any]>;
4
+ stream: AsyncIterable<["messages", [any, any]] | ["updates", Record<string, any>]>;
5
5
  abortSignal?: AbortSignal;
6
6
  emit?: AgentEventEmitter;
7
+ onInterrupt?: (interrupt: unknown) => void | Promise<void>;
7
8
  }): Promise<{
8
9
  text: string;
9
10
  }>;
@@ -19,18 +19,24 @@ export class TurnStreamConsumer {
19
19
  consume(input) {
20
20
  return __awaiter(this, void 0, void 0, function* () {
21
21
  var _a, e_1, _b, _c;
22
- var _d, _e;
22
+ var _d, _e, _f;
23
23
  let fullResponse = "";
24
24
  const textBuffer = new VegaLiteStreamBuffer();
25
25
  try {
26
- for (var _f = true, _g = __asyncValues(input.stream), _h; _h = yield _g.next(), _a = _h.done, !_a; _f = true) {
27
- _c = _h.value;
28
- _f = false;
29
- const rawChunk = _c;
26
+ for (var _g = true, _h = __asyncValues(input.stream), _j; _j = yield _h.next(), _a = _j.done, !_a; _g = true) {
27
+ _c = _j.value;
28
+ _g = false;
29
+ const [mode, chunk] = _c;
30
30
  if ((_d = input.abortSignal) === null || _d === void 0 ? void 0 : _d.aborted) {
31
31
  throw new DOMException("This operation was aborted", "AbortError");
32
32
  }
33
- const [token, metadata] = rawChunk;
33
+ if (mode === "updates") {
34
+ if ("__interrupt__" in chunk) {
35
+ yield ((_e = input.onInterrupt) === null || _e === void 0 ? void 0 : _e.call(input, chunk.__interrupt__));
36
+ }
37
+ continue;
38
+ }
39
+ const [token, metadata] = chunk;
34
40
  const nodeName = typeof (metadata === null || metadata === void 0 ? void 0 : metadata.langgraph_node) === "string"
35
41
  ? metadata.langgraph_node
36
42
  : "";
@@ -51,7 +57,7 @@ export class TurnStreamConsumer {
51
57
  .map((block) => { var _a; return String((_a = block.text) !== null && _a !== void 0 ? _a : ""); })
52
58
  .join("");
53
59
  if (reasoningDelta) {
54
- yield ((_e = input.emit) === null || _e === void 0 ? void 0 : _e.call(input, {
60
+ yield ((_f = input.emit) === null || _f === void 0 ? void 0 : _f.call(input, {
55
61
  type: "reasoning-delta",
56
62
  delta: reasoningDelta,
57
63
  }));
@@ -65,7 +71,7 @@ export class TurnStreamConsumer {
65
71
  catch (e_1_1) { e_1 = { error: e_1_1 }; }
66
72
  finally {
67
73
  try {
68
- if (!_f && !_a && (_b = _g.return)) yield _b.call(_g);
74
+ if (!_g && !_a && (_b = _h.return)) yield _b.call(_h);
69
75
  }
70
76
  finally { if (e_1) throw e_1.error; }
71
77
  }
@@ -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";
@@ -18,6 +19,7 @@ export type BaseAgentTurnInput = {
18
19
  };
19
20
  export type TextAgentTurnInput = BaseAgentTurnInput & {
20
21
  emit: AgentEventEmitter;
22
+ approvalDecision?: "approve" | "reject";
21
23
  failureLogMessage?: string;
22
24
  abortLogMessage?: string;
23
25
  };
@@ -54,6 +56,14 @@ export type PreparedAgentTurn = {
54
56
  modeName?: string | null;
55
57
  context: AgentTurnContext;
56
58
  observability: AgentTurnObservability;
59
+ resume?: {
60
+ decision: "approve" | "reject";
61
+ interrupts?: {
62
+ id: string;
63
+ count: number;
64
+ }[];
65
+ };
66
+ initialResponse?: string;
57
67
  };
58
68
  export type AgentTurnModels = {
59
69
  model: AgentChatModel;
@@ -62,12 +72,15 @@ export type AgentTurnModels = {
62
72
  };
63
73
  export type AgentRuntimeRunInput = {
64
74
  models: AgentTurnModels;
65
- messages: Messages;
75
+ input: {
76
+ messages: Messages;
77
+ } | Command;
66
78
  context: AgentTurnContext;
67
79
  observability: AgentTurnObservability;
68
80
  };
69
81
  export type RunAndPersistAgentResponseInput = BaseAgentTurnInput & {
70
82
  emit?: AgentEventEmitter;
83
+ approvalDecision?: "approve" | "reject";
71
84
  failureLogMessage: string;
72
85
  abortLogMessage: string;
73
86
  };
@@ -15,6 +15,10 @@ export type AgentEvent = {
15
15
  type: "rendering";
16
16
  phase: "start" | "end";
17
17
  label: string;
18
+ } | {
19
+ type: "interrupt";
20
+ sessionId: string;
21
+ interrupt: unknown;
18
22
  } | {
19
23
  type: "open-page";
20
24
  targetPath: string;
@@ -15,6 +15,7 @@ export declare class AgentTurnService {
15
15
  private readonly promptBuilder;
16
16
  private readonly runtime;
17
17
  private readonly streamConsumer;
18
+ private readonly pendingInterrupts;
18
19
  constructor(lifecycle: TurnLifecycleService, contextBuilder: TurnContextBuilder, modeResolver: AgentModeResolver, modelFactory: AgentModelFactory, promptBuilder: TurnPromptBuilder, runtime: AgentRuntime, streamConsumer: TurnStreamConsumer);
19
20
  private prepareTurn;
20
21
  private runAgentTurn;
@@ -9,8 +9,72 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
9
9
  };
10
10
  import { logger } from "adminforth";
11
11
  import { randomUUID } from "crypto";
12
+ import { Command } from "@langchain/langgraph";
12
13
  import { createSequenceDebugCollector } from "./agent/middleware/sequenceDebug.js";
13
14
  import { getErrorMessage, isAbortError } from "./errors.js";
15
+ function getApprovalDecision(input) {
16
+ return "approvalDecision" in input
17
+ && (input.approvalDecision === "approve" || input.approvalDecision === "reject")
18
+ ? input.approvalDecision
19
+ : undefined;
20
+ }
21
+ function getInterruptItems(interrupt) {
22
+ return Array.isArray(interrupt) ? interrupt : [interrupt];
23
+ }
24
+ function getHitlInterrupts(interrupt) {
25
+ return getInterruptItems(interrupt).flatMap((item) => {
26
+ const value = item && typeof item === "object" && "value" in item
27
+ ? item.value
28
+ : item;
29
+ const actionRequests = value && typeof value === "object"
30
+ ? value.actionRequests
31
+ : undefined;
32
+ const interruptId = item && typeof item === "object"
33
+ ? item.id
34
+ : undefined;
35
+ return typeof interruptId === "string" && Array.isArray(actionRequests)
36
+ ? [{ id: interruptId, count: actionRequests.length }]
37
+ : [];
38
+ });
39
+ }
40
+ function buildHitlDecision(decision, prompt) {
41
+ if (decision === "approve") {
42
+ return { type: "approve" };
43
+ }
44
+ return {
45
+ type: "reject",
46
+ message: prompt
47
+ ? `User rejected the pending tool execution and sent a new instruction instead: ${prompt}`
48
+ : "User rejected executing this tool",
49
+ };
50
+ }
51
+ function buildHitlResumeValue(input) {
52
+ return {
53
+ decisions: Array.from({ length: input.count }, () => (buildHitlDecision(input.decision, input.prompt))),
54
+ };
55
+ }
56
+ function buildLangGraphResume(input) {
57
+ var _a;
58
+ const interrupts = (_a = input.interrupts) !== null && _a !== void 0 ? _a : [];
59
+ if (interrupts.length === 0) {
60
+ throw new Error("No pending approval interrupt found for resume.");
61
+ }
62
+ if (interrupts.length === 1) {
63
+ return buildHitlResumeValue({
64
+ decision: input.decision,
65
+ count: interrupts[0].count,
66
+ prompt: input.prompt,
67
+ });
68
+ }
69
+ return Object.fromEntries(interrupts.map((interrupt) => [
70
+ interrupt.id,
71
+ buildHitlResumeValue({
72
+ decision: input.decision,
73
+ count: interrupt.count,
74
+ prompt: input.prompt,
75
+ }),
76
+ ]));
77
+ }
14
78
  export class AgentTurnService {
15
79
  constructor(lifecycle, contextBuilder, modeResolver, modelFactory, promptBuilder, runtime, streamConsumer) {
16
80
  this.lifecycle = lifecycle;
@@ -20,26 +84,44 @@ export class AgentTurnService {
20
84
  this.promptBuilder = promptBuilder;
21
85
  this.runtime = runtime;
22
86
  this.streamConsumer = streamConsumer;
87
+ this.pendingInterrupts = new Map();
23
88
  }
24
89
  prepareTurn(input) {
25
90
  return __awaiter(this, void 0, void 0, function* () {
26
91
  const sequenceDebugCollector = createSequenceDebugCollector();
27
- const { turnId, previousUserMessages } = yield this.lifecycle.start(input);
92
+ const approvalDecision = getApprovalDecision(input);
93
+ const shouldResume = Boolean(approvalDecision);
94
+ const pendingInterrupts = this.pendingInterrupts.get(input.sessionId);
95
+ if (shouldResume && (!pendingInterrupts || pendingInterrupts.length === 0)) {
96
+ throw new Error(`No pending approval interrupt found for session "${input.sessionId}".`);
97
+ }
98
+ const lifecycleTurn = shouldResume
99
+ ? yield this.lifecycle.resume(input)
100
+ : yield this.lifecycle.start(input);
28
101
  const context = yield this.contextBuilder.build({
29
102
  base: input,
30
- turnId,
103
+ turnId: lifecycleTurn.turnId,
31
104
  });
32
105
  return {
33
106
  prompt: input.prompt,
34
107
  sessionId: input.sessionId,
35
- turnId,
36
- previousUserMessages,
108
+ turnId: lifecycleTurn.turnId,
109
+ previousUserMessages: lifecycleTurn.previousUserMessages,
37
110
  modeName: input.modeName,
38
111
  context,
39
112
  observability: {
40
113
  emit: undefined,
41
114
  sequenceDebugSink: sequenceDebugCollector,
42
115
  },
116
+ resume: shouldResume
117
+ ? {
118
+ decision: approvalDecision,
119
+ interrupts: pendingInterrupts,
120
+ }
121
+ : undefined,
122
+ initialResponse: shouldResume && "initialResponse" in lifecycleTurn
123
+ ? lifecycleTurn.initialResponse
124
+ : undefined,
43
125
  };
44
126
  });
45
127
  }
@@ -59,31 +141,66 @@ export class AgentTurnService {
59
141
  ]);
60
142
  const stream = yield this.runtime.stream({
61
143
  models,
62
- messages,
144
+ input: input.resume
145
+ ? new Command({
146
+ resume: buildLangGraphResume({
147
+ decision: input.resume.decision,
148
+ interrupts: input.resume.interrupts,
149
+ prompt: input.prompt,
150
+ }),
151
+ })
152
+ : { messages },
63
153
  context: input.context,
64
154
  observability: input.observability,
65
155
  });
66
- return this.streamConsumer.consume({
67
- stream: stream,
68
- abortSignal: input.context.abortSignal,
69
- emit: input.observability.emit,
70
- });
156
+ let interrupted = false;
157
+ try {
158
+ return yield this.streamConsumer.consume({
159
+ stream: stream,
160
+ abortSignal: input.context.abortSignal,
161
+ emit: input.observability.emit,
162
+ onInterrupt: (interrupt) => __awaiter(this, void 0, void 0, function* () {
163
+ var _a, _b, _c;
164
+ interrupted = true;
165
+ const interrupts = getHitlInterrupts(interrupt);
166
+ const pendingInterrupts = (_a = this.pendingInterrupts.get(input.sessionId)) !== null && _a !== void 0 ? _a : [];
167
+ const mergedInterrupts = new Map(pendingInterrupts.map((pendingInterrupt) => [
168
+ pendingInterrupt.id,
169
+ pendingInterrupt.count,
170
+ ]));
171
+ for (const pendingInterrupt of interrupts) {
172
+ mergedInterrupts.set(pendingInterrupt.id, pendingInterrupt.count);
173
+ }
174
+ this.pendingInterrupts.set(input.sessionId, [...mergedInterrupts.entries()].map(([id, count]) => ({ id, count })));
175
+ yield ((_c = (_b = input.observability).emit) === null || _c === void 0 ? void 0 : _c.call(_b, {
176
+ type: "interrupt",
177
+ sessionId: input.sessionId,
178
+ interrupt,
179
+ }));
180
+ }),
181
+ });
182
+ }
183
+ finally {
184
+ if (!interrupted) {
185
+ this.pendingInterrupts.delete(input.sessionId);
186
+ }
187
+ }
71
188
  });
72
189
  }
73
190
  runAndPersistAgentResponse(input) {
74
191
  return __awaiter(this, void 0, void 0, function* () {
75
- var _a;
192
+ var _a, _b;
76
193
  const preparedTurn = yield this.prepareTurn(input);
77
194
  preparedTurn.observability.emit = input.emit;
78
- let fullResponse = "";
195
+ let fullResponse = (_a = preparedTurn.initialResponse) !== null && _a !== void 0 ? _a : "";
79
196
  let aborted = false;
80
197
  let failed = false;
81
198
  try {
82
199
  const agentResponse = yield this.runAgentTurn(preparedTurn);
83
- fullResponse = agentResponse.text;
200
+ fullResponse += agentResponse.text;
84
201
  }
85
202
  catch (error) {
86
- if (((_a = input.abortSignal) === null || _a === void 0 ? void 0 : _a.aborted) || isAbortError(error)) {
203
+ if (((_b = input.abortSignal) === null || _b === void 0 ? void 0 : _b.aborted) || isAbortError(error)) {
87
204
  aborted = true;
88
205
  logger.info(input.abortLogMessage);
89
206
  }
@@ -122,6 +239,7 @@ export class AgentTurnService {
122
239
  currentPage: input.currentPage,
123
240
  chatSurface: input.chatSurface,
124
241
  adminPublicOrigin: input.adminPublicOrigin,
242
+ approvalDecision: input.approvalDecision,
125
243
  abortSignal: input.abortSignal,
126
244
  adminUser: input.adminUser,
127
245
  emit: input.emit,
@@ -1,4 +1,7 @@
1
1
  import { type AdminUser, type IAdminForth } from 'adminforth';
2
+ type AgentToolMeta = {
3
+ isDangerous?: boolean;
4
+ };
2
5
  export type ApiBasedToolCallParams = {
3
6
  adminUser?: AdminUser;
4
7
  adminuser?: AdminUser;
@@ -10,6 +13,7 @@ export type ApiBasedToolCallParams = {
10
13
  export type ApiBasedTool = {
11
14
  description?: string;
12
15
  input_schema?: unknown;
16
+ agent?: AgentToolMeta;
13
17
  call: (params?: ApiBasedToolCallParams) => Promise<string>;
14
18
  };
15
19
  export declare function formatApiBasedToolCall(params: {
@@ -20,3 +24,4 @@ export declare function formatApiBasedToolCall(params: {
20
24
  userTimeZone?: string;
21
25
  }): Promise<string | undefined>;
22
26
  export declare function prepareApiBasedTools(adminforth: IAdminForth, hiddenResourceIds?: Iterable<string>): Record<string, ApiBasedTool>;
27
+ export {};
@@ -410,6 +410,7 @@ export function prepareApiBasedTools(adminforth, hiddenResourceIds = []) {
410
410
  apiBasedTools[toolName] = {
411
411
  description: schema.description,
412
412
  input_schema: schema.request_schema,
413
+ agent: schema.agent,
413
414
  call: (...args_1) => __awaiter(this, [...args_1], void 0, function* ({ adminUser, adminuser, abortSignal, inputs, userTimeZone, acceptLanguage } = {}) {
414
415
  if (isHiddenResourceCall(hiddenResourceIdSet, inputs)) {
415
416
  return YAML.stringify({
@@ -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