@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.
- 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/chat.ts +1 -1
- 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/ProcessingTimeline.vue +1 -0
- package/custom/conversation_area/ToolApprovalRenderer.vue +98 -0
- package/custom/conversation_area/ToolRenderer.vue +3 -2
- package/custom/conversation_area/ToolsGroup.vue +5 -1
- package/custom/skills/mutate_data/SKILL.md +10 -36
- package/custom/types.ts +5 -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/chat.ts +1 -1
- 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/ProcessingTimeline.vue +1 -0
- package/dist/custom/conversation_area/ToolApprovalRenderer.vue +98 -0
- package/dist/custom/conversation_area/ToolRenderer.vue +3 -2
- package/dist/custom/conversation_area/ToolsGroup.vue +5 -1
- package/dist/custom/skills/mutate_data/SKILL.md +10 -36
- package/dist/custom/types.ts +5 -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
|
@@ -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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
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(
|
|
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
|
-
"
|
|
28
|
-
"
|
|
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
|
|
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
|
-
|
|
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
|
|
27
|
-
_c =
|
|
28
|
-
|
|
29
|
-
const
|
|
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
|
-
|
|
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 ((
|
|
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 (!
|
|
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
|
-
|
|
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
|
};
|
package/dist/agentEvents.d.ts
CHANGED
|
@@ -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;
|
package/dist/agentTurnService.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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
|
|
200
|
+
fullResponse += agentResponse.text;
|
|
84
201
|
}
|
|
85
202
|
catch (error) {
|
|
86
|
-
if (((
|
|
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,
|
package/dist/apiBasedTools.d.ts
CHANGED
|
@@ -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 {};
|
package/dist/apiBasedTools.js
CHANGED
|
@@ -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.
|
|
74
|
+
:disabled="!agentStore.trimmedUserMessage || agentStore.isMessageInputBlocked"
|
|
74
75
|
>
|
|
75
76
|
<IconArrowUpOutline
|
|
76
77
|
class="w-8 h-8 p-1
|