@adminforth/agent 2.8.0 → 2.8.2
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/dist/application/ports.d.ts +4 -3
- package/dist/application/runTurnUseCase.d.ts +9 -0
- package/dist/application/runTurnUseCase.js +49 -17
- package/dist/custom/composables/agentStore/useAgentSessions.ts +8 -0
- package/dist/custom/composables/useAgentStore.ts +16 -7
- package/dist/custom/conversation_area/ToolApprovalRenderer.vue +1 -1
- package/dist/custom/types.ts +2 -0
- package/dist/domain/turnTypes.d.ts +3 -2
- package/dist/index.js +1 -0
- package/dist/llm/streamAdapter.d.ts +3 -9
- package/dist/llm/streamAdapter.js +21 -29
- package/dist/tools/approvalFormatter.js +6 -4
- package/dist/transport/http/context.d.ts +2 -1
- package/dist/transport/http/sessionEndpoints.js +7 -1
- package/package.json +1 -1
|
@@ -57,9 +57,10 @@ export interface LlmPort {
|
|
|
57
57
|
/**
|
|
58
58
|
* Rebuild the pending human-in-the-loop interrupts for a session from persisted
|
|
59
59
|
* checkpoint state (used when the in-process cache is empty — after a restart or
|
|
60
|
-
* on another instance). Returns already-normalized descriptors
|
|
61
|
-
* interrupt shape never leaves the llm
|
|
62
|
-
* failure (the caller must not treat that as
|
|
60
|
+
* on another instance). Returns already-normalized descriptors, each carrying the
|
|
61
|
+
* tool calls it is waiting on; the LangGraph interrupt shape never leaves the llm
|
|
62
|
+
* layer. Throws on a checkpoint/runtime failure (the caller must not treat that as
|
|
63
|
+
* "no pending interrupt").
|
|
63
64
|
*/
|
|
64
65
|
getPendingInterrupts(input: {
|
|
65
66
|
completionAdapter: AgentModeCompletionAdapter;
|
|
@@ -70,6 +70,15 @@ export declare class RunTurnUseCase {
|
|
|
70
70
|
* MemorySaver the in-process cache is authoritative (single process, cannot be stale).
|
|
71
71
|
*/
|
|
72
72
|
private resolvePendingInterrupts;
|
|
73
|
+
/** Describe pending tool calls in plain language an admin (not a developer) can act on. */
|
|
74
|
+
private describeApprovals;
|
|
75
|
+
/**
|
|
76
|
+
* Describe what a session is still waiting for approval on, so a client that (re)loads
|
|
77
|
+
* the conversation can rebuild the approval prompt — it lives only in the browser, so a
|
|
78
|
+
* page reload would otherwise leave the user with no way to answer it. Empty when
|
|
79
|
+
* nothing is pending.
|
|
80
|
+
*/
|
|
81
|
+
getPendingApprovals(sessionId: string, modeName?: string | null): Promise<string[]>;
|
|
73
82
|
private requiresSessionOwnership;
|
|
74
83
|
/** Verify the session exists and belongs to the requesting admin user. */
|
|
75
84
|
assertSessionOwnership(sessionId: string, adminUser: BaseAgentTurnInput["adminUser"]): Promise<void>;
|
|
@@ -122,6 +122,29 @@ export class RunTurnUseCase {
|
|
|
122
122
|
return (_a = this.pendingInterrupts.get(sessionId)) !== null && _a !== void 0 ? _a : [];
|
|
123
123
|
});
|
|
124
124
|
}
|
|
125
|
+
/** Describe pending tool calls in plain language an admin (not a developer) can act on. */
|
|
126
|
+
describeApprovals(interrupts) {
|
|
127
|
+
const adminforth = this.deps.getAdminforth();
|
|
128
|
+
return interrupts
|
|
129
|
+
.flatMap((interrupt) => interrupt.requests)
|
|
130
|
+
.map((request) => formatApprovalRequest(adminforth, request));
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Describe what a session is still waiting for approval on, so a client that (re)loads
|
|
134
|
+
* the conversation can rebuild the approval prompt — it lives only in the browser, so a
|
|
135
|
+
* page reload would otherwise leave the user with no way to answer it. Empty when
|
|
136
|
+
* nothing is pending.
|
|
137
|
+
*/
|
|
138
|
+
getPendingApprovals(sessionId, modeName) {
|
|
139
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
140
|
+
// Loading a conversation must not fail because the checkpoint store is unhappy
|
|
141
|
+
// (already logged by resolvePendingInterrupts). Without the prompt the user simply
|
|
142
|
+
// falls back to the implicit reject in `prepareTurn`.
|
|
143
|
+
const interrupts = yield this.resolvePendingInterrupts(sessionId, this.resolveMode(modeName))
|
|
144
|
+
.catch(() => []);
|
|
145
|
+
return this.describeApprovals(interrupts);
|
|
146
|
+
});
|
|
147
|
+
}
|
|
125
148
|
requiresSessionOwnership(input) {
|
|
126
149
|
return !input.chatSurface;
|
|
127
150
|
}
|
|
@@ -194,6 +217,7 @@ export class RunTurnUseCase {
|
|
|
194
217
|
mode,
|
|
195
218
|
context: this.buildContext(input, editTurnId),
|
|
196
219
|
observability: { emit: input.emit, sequenceDebugSink },
|
|
220
|
+
freshPrompt: true,
|
|
197
221
|
branchFromCheckpointId,
|
|
198
222
|
};
|
|
199
223
|
});
|
|
@@ -213,20 +237,31 @@ export class RunTurnUseCase {
|
|
|
213
237
|
let turnId;
|
|
214
238
|
let previousUserMessages = [];
|
|
215
239
|
let initialResponse;
|
|
216
|
-
let
|
|
240
|
+
let resume;
|
|
217
241
|
if (shouldResume) {
|
|
218
|
-
resumeInterrupts = yield this.resolvePendingInterrupts(input.sessionId, mode);
|
|
242
|
+
const resumeInterrupts = yield this.resolvePendingInterrupts(input.sessionId, mode);
|
|
219
243
|
if (resumeInterrupts.length === 0) {
|
|
220
244
|
throw new Error(`No pending approval interrupt found for session "${input.sessionId}".`);
|
|
221
245
|
}
|
|
222
246
|
const resumeState = yield this.deps.sessions.getResumeState(input.sessionId);
|
|
223
247
|
turnId = resumeState.turnId;
|
|
224
248
|
initialResponse = resumeState.initialResponse;
|
|
249
|
+
resume = { decision: approvalDecision, interrupts: resumeInterrupts };
|
|
225
250
|
}
|
|
226
251
|
else {
|
|
252
|
+
// A pending approval leaves the provider thread with a tool call that has no
|
|
253
|
+
// result, so a plain new message would be rejected wholesale ("No tool output
|
|
254
|
+
// found for function call ..."). Sending one means the user moved on, so the
|
|
255
|
+
// pending call is rejected as part of this turn and the prompt rides along in
|
|
256
|
+
// the rejection. Reached whenever the approval UI is gone — after a page
|
|
257
|
+
// reload, from a second tab, or from a chat surface.
|
|
258
|
+
const pending = yield this.resolvePendingInterrupts(input.sessionId, mode);
|
|
227
259
|
previousUserMessages = yield this.deps.sessions.getPreviousUserMessages(input.sessionId);
|
|
228
260
|
turnId = yield this.deps.sessions.createNewTurn(input.sessionId, input.prompt);
|
|
229
261
|
yield this.deps.sessions.touchSession(input.sessionId);
|
|
262
|
+
if (pending.length > 0) {
|
|
263
|
+
resume = { decision: "reject", interrupts: pending };
|
|
264
|
+
}
|
|
230
265
|
}
|
|
231
266
|
return {
|
|
232
267
|
prompt: input.prompt,
|
|
@@ -239,9 +274,8 @@ export class RunTurnUseCase {
|
|
|
239
274
|
emit: input.emit,
|
|
240
275
|
sequenceDebugSink,
|
|
241
276
|
},
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
: undefined,
|
|
277
|
+
freshPrompt: !shouldResume,
|
|
278
|
+
resume,
|
|
245
279
|
initialResponse,
|
|
246
280
|
};
|
|
247
281
|
});
|
|
@@ -249,7 +283,7 @@ export class RunTurnUseCase {
|
|
|
249
283
|
resolveUserLanguage(prepared) {
|
|
250
284
|
return __awaiter(this, void 0, void 0, function* () {
|
|
251
285
|
var _a;
|
|
252
|
-
if (prepared.
|
|
286
|
+
if (!prepared.freshPrompt) {
|
|
253
287
|
return (_a = this.lastDetectedLanguage.get(prepared.sessionId)) !== null && _a !== void 0 ? _a : null;
|
|
254
288
|
}
|
|
255
289
|
const detected = yield this.deps.llm
|
|
@@ -273,30 +307,28 @@ export class RunTurnUseCase {
|
|
|
273
307
|
buildMessages(prepared) {
|
|
274
308
|
return [{ role: "user", content: prepared.prompt }];
|
|
275
309
|
}
|
|
276
|
-
handleInterrupt(prepared, interrupt, descriptors
|
|
310
|
+
handleInterrupt(prepared, interrupt, descriptors) {
|
|
277
311
|
return __awaiter(this, void 0, void 0, function* () {
|
|
278
312
|
var _a, _b, _c;
|
|
279
313
|
if (!this.deps.hasPersistentCheckpointer) {
|
|
280
314
|
const existing = (_a = this.pendingInterrupts.get(prepared.sessionId)) !== null && _a !== void 0 ? _a : [];
|
|
281
|
-
const merged = new Map(existing.map((item) => [item.id, item
|
|
315
|
+
const merged = new Map(existing.map((item) => [item.id, item]));
|
|
282
316
|
for (const item of descriptors) {
|
|
283
|
-
merged.set(item.id, item
|
|
317
|
+
merged.set(item.id, item);
|
|
284
318
|
}
|
|
285
|
-
this.pendingInterrupts.set(prepared.sessionId, [...merged.
|
|
319
|
+
this.pendingInterrupts.set(prepared.sessionId, [...merged.values()]);
|
|
286
320
|
}
|
|
287
|
-
const adminforth = this.deps.getAdminforth();
|
|
288
321
|
yield ((_c = (_b = prepared.observability).emit) === null || _c === void 0 ? void 0 : _c.call(_b, {
|
|
289
322
|
type: "interrupt",
|
|
290
323
|
sessionId: prepared.sessionId,
|
|
291
324
|
interrupt,
|
|
292
|
-
approvals:
|
|
325
|
+
approvals: this.describeApprovals(descriptors),
|
|
293
326
|
}));
|
|
294
327
|
});
|
|
295
328
|
}
|
|
296
329
|
runAgentTurn(prepared) {
|
|
297
330
|
return __awaiter(this, void 0, void 0, function* () {
|
|
298
331
|
var _a, e_1, _b, _c;
|
|
299
|
-
var _d;
|
|
300
332
|
const userLanguage = yield this.resolveUserLanguage(prepared);
|
|
301
333
|
const streamInput = prepared.resume
|
|
302
334
|
? {
|
|
@@ -322,16 +354,16 @@ export class RunTurnUseCase {
|
|
|
322
354
|
let interrupted = false;
|
|
323
355
|
try {
|
|
324
356
|
try {
|
|
325
|
-
for (var
|
|
357
|
+
for (var _d = true, stream_1 = __asyncValues(stream), stream_1_1; stream_1_1 = yield stream_1.next(), _a = stream_1_1.done, !_a; _d = true) {
|
|
326
358
|
_c = stream_1_1.value;
|
|
327
|
-
|
|
359
|
+
_d = false;
|
|
328
360
|
const chunk = _c;
|
|
329
361
|
if (abortSignal === null || abortSignal === void 0 ? void 0 : abortSignal.aborted) {
|
|
330
362
|
throw new DOMException("This operation was aborted", "AbortError");
|
|
331
363
|
}
|
|
332
364
|
if (chunk.kind === "interrupt") {
|
|
333
365
|
interrupted = true;
|
|
334
|
-
yield this.handleInterrupt(prepared, chunk.interrupt, chunk.descriptors
|
|
366
|
+
yield this.handleInterrupt(prepared, chunk.interrupt, chunk.descriptors);
|
|
335
367
|
continue;
|
|
336
368
|
}
|
|
337
369
|
if (chunk.kind === "reasoning") {
|
|
@@ -349,7 +381,7 @@ export class RunTurnUseCase {
|
|
|
349
381
|
catch (e_1_1) { e_1 = { error: e_1_1 }; }
|
|
350
382
|
finally {
|
|
351
383
|
try {
|
|
352
|
-
if (!
|
|
384
|
+
if (!_d && !_a && (_b = stream_1.return)) yield _b.call(stream_1);
|
|
353
385
|
}
|
|
354
386
|
finally { if (e_1) throw e_1.error; }
|
|
355
387
|
}
|
|
@@ -23,6 +23,7 @@ type CreateAgentSessionManagerOptions = {
|
|
|
23
23
|
blockCloseOfChat: Ref<boolean>;
|
|
24
24
|
adminforth: AdminforthLike;
|
|
25
25
|
setCurrentChat: (sessionId: string) => void;
|
|
26
|
+
restorePendingToolApproval: (sessionId: string, messages: string[]) => void;
|
|
26
27
|
};
|
|
27
28
|
|
|
28
29
|
export function createAgentSessionManager({
|
|
@@ -39,6 +40,7 @@ export function createAgentSessionManager({
|
|
|
39
40
|
blockCloseOfChat,
|
|
40
41
|
adminforth,
|
|
41
42
|
setCurrentChat,
|
|
43
|
+
restorePendingToolApproval,
|
|
42
44
|
}: CreateAgentSessionManagerOptions) {
|
|
43
45
|
function sortSessionsListByTimestamp(sessionsListToSort: ISessionsListItem[]) {
|
|
44
46
|
return [...sessionsListToSort].sort((a: ISessionsListItem, b: ISessionsListItem) => b.timestamp.localeCompare(a.timestamp));
|
|
@@ -113,6 +115,12 @@ export function createAgentSessionManager({
|
|
|
113
115
|
// The session fetch can bail out (deleted/expired session), leaving no stored
|
|
114
116
|
// messages — keep `messages` an array so consumers can iterate it safely.
|
|
115
117
|
currentChat.value.messages = (currentSession.value?.messages ?? []).flatMap(mapStoredMessage);
|
|
118
|
+
// Only the turns are stored; an approval the agent is still waiting on is replayed
|
|
119
|
+
// from the backend so the user can answer it after a reload instead of being stuck.
|
|
120
|
+
const pendingApprovals = currentSession.value?.pendingApprovals ?? [];
|
|
121
|
+
if (pendingApprovals.length) {
|
|
122
|
+
restorePendingToolApproval(sessionId, pendingApprovals);
|
|
123
|
+
}
|
|
116
124
|
}
|
|
117
125
|
}
|
|
118
126
|
|
|
@@ -123,18 +123,13 @@ export const useAgentStore = defineStore('agent', () => {
|
|
|
123
123
|
});
|
|
124
124
|
}
|
|
125
125
|
|
|
126
|
-
function
|
|
127
|
-
// The backend describes each pending tool call in plain language; its raw
|
|
128
|
-
// descriptions are only the fallback when it could not.
|
|
129
|
-
const formatted = Array.isArray(approvals)
|
|
130
|
-
? approvals.filter((line): line is string => typeof line === 'string' && Boolean(line))
|
|
131
|
-
: [];
|
|
126
|
+
function addToolApprovalPart(sessionId: string, messages: string[]) {
|
|
132
127
|
const approvalPart = {
|
|
133
128
|
type: 'data-tool-approval' as const,
|
|
134
129
|
data: {
|
|
135
130
|
sessionId,
|
|
136
131
|
status: 'pending' as const,
|
|
137
|
-
messages
|
|
132
|
+
messages,
|
|
138
133
|
},
|
|
139
134
|
};
|
|
140
135
|
const lastChatMessage = currentChat.value?.lastMessage;
|
|
@@ -151,6 +146,16 @@ export const useAgentStore = defineStore('agent', () => {
|
|
|
151
146
|
});
|
|
152
147
|
}
|
|
153
148
|
|
|
149
|
+
function addToolApprovalMessage(sessionId: string, interrupt: unknown, approvals?: unknown) {
|
|
150
|
+
// The backend describes each pending tool call in plain language; its raw
|
|
151
|
+
// descriptions are only the fallback when it could not.
|
|
152
|
+
const formatted = Array.isArray(approvals)
|
|
153
|
+
? approvals.filter((line): line is string => typeof line === 'string' && Boolean(line))
|
|
154
|
+
: [];
|
|
155
|
+
|
|
156
|
+
addToolApprovalPart(sessionId, formatted.length ? formatted : getToolApprovalMessages(interrupt));
|
|
157
|
+
}
|
|
158
|
+
|
|
154
159
|
async function submitToolApproval(sessionId: string, decision: 'approve' | 'reject') {
|
|
155
160
|
const message = (currentChat.value?.messages ?? [])
|
|
156
161
|
.findLast(candidate => candidate.role === 'assistant' && (candidate.parts ?? []).some((part: any) => {
|
|
@@ -172,6 +177,9 @@ export const useAgentStore = defineStore('agent', () => {
|
|
|
172
177
|
try {
|
|
173
178
|
await submitToolApprovalResponse(sessionId, decision);
|
|
174
179
|
|
|
180
|
+
if (currentSession.value?.sessionId === sessionId) {
|
|
181
|
+
currentSession.value.pendingApprovals = [];
|
|
182
|
+
}
|
|
175
183
|
if (approvalPart?.data) {
|
|
176
184
|
approvalPart.data.status = decision === 'approve' ? 'approved' : 'rejected';
|
|
177
185
|
}
|
|
@@ -236,6 +244,7 @@ export const useAgentStore = defineStore('agent', () => {
|
|
|
236
244
|
blockCloseOfChat,
|
|
237
245
|
adminforth,
|
|
238
246
|
setCurrentChat,
|
|
247
|
+
restorePendingToolApproval: addToolApprovalPart,
|
|
239
248
|
});
|
|
240
249
|
|
|
241
250
|
// "Turn active" = a request is in flight (submitted or streaming) or paused for HITL
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
class="flex gap-2"
|
|
20
20
|
>
|
|
21
21
|
<span class="mt-2 h-1.5 w-1.5 shrink-0 rounded-full bg-lightListTableHeadingText dark:bg-darkListTableHeadingText" />
|
|
22
|
-
<span class="min-w-0 break-words">{{ message }}</span>
|
|
22
|
+
<span class="min-w-0 whitespace-pre-line break-words">{{ message }}</span>
|
|
23
23
|
</li>
|
|
24
24
|
</ul>
|
|
25
25
|
<div class="mt-4 flex flex-wrap gap-2">
|
package/dist/custom/types.ts
CHANGED
|
@@ -47,6 +47,8 @@ export interface IAgentSession {
|
|
|
47
47
|
title: string;
|
|
48
48
|
timestamp: string;
|
|
49
49
|
messages: IMessage[];
|
|
50
|
+
/** Tool calls this session is still waiting for the user to approve, in plain language. */
|
|
51
|
+
pendingApprovals?: string[];
|
|
50
52
|
}
|
|
51
53
|
|
|
52
54
|
export interface ISessionsListItem {
|
|
@@ -16,7 +16,10 @@ export type DebugSink = {
|
|
|
16
16
|
/** A pending human-in-the-loop approval, normalized (provider-agnostic). */
|
|
17
17
|
export type PendingInterrupt = {
|
|
18
18
|
id: string;
|
|
19
|
+
/** Number of decisions the provider expects on resume (one per pending tool call). */
|
|
19
20
|
count: number;
|
|
21
|
+
/** The pending tool calls, normalized so they can be described to the user. */
|
|
22
|
+
requests: ApprovalRequest[];
|
|
20
23
|
};
|
|
21
24
|
export type BaseAgentTurnInput = {
|
|
22
25
|
prompt: string;
|
|
@@ -98,6 +101,4 @@ export type AgentStreamChunk = {
|
|
|
98
101
|
kind: "interrupt";
|
|
99
102
|
interrupt: unknown;
|
|
100
103
|
descriptors: PendingInterrupt[];
|
|
101
|
-
/** The pending tool calls, normalized so they can be described to the user. */
|
|
102
|
-
requests: ApprovalRequest[];
|
|
103
104
|
};
|
package/dist/index.js
CHANGED
|
@@ -161,6 +161,7 @@ export default class AdminForthAgentPlugin extends AdminForthPlugin {
|
|
|
161
161
|
}),
|
|
162
162
|
runAndPersistAgentResponse: this.runTurnUseCase.runAndPersistAgentResponse.bind(this.runTurnUseCase),
|
|
163
163
|
getSessionTurns: this.sessionStore.getSessionTurns.bind(this.sessionStore),
|
|
164
|
+
getPendingApprovals: (sessionId) => this.runTurnUseCase.getPendingApprovals(sessionId),
|
|
164
165
|
createNewTurn: this.sessionStore.createNewTurn.bind(this.sessionStore),
|
|
165
166
|
createSystemTurn: this.sessionStore.createSystemTurn.bind(this.sessionStore),
|
|
166
167
|
appendSteerToCurrentTurn: this.sessionStore.appendSteerToCurrentTurn.bind(this.sessionStore),
|
|
@@ -1,16 +1,10 @@
|
|
|
1
|
-
import type { ApprovalRequest } from "../tools/approvalFormatter.js";
|
|
2
1
|
import type { AgentStreamChunk, PendingInterrupt } from "../domain/turnTypes.js";
|
|
3
2
|
/**
|
|
4
|
-
* Normalize raw LangGraph interrupt object(s) into provider-agnostic descriptors
|
|
5
|
-
* Kept in the llm layer so the LangGraph
|
|
3
|
+
* Normalize raw LangGraph interrupt object(s) into provider-agnostic descriptors, each
|
|
4
|
+
* carrying the tool calls it is waiting on. Kept in the llm layer so the LangGraph
|
|
5
|
+
* interrupt shape never reaches the app.
|
|
6
6
|
*/
|
|
7
7
|
export declare function normalizeInterrupts(interrupt: unknown): PendingInterrupt[];
|
|
8
|
-
/**
|
|
9
|
-
* Pull the tool calls out of the raw interrupt so the application layer can describe
|
|
10
|
-
* them to the user. Like {@link normalizeInterrupts}, this keeps the LangGraph shape
|
|
11
|
-
* from leaking past the llm layer.
|
|
12
|
-
*/
|
|
13
|
-
export declare function extractApprovalRequests(interrupt: unknown): ApprovalRequest[];
|
|
14
8
|
type RawStreamEntry = ["messages", [any, any]] | ["updates", Record<string, any>];
|
|
15
9
|
/**
|
|
16
10
|
* Convert one raw LangGraph stream entry into zero or more normalized chunks.
|
|
@@ -25,8 +25,22 @@ function getInterruptItems(interrupt) {
|
|
|
25
25
|
return Array.isArray(interrupt) ? interrupt : [interrupt];
|
|
26
26
|
}
|
|
27
27
|
/**
|
|
28
|
-
*
|
|
29
|
-
*
|
|
28
|
+
* Pull the tool calls out of one interrupt's action requests so they can be described
|
|
29
|
+
* to the user. Drops entries the provider did not name.
|
|
30
|
+
*/
|
|
31
|
+
function toApprovalRequests(actionRequests) {
|
|
32
|
+
return actionRequests.flatMap((actionRequest) => {
|
|
33
|
+
var _a, _b;
|
|
34
|
+
const toolName = (_a = actionRequest === null || actionRequest === void 0 ? void 0 : actionRequest.name) !== null && _a !== void 0 ? _a : actionRequest === null || actionRequest === void 0 ? void 0 : actionRequest.action;
|
|
35
|
+
return typeof toolName === "string" && toolName
|
|
36
|
+
? [{ toolName, args: ((_b = actionRequest === null || actionRequest === void 0 ? void 0 : actionRequest.args) !== null && _b !== void 0 ? _b : {}) }]
|
|
37
|
+
: [];
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Normalize raw LangGraph interrupt object(s) into provider-agnostic descriptors, each
|
|
42
|
+
* carrying the tool calls it is waiting on. Kept in the llm layer so the LangGraph
|
|
43
|
+
* interrupt shape never reaches the app.
|
|
30
44
|
*/
|
|
31
45
|
export function normalizeInterrupts(interrupt) {
|
|
32
46
|
return getInterruptItems(interrupt).flatMap((item) => {
|
|
@@ -40,35 +54,14 @@ export function normalizeInterrupts(interrupt) {
|
|
|
40
54
|
? item.id
|
|
41
55
|
: undefined;
|
|
42
56
|
return typeof interruptId === "string" && Array.isArray(actionRequests)
|
|
43
|
-
? [{
|
|
57
|
+
? [{
|
|
58
|
+
id: interruptId,
|
|
59
|
+
count: actionRequests.length,
|
|
60
|
+
requests: toApprovalRequests(actionRequests),
|
|
61
|
+
}]
|
|
44
62
|
: [];
|
|
45
63
|
});
|
|
46
64
|
}
|
|
47
|
-
/**
|
|
48
|
-
* Pull the tool calls out of the raw interrupt so the application layer can describe
|
|
49
|
-
* them to the user. Like {@link normalizeInterrupts}, this keeps the LangGraph shape
|
|
50
|
-
* from leaking past the llm layer.
|
|
51
|
-
*/
|
|
52
|
-
export function extractApprovalRequests(interrupt) {
|
|
53
|
-
return getInterruptItems(interrupt).flatMap((item) => {
|
|
54
|
-
const value = item && typeof item === "object" && "value" in item
|
|
55
|
-
? item.value
|
|
56
|
-
: item;
|
|
57
|
-
const actionRequests = value && typeof value === "object"
|
|
58
|
-
? value.actionRequests
|
|
59
|
-
: undefined;
|
|
60
|
-
if (!Array.isArray(actionRequests)) {
|
|
61
|
-
return [];
|
|
62
|
-
}
|
|
63
|
-
return actionRequests.flatMap((actionRequest) => {
|
|
64
|
-
var _a, _b;
|
|
65
|
-
const toolName = (_a = actionRequest === null || actionRequest === void 0 ? void 0 : actionRequest.name) !== null && _a !== void 0 ? _a : actionRequest === null || actionRequest === void 0 ? void 0 : actionRequest.action;
|
|
66
|
-
return typeof toolName === "string" && toolName
|
|
67
|
-
? [{ toolName, args: ((_b = actionRequest === null || actionRequest === void 0 ? void 0 : actionRequest.args) !== null && _b !== void 0 ? _b : {}) }]
|
|
68
|
-
: [];
|
|
69
|
-
});
|
|
70
|
-
});
|
|
71
|
-
}
|
|
72
65
|
function extractBlocks(token) {
|
|
73
66
|
if (Array.isArray(token === null || token === void 0 ? void 0 : token.contentBlocks)) {
|
|
74
67
|
return token.contentBlocks;
|
|
@@ -91,7 +84,6 @@ export function parseRawStreamChunk(entry) {
|
|
|
91
84
|
kind: "interrupt",
|
|
92
85
|
interrupt,
|
|
93
86
|
descriptors: normalizeInterrupts(interrupt),
|
|
94
|
-
requests: extractApprovalRequests(interrupt),
|
|
95
87
|
}];
|
|
96
88
|
}
|
|
97
89
|
return [];
|
|
@@ -59,7 +59,9 @@ function formatRecord(record, resource) {
|
|
|
59
59
|
return `${(column === null || column === void 0 ? void 0 : column.label) || humanize(name)}: ${formatValue(value, column)}`;
|
|
60
60
|
});
|
|
61
61
|
const hidden = entries.length - shown.length;
|
|
62
|
-
|
|
62
|
+
const lines = hidden > 0 ? [...shown, `and ${hidden} more`] : shown;
|
|
63
|
+
// One field per line: a wide record is unreadable as a single run-on sentence.
|
|
64
|
+
return lines.join(",\n");
|
|
63
65
|
}
|
|
64
66
|
function actionLabel(resource, actionId) {
|
|
65
67
|
var _a, _b;
|
|
@@ -81,11 +83,11 @@ export function formatApprovalRequest(adminforth, request) {
|
|
|
81
83
|
switch (toolName) {
|
|
82
84
|
case "create_record": {
|
|
83
85
|
const fields = formatRecord(args.record, resource);
|
|
84
|
-
return `Create a new record in ${label}${fields ?
|
|
86
|
+
return `Create a new record in ${label}${fields ? `:\n${fields}` : ""}`;
|
|
85
87
|
}
|
|
86
88
|
case "update_record": {
|
|
87
89
|
const fields = formatRecord(args.record, resource);
|
|
88
|
-
return `Update record with id:${formatValue(args.recordId)} in ${label}${fields ?
|
|
90
|
+
return `Update record with id:${formatValue(args.recordId)} in ${label}${fields ? `, set:\n${fields}` : ""}`;
|
|
89
91
|
}
|
|
90
92
|
case "delete_record":
|
|
91
93
|
return `Delete record with id:${formatValue(args.primaryKey)} from ${label} — this cannot be undone`;
|
|
@@ -98,7 +100,7 @@ export function formatApprovalRequest(adminforth, request) {
|
|
|
98
100
|
}
|
|
99
101
|
default: {
|
|
100
102
|
const fields = formatRecord(args, resource);
|
|
101
|
-
return `${humanize(toolName)}${fields ?
|
|
103
|
+
return `${humanize(toolName)}${fields ? `:\n${fields}` : ""}`;
|
|
102
104
|
}
|
|
103
105
|
}
|
|
104
106
|
}
|
|
@@ -22,11 +22,12 @@ export type AgentEndpointsContext = {
|
|
|
22
22
|
}>;
|
|
23
23
|
runAndPersistAgentResponse(input: RunAndPersistAgentResponseInput): Promise<RunAndPersistAgentResponseResult>;
|
|
24
24
|
getSessionTurns(sessionId: string): Promise<SessionTurn[]>;
|
|
25
|
+
getPendingApprovals(sessionId: string): Promise<string[]>;
|
|
25
26
|
createNewTurn(sessionId: string, prompt: string, response?: string): Promise<string>;
|
|
26
27
|
createSystemTurn(sessionId: string, systemMessage: string): Promise<string>;
|
|
27
28
|
appendSteerToCurrentTurn(sessionId: string, steerText: string): Promise<void>;
|
|
28
29
|
handleChatSurfaceMessage(adapter: ChatSurfaceAdapter, incoming: ChatSurfaceIncomingMessage, sink: ChatSurfaceEventSink, request: IAdminForthEndpointHandlerInput): Promise<void>;
|
|
29
30
|
};
|
|
30
31
|
export type CoreEndpointsContext = Pick<AgentEndpointsContext, "options" | "handleTurn" | "handleEditTurn" | "handleSpeechTurn" | "steer">;
|
|
31
|
-
export type SessionEndpointsContext = Pick<AgentEndpointsContext, "adminforth" | "options" | "getSessionTurns" | "createNewTurn" | "createSystemTurn" | "appendSteerToCurrentTurn">;
|
|
32
|
+
export type SessionEndpointsContext = Pick<AgentEndpointsContext, "adminforth" | "options" | "getSessionTurns" | "getPendingApprovals" | "createNewTurn" | "createSystemTurn" | "appendSteerToCurrentTurn">;
|
|
32
33
|
export type ChatSurfaceEndpointsContext = Pick<AgentEndpointsContext, "adminforth" | "options" | "handleChatSurfaceMessage">;
|
|
@@ -69,12 +69,18 @@ export function setupSessionEndpoints(ctx, server) {
|
|
|
69
69
|
error: 'Unauthorized'
|
|
70
70
|
};
|
|
71
71
|
}
|
|
72
|
-
const turns = yield
|
|
72
|
+
const [turns, pendingApprovals] = yield Promise.all([
|
|
73
|
+
ctx.getSessionTurns(sessionId),
|
|
74
|
+
// The approval prompt lives only in the browser, so a reload has to rebuild it
|
|
75
|
+
// from the agent's persisted state — otherwise the user cannot answer it.
|
|
76
|
+
ctx.getPendingApprovals(sessionId),
|
|
77
|
+
]);
|
|
73
78
|
return {
|
|
74
79
|
session: {
|
|
75
80
|
sessionId,
|
|
76
81
|
title: session[ctx.options.sessionResource.titleField],
|
|
77
82
|
timestamp: session[ctx.options.sessionResource.createdAtField],
|
|
83
|
+
pendingApprovals,
|
|
78
84
|
messages: turns.flatMap(turn => {
|
|
79
85
|
const messages = [];
|
|
80
86
|
if (turn.prompt === AGENT_SYSTEM_TURN_PROMPT) {
|