@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.
@@ -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; the LangGraph
61
- * interrupt shape never leaves the llm layer. Throws on a checkpoint/runtime
62
- * failure (the caller must not treat that as "no pending interrupt").
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 resumeInterrupts;
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
- resume: shouldResume
243
- ? { decision: approvalDecision, interrupts: resumeInterrupts }
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.resume) {
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, requests) {
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.count]));
315
+ const merged = new Map(existing.map((item) => [item.id, item]));
282
316
  for (const item of descriptors) {
283
- merged.set(item.id, item.count);
317
+ merged.set(item.id, item);
284
318
  }
285
- this.pendingInterrupts.set(prepared.sessionId, [...merged.entries()].map(([id, count]) => ({ id, count })));
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: requests.map((request) => formatApprovalRequest(adminforth, request)),
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 _e = true, stream_1 = __asyncValues(stream), stream_1_1; stream_1_1 = yield stream_1.next(), _a = stream_1_1.done, !_a; _e = true) {
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
- _e = false;
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, (_d = chunk.requests) !== null && _d !== void 0 ? _d : []);
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 (!_e && !_a && (_b = stream_1.return)) yield _b.call(stream_1);
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 addToolApprovalMessage(sessionId: string, interrupt: unknown, approvals?: unknown) {
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: formatted.length ? formatted : getToolApprovalMessages(interrupt),
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">
@@ -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 interrupt shape never reaches the app.
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
- * Normalize raw LangGraph interrupt object(s) into provider-agnostic descriptors.
29
- * Kept in the llm layer so the LangGraph interrupt shape never reaches the app.
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
- ? [{ id: interruptId, count: actionRequests.length }]
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
- return hidden > 0 ? `${shown.join(", ")} and ${hidden} more` : shown.join(", ");
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 ? ` — ${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 ? ` — set ${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 ? ` — ${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 ctx.getSessionTurns(sessionId);
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) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adminforth/agent",
3
- "version": "2.8.0",
3
+ "version": "2.8.2",
4
4
  "main": "dist/index.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "type": "module",