@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.
- package/agent/middleware/apiBasedTools.ts +31 -25
- package/agent/runtime/AgentRuntime.ts +24 -3
- package/agent/systemPrompt.ts +3 -6
- package/agent/turn/TurnLifecycleService.ts +18 -0
- package/agent/turn/TurnStreamConsumer.ts +11 -3
- package/agent/turn/turnTypes.ts +9 -1
- package/agentEvents.ts +5 -0
- package/agentTurnService.ts +158 -12
- package/apiBasedTools.ts +7 -0
- package/build.log +3 -2
- package/custom/ChatFooter.vue +3 -2
- package/custom/composables/agentStore/useAgentChat.ts +169 -6
- package/custom/composables/agentStore/useAgentSessions.ts +3 -1
- package/custom/composables/useAgentStore.ts +87 -0
- package/custom/conversation_area/MessageRenderer.vue +6 -1
- package/custom/conversation_area/ToolApprovalRenderer.vue +98 -0
- package/custom/skills/mutate_data/SKILL.md +10 -36
- package/custom/types.ts +4 -1
- package/dist/agent/middleware/apiBasedTools.js +26 -25
- package/dist/agent/runtime/AgentRuntime.d.ts +1 -1
- package/dist/agent/runtime/AgentRuntime.js +18 -3
- package/dist/agent/systemPrompt.js +3 -6
- package/dist/agent/turn/TurnLifecycleService.d.ts +8 -1
- package/dist/agent/turn/TurnLifecycleService.js +17 -1
- package/dist/agent/turn/TurnStreamConsumer.d.ts +2 -1
- package/dist/agent/turn/TurnStreamConsumer.js +14 -8
- package/dist/agent/turn/turnTypes.d.ts +14 -1
- package/dist/agentEvents.d.ts +4 -0
- package/dist/agentTurnService.d.ts +1 -0
- package/dist/agentTurnService.js +132 -14
- package/dist/apiBasedTools.d.ts +5 -0
- package/dist/apiBasedTools.js +1 -0
- package/dist/custom/ChatFooter.vue +3 -2
- package/dist/custom/composables/agentStore/useAgentChat.ts +169 -6
- package/dist/custom/composables/agentStore/useAgentSessions.ts +3 -1
- package/dist/custom/composables/useAgentStore.ts +87 -0
- package/dist/custom/conversation_area/MessageRenderer.vue +6 -1
- package/dist/custom/conversation_area/ToolApprovalRenderer.vue +98 -0
- package/dist/custom/skills/mutate_data/SKILL.md +10 -36
- package/dist/custom/types.ts +4 -1
- package/dist/endpoints/core.js +28 -0
- package/dist/index.js +1 -1
- package/dist/sessionStore.d.ts +1 -0
- package/dist/sessionStore.js +6 -0
- package/dist/surfaces/web-sse/createSseEventEmitter.js +13 -0
- package/endpoints/core.ts +30 -0
- package/index.ts +1 -1
- package/package.json +3 -6
- package/sessionStore.ts +11 -0
- 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
|
-
|
|
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
|
|
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
|
-
|
|
136
|
-
|
|
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
|
-
|
|
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
|
-
|
|
154
|
-
|
|
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
|
-
`
|
|
158
|
+
`Error calling tool "${request.toolCall.name}": ${error instanceof Error ? error.stack ?? error.message : String(error)}`,
|
|
158
159
|
);
|
|
159
|
-
toolCallTracker.finishError(
|
|
160
|
-
|
|
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(
|
|
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,
|
package/agent/systemPrompt.ts
CHANGED
|
@@ -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
|
-
"
|
|
29
|
-
"
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
package/agent/turn/turnTypes.ts
CHANGED
|
@@ -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
package/agentTurnService.ts
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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
|
|
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,
|
|
66
|
-
total size is 1,
|
|
66
|
+
sent 1,682,590 bytes received 940 bytes 3,367,060.00 bytes/sec
|
|
67
|
+
total size is 1,678,362 speedup is 1.00
|
package/custom/ChatFooter.vue
CHANGED
|
@@ -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.
|
|
74
|
+
:disabled="!agentStore.trimmedUserMessage || agentStore.isMessageInputBlocked"
|
|
74
75
|
>
|
|
75
76
|
<IconArrowUpOutline
|
|
76
77
|
class="w-8 h-8 p-1
|