@adminforth/agent 2.0.10 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (201) hide show
  1. package/README.md +333 -10
  2. package/dist/application/ports.d.ts +66 -0
  3. package/dist/application/runTurnUseCase.d.ts +71 -0
  4. package/dist/application/runTurnUseCase.js +364 -0
  5. package/dist/custom/ChatFooter.vue +58 -30
  6. package/dist/custom/SteerDebugPanel.vue +45 -0
  7. package/dist/custom/SteerQueue.vue +54 -0
  8. package/dist/custom/composables/agentStore/constants.ts +1 -0
  9. package/dist/custom/composables/agentStore/useAgentSessions.ts +35 -16
  10. package/dist/custom/composables/agentStore/useAgentSteerQueue.ts +114 -0
  11. package/dist/custom/composables/useAgentStore.ts +46 -1
  12. package/dist/custom/conversation_area/MessageRenderer.vue +7 -1
  13. package/dist/custom/speech_recognition_frontend/AudioLines.vue +1 -1
  14. package/dist/custom/speech_recognition_frontend/MicrophoneButon.vue +3 -3
  15. package/dist/{agentEvents.d.ts → domain/agentEvents.d.ts} +5 -1
  16. package/dist/domain/steerBuffer.d.ts +33 -0
  17. package/dist/domain/steerBuffer.js +51 -0
  18. package/dist/{agent → domain}/systemPrompt.js +1 -1
  19. package/dist/{agent/turn → domain}/turnTypes.d.ts +41 -37
  20. package/dist/{agent/turn/VegaLiteStreamBuffer.d.ts → domain/vegaLiteStreamBuffer.d.ts} +1 -1
  21. package/dist/index.d.ts +4 -2
  22. package/dist/index.js +49 -35
  23. package/dist/{agent/runtime/AgentContext.d.ts → llm/agentContext.d.ts} +3 -3
  24. package/dist/{agent/simpleAgent.d.ts → llm/agentModels.d.ts} +18 -16
  25. package/dist/llm/agentModels.js +65 -0
  26. package/dist/{agent/runtime/AgentRuntime.d.ts → llm/agentRuntime.d.ts} +14 -2
  27. package/dist/{agent/runtime/AgentRuntime.js → llm/agentRuntime.js} +41 -6
  28. package/dist/llm/langGraphLlm.d.ts +25 -0
  29. package/dist/llm/langGraphLlm.js +54 -0
  30. package/dist/{agent/middleware/apiBasedTools.d.ts → llm/middleware/apiToolsMiddleware.d.ts} +1 -1
  31. package/dist/{agent/middleware/apiBasedTools.js → llm/middleware/apiToolsMiddleware.js} +8 -14
  32. package/dist/{agent → llm}/middleware/sequenceDebug.d.ts +1 -1
  33. package/dist/llm/middleware/steerMiddleware.d.ts +21 -0
  34. package/dist/llm/middleware/steerMiddleware.js +50 -0
  35. package/dist/{agent/models/AgentModelFactory.d.ts → llm/modelFactory.d.ts} +1 -1
  36. package/dist/{agent/models/AgentModelFactory.js → llm/modelFactory.js} +1 -1
  37. package/dist/llm/streamAdapter.d.ts +18 -0
  38. package/dist/llm/streamAdapter.js +117 -0
  39. package/dist/{sessionStore.d.ts → persistence/sessionStore.d.ts} +19 -2
  40. package/dist/{sessionStore.js → persistence/sessionStore.js} +53 -0
  41. package/dist/{agent/tools/AgentToolProvider.d.ts → tools/agentToolProvider.d.ts} +1 -1
  42. package/dist/{agent/tools/AgentToolProvider.js → tools/agentToolProvider.js} +1 -1
  43. package/dist/{apiBasedTools.js → tools/apiBasedTools.js} +0 -3
  44. package/dist/{agent/tools → tools}/apiTool.d.ts +1 -1
  45. package/dist/{agent/tools → tools}/fetchSkill.js +1 -1
  46. package/dist/{agent/tools → tools}/fetchToolSchema.d.ts +1 -1
  47. package/dist/{agent/tools → tools}/index.d.ts +1 -1
  48. package/dist/{agent/tools → tools}/navigateUser.d.ts +3 -3
  49. package/dist/{endpoints → transport/http}/context.d.ts +12 -4
  50. package/dist/transport/http/context.js +1 -0
  51. package/dist/{endpoints/core.js → transport/http/coreEndpoints.js} +23 -3
  52. package/dist/{endpoints/sessions.js → transport/http/sessionEndpoints.js} +36 -1
  53. package/dist/transport/sse/sseWriter.d.ts +29 -0
  54. package/dist/{surfaces/web-sse/createSseEventEmitter.js → transport/sse/sseWriter.js} +27 -0
  55. package/dist/{chatSurfaceService.d.ts → transport/surfaces/chatSurfaceService.d.ts} +3 -3
  56. package/dist/{chatSurfaceService.js → transport/surfaces/chatSurfaceService.js} +7 -5
  57. package/dist/{agent/speech/SpeechTurnService.d.ts → transport/surfaces/speechTurnService.d.ts} +1 -1
  58. package/dist/{agent/speech/SpeechTurnService.js → transport/surfaces/speechTurnService.js} +2 -2
  59. package/dist/types.d.ts +10 -4
  60. package/package.json +11 -2
  61. package/.woodpecker/buildRelease.sh +0 -13
  62. package/.woodpecker/buildSlackNotify.sh +0 -46
  63. package/.woodpecker/release.yml +0 -57
  64. package/agent/checkpointer.ts +0 -329
  65. package/agent/languageDetect.ts +0 -92
  66. package/agent/middleware/apiBasedTools.ts +0 -174
  67. package/agent/middleware/openAiResponsesContinuation.ts +0 -92
  68. package/agent/middleware/sequenceDebug.ts +0 -429
  69. package/agent/models/AgentModeResolver.ts +0 -9
  70. package/agent/models/AgentModelFactory.ts +0 -28
  71. package/agent/runtime/AgentContext.ts +0 -30
  72. package/agent/runtime/AgentRuntime.ts +0 -89
  73. package/agent/simpleAgent.ts +0 -199
  74. package/agent/skills/registry.ts +0 -162
  75. package/agent/speech/SpeechTurnService.ts +0 -179
  76. package/agent/systemPrompt.ts +0 -133
  77. package/agent/toolCallEvents.ts +0 -135
  78. package/agent/tools/AgentToolProvider.ts +0 -28
  79. package/agent/tools/apiTool.ts +0 -66
  80. package/agent/tools/fetchSkill.ts +0 -68
  81. package/agent/tools/fetchToolSchema.ts +0 -47
  82. package/agent/tools/getUserLocation.ts +0 -45
  83. package/agent/tools/index.ts +0 -31
  84. package/agent/tools/navigateUser.ts +0 -210
  85. package/agent/turn/TurnContextBuilder.ts +0 -36
  86. package/agent/turn/TurnLifecycleService.ts +0 -47
  87. package/agent/turn/TurnPersistenceService.ts +0 -33
  88. package/agent/turn/TurnPromptBuilder.ts +0 -51
  89. package/agent/turn/TurnStreamConsumer.ts +0 -69
  90. package/agent/turn/VegaLiteStreamBuffer.ts +0 -90
  91. package/agent/turn/turnTypes.ts +0 -100
  92. package/agentEvents.ts +0 -75
  93. package/agentTurnService.ts +0 -314
  94. package/apiBasedTools.ts +0 -712
  95. package/build.log +0 -67
  96. package/chatSurfaceService.ts +0 -319
  97. package/custom/ChatFooter.vue +0 -173
  98. package/custom/ChatHeader.vue +0 -78
  99. package/custom/ChatSurface.vue +0 -160
  100. package/custom/CustomAutoScrollContainer.vue +0 -164
  101. package/custom/SessionsHistory.vue +0 -88
  102. package/custom/chat.ts +0 -82
  103. package/custom/composables/agentAudio/utils.ts +0 -220
  104. package/custom/composables/agentStore/constants.ts +0 -19
  105. package/custom/composables/agentStore/pageContext.ts +0 -8
  106. package/custom/composables/agentStore/useAgentChat.ts +0 -241
  107. package/custom/composables/agentStore/useAgentPlaceholder.ts +0 -142
  108. package/custom/composables/agentStore/useAgentSessions.ts +0 -350
  109. package/custom/composables/useAgentAudio.ts +0 -272
  110. package/custom/composables/useAgentStore.ts +0 -510
  111. package/custom/composables/useAgentTransitions.ts +0 -58
  112. package/custom/conversation_area/ConversationArea.vue +0 -301
  113. package/custom/conversation_area/MessageRenderer.vue +0 -49
  114. package/custom/conversation_area/ProcessingTimeline.vue +0 -286
  115. package/custom/conversation_area/ReasoningRenderer.vue +0 -79
  116. package/custom/conversation_area/SystemMessageRenderer.vue +0 -28
  117. package/custom/conversation_area/TextRenderer.vue +0 -182
  118. package/custom/conversation_area/ThreeDotsAnimation.vue +0 -35
  119. package/custom/conversation_area/ToolApprovalRenderer.vue +0 -98
  120. package/custom/conversation_area/ToolRenderer.vue +0 -178
  121. package/custom/conversation_area/ToolsGroup.vue +0 -59
  122. package/custom/env.d.ts +0 -7
  123. package/custom/incremark_code_renderers/IncremarkShikiCodeBlock.vue +0 -393
  124. package/custom/incremark_code_renderers/incremarkCodeHighlight.ts +0 -285
  125. package/custom/incremark_code_renderers/incremarkRenderer.ts +0 -653
  126. package/custom/incremark_code_renderers/renderIncremarkMarkdown.ts +0 -118
  127. package/custom/package.json +0 -30
  128. package/custom/pnpm-lock.yaml +0 -2394
  129. package/custom/public/agentAudio/agent-processing.mp3 +0 -0
  130. package/custom/skills/analyze_data/SKILL.md +0 -25
  131. package/custom/skills/charts/SKILL.md +0 -193
  132. package/custom/skills/fetch_data/SKILL.md +0 -31
  133. package/custom/skills/mutate_data/SKILL.md +0 -121
  134. package/custom/speech_recognition_frontend/AudioLines.vue +0 -43
  135. package/custom/speech_recognition_frontend/MicrophoneButon.vue +0 -185
  136. package/custom/speech_recognition_frontend/types/voice-activity-detection.d.ts +0 -22
  137. package/custom/speech_recognition_frontend/voiceActivityDetection.ts +0 -62
  138. package/custom/tsconfig.json +0 -15
  139. package/custom/types.ts +0 -108
  140. package/custom/utils.ts +0 -38
  141. package/dist/agent/middleware/openAiResponsesContinuation.d.ts +0 -1
  142. package/dist/agent/middleware/openAiResponsesContinuation.js +0 -66
  143. package/dist/agent/models/AgentModeResolver.d.ts +0 -9
  144. package/dist/agent/models/AgentModeResolver.js +0 -9
  145. package/dist/agent/simpleAgent.js +0 -118
  146. package/dist/agent/turn/TurnContextBuilder.d.ts +0 -14
  147. package/dist/agent/turn/TurnContextBuilder.js +0 -31
  148. package/dist/agent/turn/TurnLifecycleService.d.ts +0 -24
  149. package/dist/agent/turn/TurnLifecycleService.js +0 -47
  150. package/dist/agent/turn/TurnPersistenceService.d.ts +0 -13
  151. package/dist/agent/turn/TurnPersistenceService.js +0 -35
  152. package/dist/agent/turn/TurnPromptBuilder.d.ts +0 -19
  153. package/dist/agent/turn/TurnPromptBuilder.js +0 -43
  154. package/dist/agent/turn/TurnStreamConsumer.d.ts +0 -11
  155. package/dist/agent/turn/TurnStreamConsumer.js +0 -84
  156. package/dist/agentTurnService.d.ts +0 -24
  157. package/dist/agentTurnService.js +0 -269
  158. package/dist/surfaces/web-sse/createSseEventEmitter.d.ts +0 -14
  159. package/endpoints/chatSurfaces.ts +0 -66
  160. package/endpoints/context.ts +0 -52
  161. package/endpoints/core.ts +0 -181
  162. package/endpoints/sessions.ts +0 -183
  163. package/errors.ts +0 -10
  164. package/index.ts +0 -192
  165. package/pnpm-workspace.yaml +0 -4
  166. package/sanitizeSpeechText.ts +0 -32
  167. package/sessionStore.ts +0 -105
  168. package/surfaces/web-sse/createSseEventEmitter.ts +0 -311
  169. package/tsconfig.json +0 -14
  170. package/types.ts +0 -127
  171. /package/dist/{agent/turn/turnTypes.js → application/ports.js} +0 -0
  172. /package/dist/{agentEvents.js → domain/agentEvents.js} +0 -0
  173. /package/dist/{agent → domain}/languageDetect.d.ts +0 -0
  174. /package/dist/{agent → domain}/languageDetect.js +0 -0
  175. /package/dist/{agent → domain}/systemPrompt.d.ts +0 -0
  176. /package/dist/{agent → domain}/toolCallEvents.d.ts +0 -0
  177. /package/dist/{agent → domain}/toolCallEvents.js +0 -0
  178. /package/dist/{endpoints/context.js → domain/turnTypes.js} +0 -0
  179. /package/dist/{agent/turn/VegaLiteStreamBuffer.js → domain/vegaLiteStreamBuffer.js} +0 -0
  180. /package/dist/{agent/runtime/AgentContext.js → llm/agentContext.js} +0 -0
  181. /package/dist/{agent → llm}/middleware/sequenceDebug.js +0 -0
  182. /package/dist/{agent/checkpointer.d.ts → persistence/checkpointStore.d.ts} +0 -0
  183. /package/dist/{agent/checkpointer.js → persistence/checkpointStore.js} +0 -0
  184. /package/dist/{errors.d.ts → shared/errors.d.ts} +0 -0
  185. /package/dist/{errors.js → shared/errors.js} +0 -0
  186. /package/dist/{sanitizeSpeechText.d.ts → shared/sanitizeSpeechText.d.ts} +0 -0
  187. /package/dist/{sanitizeSpeechText.js → shared/sanitizeSpeechText.js} +0 -0
  188. /package/dist/{apiBasedTools.d.ts → tools/apiBasedTools.d.ts} +0 -0
  189. /package/dist/{agent/tools → tools}/apiTool.js +0 -0
  190. /package/dist/{agent/tools → tools}/fetchSkill.d.ts +0 -0
  191. /package/dist/{agent/tools → tools}/fetchToolSchema.js +0 -0
  192. /package/dist/{agent/tools → tools}/getUserLocation.d.ts +0 -0
  193. /package/dist/{agent/tools → tools}/getUserLocation.js +0 -0
  194. /package/dist/{agent/tools → tools}/index.js +0 -0
  195. /package/dist/{agent/tools → tools}/navigateUser.js +0 -0
  196. /package/dist/{agent → tools}/skills/registry.d.ts +0 -0
  197. /package/dist/{agent → tools}/skills/registry.js +0 -0
  198. /package/dist/{endpoints/chatSurfaces.d.ts → transport/http/chatSurfaceEndpoints.d.ts} +0 -0
  199. /package/dist/{endpoints/chatSurfaces.js → transport/http/chatSurfaceEndpoints.js} +0 -0
  200. /package/dist/{endpoints/core.d.ts → transport/http/coreEndpoints.d.ts} +0 -0
  201. /package/dist/{endpoints/sessions.d.ts → transport/http/sessionEndpoints.d.ts} +0 -0
@@ -0,0 +1,364 @@
1
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
2
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
3
+ return new (P || (P = Promise))(function (resolve, reject) {
4
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
5
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
6
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
7
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
8
+ });
9
+ };
10
+ var __asyncValues = (this && this.__asyncValues) || function (o) {
11
+ if (!Symbol.asyncIterator) throw new TypeError("Symbol.asyncIterator is not defined.");
12
+ var m = o[Symbol.asyncIterator], i;
13
+ return m ? m.call(o) : (o = typeof __values === "function" ? __values(o) : o[Symbol.iterator](), i = {}, verb("next"), verb("throw"), verb("return"), i[Symbol.asyncIterator] = function () { return this; }, i);
14
+ function verb(n) { i[n] = o[n] && function (v) { return new Promise(function (resolve, reject) { v = o[n](v), settle(resolve, reject, v.done, v.value); }); }; }
15
+ function settle(resolve, reject, d, v) { Promise.resolve(v).then(function(v) { resolve({ value: v, done: d }); }, reject); }
16
+ };
17
+ import { logger } from "adminforth";
18
+ import { randomUUID } from "crypto";
19
+ import { VegaLiteStreamBuffer } from "../domain/vegaLiteStreamBuffer.js";
20
+ import { buildAgentTurnSystemPrompt } from "../domain/systemPrompt.js";
21
+ import { getErrorMessage, isAbortError } from "../shared/errors.js";
22
+ function getApprovalDecision(input) {
23
+ return "approvalDecision" in input
24
+ && (input.approvalDecision === "approve" || input.approvalDecision === "reject")
25
+ ? input.approvalDecision
26
+ : undefined;
27
+ }
28
+ function buildHitlDecision(decision, prompt) {
29
+ if (decision === "approve") {
30
+ return { type: "approve" };
31
+ }
32
+ return {
33
+ type: "reject",
34
+ message: prompt
35
+ ? `User rejected the pending tool execution and sent a new instruction instead: ${prompt}`
36
+ : "User rejected executing this tool",
37
+ };
38
+ }
39
+ function buildHitlResumeValue(input) {
40
+ return {
41
+ decisions: Array.from({ length: input.count }, () => (buildHitlDecision(input.decision, input.prompt))),
42
+ };
43
+ }
44
+ /**
45
+ * Build the provider-agnostic resume payload for a human-in-the-loop turn.
46
+ * Exported for unit testing; the LLM port wraps it into a LangGraph Command.
47
+ */
48
+ export function buildResumeValue(input) {
49
+ var _a;
50
+ const interrupts = (_a = input.interrupts) !== null && _a !== void 0 ? _a : [];
51
+ if (interrupts.length === 0) {
52
+ throw new Error("No pending approval interrupt found for resume.");
53
+ }
54
+ if (interrupts.length === 1) {
55
+ return buildHitlResumeValue({
56
+ decision: input.decision,
57
+ count: interrupts[0].count,
58
+ prompt: input.prompt,
59
+ });
60
+ }
61
+ return Object.fromEntries(interrupts.map((interrupt) => [
62
+ interrupt.id,
63
+ buildHitlResumeValue({
64
+ decision: input.decision,
65
+ count: interrupt.count,
66
+ prompt: input.prompt,
67
+ }),
68
+ ]));
69
+ }
70
+ /**
71
+ * Single entry point for running an agent turn: prepare (start/resume) → build
72
+ * the prompt → stream from the LLM port → consume the typed stream into SSE
73
+ * events → persist the result. Replaces the former AgentTurnService plus the
74
+ * TurnLifecycle/Context/Prompt/Persistence/StreamConsumer + ModeResolver stack.
75
+ */
76
+ export class RunTurnUseCase {
77
+ constructor(deps) {
78
+ this.deps = deps;
79
+ this.pendingInterrupts = new Map();
80
+ }
81
+ resolveMode(modeName) {
82
+ var _a;
83
+ return (_a = this.deps.modes.find((mode) => mode.name === modeName)) !== null && _a !== void 0 ? _a : this.deps.modes[0];
84
+ }
85
+ buildContext(input, turnId) {
86
+ var _a, _b;
87
+ return {
88
+ adminUser: input.adminUser,
89
+ sessionId: input.sessionId,
90
+ turnId,
91
+ abortSignal: input.abortSignal,
92
+ currentPage: input.currentPage,
93
+ chatSurface: input.chatSurface,
94
+ userTimeZone: (_a = input.userTimeZone) !== null && _a !== void 0 ? _a : "UTC",
95
+ adminPublicOrigin: (_b = input.adminPublicOrigin) !== null && _b !== void 0 ? _b : this.deps.getAdminforth().config.baseUrlSlashed,
96
+ };
97
+ }
98
+ /**
99
+ * Resolve the pending HITL interrupts for a resume. With a persistent checkpointer the
100
+ * checkpoint is authoritative (correct across instances and restarts); with the in-memory
101
+ * MemorySaver the in-process cache is authoritative (single process, cannot be stale).
102
+ */
103
+ resolvePendingInterrupts(sessionId, mode) {
104
+ return __awaiter(this, void 0, void 0, function* () {
105
+ var _a;
106
+ if (this.deps.hasPersistentCheckpointer) {
107
+ // The in-process cache can be stale after a cross-instance resume, so it is intentionally
108
+ // NOT consulted here. A failure to read the checkpoint is surfaced as a real error — never
109
+ // masked as "no pending approval".
110
+ try {
111
+ return yield this.deps.llm.getPendingInterrupts({
112
+ completionAdapter: mode.completionAdapter,
113
+ sessionId,
114
+ });
115
+ }
116
+ catch (error) {
117
+ logger.error(`Failed to read pending approval state from checkpoint for session "${sessionId}": ${getErrorMessage(error)}`);
118
+ throw error;
119
+ }
120
+ }
121
+ return (_a = this.pendingInterrupts.get(sessionId)) !== null && _a !== void 0 ? _a : [];
122
+ });
123
+ }
124
+ prepareTurn(input) {
125
+ return __awaiter(this, void 0, void 0, function* () {
126
+ const sequenceDebugSink = this.deps.createDebugSink();
127
+ const mode = this.resolveMode(input.modeName);
128
+ const approvalDecision = getApprovalDecision(input);
129
+ const shouldResume = Boolean(approvalDecision);
130
+ let turnId;
131
+ let previousUserMessages = [];
132
+ let initialResponse;
133
+ let resumeInterrupts;
134
+ if (shouldResume) {
135
+ resumeInterrupts = yield this.resolvePendingInterrupts(input.sessionId, mode);
136
+ if (resumeInterrupts.length === 0) {
137
+ throw new Error(`No pending approval interrupt found for session "${input.sessionId}".`);
138
+ }
139
+ const resumeState = yield this.deps.sessions.getResumeState(input.sessionId);
140
+ turnId = resumeState.turnId;
141
+ initialResponse = resumeState.initialResponse;
142
+ }
143
+ else {
144
+ previousUserMessages = yield this.deps.sessions.getPreviousUserMessages(input.sessionId);
145
+ turnId = yield this.deps.sessions.createNewTurn(input.sessionId, input.prompt);
146
+ yield this.deps.sessions.touchSession(input.sessionId);
147
+ }
148
+ return {
149
+ prompt: input.prompt,
150
+ sessionId: input.sessionId,
151
+ turnId,
152
+ previousUserMessages,
153
+ mode,
154
+ context: this.buildContext(input, turnId),
155
+ observability: {
156
+ emit: input.emit,
157
+ sequenceDebugSink,
158
+ },
159
+ resume: shouldResume
160
+ ? { decision: approvalDecision, interrupts: resumeInterrupts }
161
+ : undefined,
162
+ initialResponse,
163
+ };
164
+ });
165
+ }
166
+ buildMessages(prepared) {
167
+ return __awaiter(this, void 0, void 0, function* () {
168
+ const userLanguage = yield this.deps.llm
169
+ .detectLanguage({
170
+ completionAdapter: prepared.mode.completionAdapter,
171
+ prompt: prepared.prompt,
172
+ previousUserMessages: prepared.previousUserMessages,
173
+ })
174
+ .catch((error) => {
175
+ var _a;
176
+ if (((_a = prepared.context.abortSignal) === null || _a === void 0 ? void 0 : _a.aborted) || isAbortError(error)) {
177
+ throw error;
178
+ }
179
+ logger.warn(`Failed to detect user language: ${getErrorMessage(error)}`);
180
+ return null;
181
+ });
182
+ const adminforth = this.deps.getAdminforth();
183
+ const systemPrompt = buildAgentTurnSystemPrompt({
184
+ agentSystemPrompt: yield this.deps.getAgentSystemPrompt(),
185
+ adminUser: prepared.context.adminUser,
186
+ usernameField: adminforth.config.auth.usernameField,
187
+ userLanguage,
188
+ chatSurface: prepared.context.chatSurface,
189
+ });
190
+ return [
191
+ { role: "system", content: systemPrompt },
192
+ { role: "user", content: prepared.prompt },
193
+ ];
194
+ });
195
+ }
196
+ handleInterrupt(prepared, interrupt, descriptors) {
197
+ return __awaiter(this, void 0, void 0, function* () {
198
+ var _a, _b, _c;
199
+ if (!this.deps.hasPersistentCheckpointer) {
200
+ const existing = (_a = this.pendingInterrupts.get(prepared.sessionId)) !== null && _a !== void 0 ? _a : [];
201
+ const merged = new Map(existing.map((item) => [item.id, item.count]));
202
+ for (const item of descriptors) {
203
+ merged.set(item.id, item.count);
204
+ }
205
+ this.pendingInterrupts.set(prepared.sessionId, [...merged.entries()].map(([id, count]) => ({ id, count })));
206
+ }
207
+ yield ((_c = (_b = prepared.observability).emit) === null || _c === void 0 ? void 0 : _c.call(_b, {
208
+ type: "interrupt",
209
+ sessionId: prepared.sessionId,
210
+ interrupt,
211
+ }));
212
+ });
213
+ }
214
+ runAgentTurn(prepared) {
215
+ return __awaiter(this, void 0, void 0, function* () {
216
+ var _a, e_1, _b, _c;
217
+ const streamInput = prepared.resume
218
+ ? {
219
+ resume: buildResumeValue({
220
+ decision: prepared.resume.decision,
221
+ interrupts: prepared.resume.interrupts,
222
+ prompt: prepared.prompt,
223
+ }),
224
+ }
225
+ : { messages: yield this.buildMessages(prepared) };
226
+ const stream = yield this.deps.llm.streamTurn({
227
+ completionAdapter: prepared.mode.completionAdapter,
228
+ input: streamInput,
229
+ context: prepared.context,
230
+ observability: prepared.observability,
231
+ });
232
+ const { emit } = prepared.observability;
233
+ const abortSignal = prepared.context.abortSignal;
234
+ const textBuffer = new VegaLiteStreamBuffer();
235
+ let fullResponse = "";
236
+ let interrupted = false;
237
+ try {
238
+ try {
239
+ 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) {
240
+ _c = stream_1_1.value;
241
+ _d = false;
242
+ const chunk = _c;
243
+ if (abortSignal === null || abortSignal === void 0 ? void 0 : abortSignal.aborted) {
244
+ throw new DOMException("This operation was aborted", "AbortError");
245
+ }
246
+ if (chunk.kind === "interrupt") {
247
+ interrupted = true;
248
+ yield this.handleInterrupt(prepared, chunk.interrupt, chunk.descriptors);
249
+ continue;
250
+ }
251
+ if (chunk.kind === "reasoning") {
252
+ if (chunk.delta) {
253
+ yield (emit === null || emit === void 0 ? void 0 : emit({ type: "reasoning-delta", delta: chunk.delta }));
254
+ }
255
+ continue;
256
+ }
257
+ if (chunk.delta) {
258
+ fullResponse += chunk.delta;
259
+ yield textBuffer.push(chunk.delta, emit);
260
+ }
261
+ }
262
+ }
263
+ catch (e_1_1) { e_1 = { error: e_1_1 }; }
264
+ finally {
265
+ try {
266
+ if (!_d && !_a && (_b = stream_1.return)) yield _b.call(stream_1);
267
+ }
268
+ finally { if (e_1) throw e_1.error; }
269
+ }
270
+ yield textBuffer.flush(emit);
271
+ return { text: fullResponse };
272
+ }
273
+ finally {
274
+ // When the turn is interrupted for HITL approval it will resume, so any steers
275
+ // buffered meanwhile must survive to be applied on resume. Otherwise the turn is
276
+ // done and leftover (unconsumed) steers must not leak into the next turn.
277
+ if (!interrupted) {
278
+ this.deps.steerBuffer.clear(prepared.sessionId);
279
+ }
280
+ if (!this.deps.hasPersistentCheckpointer && !interrupted) {
281
+ this.pendingInterrupts.delete(prepared.sessionId);
282
+ }
283
+ }
284
+ });
285
+ }
286
+ runAndPersistAgentResponse(input) {
287
+ return __awaiter(this, void 0, void 0, function* () {
288
+ var _a, _b;
289
+ const prepared = yield this.prepareTurn(input);
290
+ let fullResponse = (_a = prepared.initialResponse) !== null && _a !== void 0 ? _a : "";
291
+ let aborted = false;
292
+ let failed = false;
293
+ try {
294
+ const agentResponse = yield this.runAgentTurn(prepared);
295
+ fullResponse += agentResponse.text;
296
+ }
297
+ catch (error) {
298
+ if (((_b = input.abortSignal) === null || _b === void 0 ? void 0 : _b.aborted) || isAbortError(error)) {
299
+ aborted = true;
300
+ logger.info(input.abortLogMessage);
301
+ }
302
+ else {
303
+ failed = true;
304
+ fullResponse = getErrorMessage(error);
305
+ logger.error(`${input.failureLogMessage}:\n${fullResponse}`);
306
+ }
307
+ }
308
+ prepared.observability.sequenceDebugSink.flush();
309
+ yield this.deps.sessions.saveTurnResponse({
310
+ turnId: prepared.turnId,
311
+ responseText: fullResponse,
312
+ debugHistory: prepared.observability.sequenceDebugSink.getHistory(),
313
+ });
314
+ return {
315
+ text: fullResponse,
316
+ turnId: prepared.turnId,
317
+ aborted,
318
+ failed,
319
+ };
320
+ });
321
+ }
322
+ handleTurn(input) {
323
+ return __awaiter(this, void 0, void 0, function* () {
324
+ var _a, _b;
325
+ yield input.emit({
326
+ type: "turn-started",
327
+ messageId: randomUUID(),
328
+ });
329
+ const agentResponse = yield this.runAndPersistAgentResponse({
330
+ prompt: input.prompt,
331
+ sessionId: input.sessionId,
332
+ modeName: input.modeName,
333
+ userTimeZone: input.userTimeZone,
334
+ currentPage: input.currentPage,
335
+ chatSurface: input.chatSurface,
336
+ adminPublicOrigin: input.adminPublicOrigin,
337
+ approvalDecision: input.approvalDecision,
338
+ abortSignal: input.abortSignal,
339
+ adminUser: input.adminUser,
340
+ emit: input.emit,
341
+ failureLogMessage: (_a = input.failureLogMessage) !== null && _a !== void 0 ? _a : "Agent response failed",
342
+ abortLogMessage: (_b = input.abortLogMessage) !== null && _b !== void 0 ? _b : "Agent response aborted",
343
+ });
344
+ if (agentResponse.failed) {
345
+ yield input.emit({
346
+ type: "error",
347
+ error: agentResponse.text,
348
+ });
349
+ }
350
+ else if (!agentResponse.aborted) {
351
+ yield input.emit({
352
+ type: "response",
353
+ text: agentResponse.text,
354
+ sessionId: input.sessionId,
355
+ turnId: agentResponse.turnId,
356
+ });
357
+ }
358
+ yield input.emit({
359
+ type: "finish",
360
+ });
361
+ return agentResponse;
362
+ });
363
+ }
364
+ }
@@ -7,27 +7,30 @@
7
7
  transition: `transform ${agentTransitions.TRANSITION_DURATION}ms ease-in-out`
8
8
  }"
9
9
  >
10
- <div
11
- class="w-full border rounded-lg pb-8"
12
- :class="agentStore.isAudioChatMode ? 'border-none mt-8' : 'border dark:bg-gray-700'"
13
- >
10
+ <SteerQueue />
11
+ <!-- <SteerDebugPanel /> -->
12
+ <div
13
+ class="w-full border rounded-lg flex flex-col"
14
+ :class="agentStore.isAudioChatMode ? 'border-none mt-8' : 'border dark:bg-gray-700'"
15
+ >
14
16
  <textarea
15
17
  v-if="!agentStore.isAudioChatMode"
16
18
  v-model="agentStore.userMessageInput"
17
19
  ref="textInput"
20
+ rows="1"
18
21
  @input="autoResize"
19
22
  :class="[
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',
23
+ 'h-8 px-4 py-3 border-b rounded-xl rounded-b-none 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
24
  { '!text-base': coreStore.isIos }
22
25
  ]"
23
- :placeholder="agentStore.hasPendingToolApproval ? 'Approve or reject the pending action to continue' : agentStore.userMessagePlaceholder"
24
- :disabled="agentStore.isMessageInputBlocked"
26
+ :placeholder="placeholderText"
25
27
  @keydown.enter.exact.prevent="sendMessage"
26
28
  />
29
+ <div class="flex items-center justify-between px-2 py-1">
27
30
  <div
28
31
  v-if="agentStore.availableModes.length > 1"
29
32
  ref="modeMenu"
30
- class="absolute bottom-2 left-4"
33
+ class="relative"
31
34
  >
32
35
  <button
33
36
  aria-label="Select mode"
@@ -46,22 +49,30 @@
46
49
  :class="isModeMenuOpen ? '!rotate-180' : '!rotate-0'"
47
50
  />
48
51
  </button>
49
-
50
- <div
51
- v-if="isModeMenuOpen"
52
- class="absolute bottom-full left-0 mb-2 min-w-40 overflow-hidden rounded-md border border-gray-200 bg-white shadow-lg dark:border-gray-600 dark:bg-gray-800"
53
- >
54
- <button
55
- v-for="mode in agentStore.availableModes"
56
- :key="mode.name"
57
- class="block w-full px-3 py-2 text-left text-sm text-lightInputText transition-colors duration-150 hover:bg-gray-100 dark:text-darkInputText dark:hover:bg-gray-700"
58
- :class="mode.name === agentStore.activeModeName ? 'bg-gray-100 dark:bg-gray-700' : ''"
59
- type="button"
60
- @click="selectMode(mode.name)"
52
+ <Transition
53
+ enter-active-class="transition ease-out duration-200"
54
+ enter-from-class="opacity-0 scale-95"
55
+ enter-to-class="opacity-100 scale-100"
56
+ leave-active-class="transition ease-in duration-150"
57
+ leave-from-class="opacity-100 scale-100"
58
+ leave-to-class="opacity-0 scale-95"
59
+ >
60
+ <div
61
+ v-if="isModeMenuOpen"
62
+ class="absolute bottom-full left-0 mb-2 min-w-40 overflow-hidden rounded-md border border-gray-200 bg-white shadow-lg dark:border-gray-600 dark:bg-gray-800"
61
63
  >
62
- {{ mode.name }}
63
- </button>
64
- </div>
64
+ <button
65
+ v-for="mode in agentStore.availableModes"
66
+ :key="mode.name"
67
+ class="block w-full px-3 py-2 text-left text-sm text-lightInputText transition-colors duration-150 hover:bg-gray-100 dark:text-darkInputText dark:hover:bg-gray-700"
68
+ :class="mode.name === agentStore.activeModeName ? 'bg-gray-100 dark:bg-gray-700' : ''"
69
+ type="button"
70
+ @click="selectMode(mode.name)"
71
+ >
72
+ {{ mode.name }}
73
+ </button>
74
+ </div>
75
+ </Transition>
65
76
  </div>
66
77
  <MicrophoneButton
67
78
  v-if="props.meta.hasAudioAdapter"
@@ -69,32 +80,33 @@
69
80
  <template v-if="!agentStore.isAudioChatMode">
70
81
  <Button
71
82
  v-if="!agentStore.isResponseInProgress"
72
- class="absolute right-4 bottom-2 !p-0 h-9 w-9 transition-opacity duration-200"
83
+ class=" !p-0 h-7 w-7 transition-opacity duration-200"
73
84
  @click="sendMessage"
74
- :disabled="!agentStore.trimmedUserMessage || agentStore.isMessageInputBlocked"
85
+ :disabled="!agentStore.trimmedUserMessage"
75
86
  >
76
87
  <IconArrowUpOutline
77
- class="w-8 h-8 p-1
88
+ class="w-6 h-6
78
89
  text-white"
79
90
  />
80
91
  </Button>
81
92
  <Button
82
93
  v-else
83
- class="absolute right-4 bottom-2 !p-0 h-9 w-9"
94
+ class="right-4 bottom-2 !p-0 h-7 w-7"
84
95
  @click="stopCurrentRequest"
85
96
  >
86
97
  <div
87
- class="w-3 h-3 bg-white rounded-sm"
98
+ class="w-2.5 h-2.5 bg-white rounded-sm"
88
99
  />
89
100
  </Button>
90
101
  </template>
102
+ </div>
91
103
  </div>
92
104
  </div>
93
105
  </template>
94
106
 
95
107
  <script setup lang="ts">
96
108
  import { IconArrowUpOutline, IconAngleDownOutline } from '@iconify-prerendered/vue-flowbite';
97
- import { useTemplateRef, onMounted, ref, onUnmounted } from 'vue';
109
+ import { useTemplateRef, onMounted, ref, onUnmounted, watch, computed } from 'vue';
98
110
  import { onClickOutside } from '@vueuse/core'
99
111
  import { useAgentStore } from './composables/useAgentStore';
100
112
  import { useAgentTransitions } from './composables/useAgentTransitions';
@@ -102,6 +114,8 @@ import { Button } from '@/afcl';
102
114
  import { useCoreStore } from '@/stores/core';
103
115
  import { remToPx } from './utils';
104
116
  import MicrophoneButton from './speech_recognition_frontend/MicrophoneButon.vue';
117
+ import SteerQueue from './SteerQueue.vue';
118
+ import SteerDebugPanel from './SteerDebugPanel.vue';
105
119
 
106
120
  const props = defineProps<{
107
121
  meta: {
@@ -124,6 +138,13 @@ const agentTransitions = useAgentTransitions();
124
138
  const coreStore = useCoreStore();
125
139
  const isModeMenuOpen = ref(false);
126
140
 
141
+ const placeholderText = computed(() => {
142
+ if (agentStore.isTurnActive) {
143
+ return 'Queue a follow-up — send after, or steer it…';
144
+ }
145
+ return agentStore.userMessagePlaceholder;
146
+ });
147
+
127
148
  onClickOutside(modeMenu as any, () => { isModeMenuOpen.value = false; });
128
149
 
129
150
  onMounted(async () => {
@@ -131,6 +152,11 @@ onMounted(async () => {
131
152
  agentStore.setCurrentGenerationModeFromLocalStorage();
132
153
  agentStore.regisrerTextInput(textInput.value);
133
154
  textInput.value?.focus();
155
+ autoResize();
156
+ });
157
+
158
+ watch(() => textInput.value?.value, () => {
159
+ autoResize();
134
160
  });
135
161
 
136
162
  function autoResize() {
@@ -161,7 +187,9 @@ function selectMode(modeName: string) {
161
187
  async function sendMessage() {
162
188
  if (agentStore.isAudioChatMode) return;
163
189
  isModeMenuOpen.value = false;
164
- await agentStore.sendMessage();
190
+ // Routes to a steer (folded into the running turn) while generating, or a normal
191
+ // new-turn send when idle.
192
+ await agentStore.submitUserMessage();
165
193
  autoResize();
166
194
  props.conversationAreaRef?.handleSendMessage();
167
195
  }
@@ -0,0 +1,45 @@
1
+ <!--
2
+ TEMPORARY debug helper: enqueues a test steer message into the buffer WITHOUT sending
3
+ or steering it, so the SteerQueue enter/leave animation can be exercised without a live
4
+ generating turn. Remove before shipping.
5
+ -->
6
+ <template>
7
+ <div
8
+ class="fixed bottom-[400px] left-4 z-[9999] flex items-center gap-2 rounded-lg border border-dashed border-amber-400 bg-amber-50/95 px-2.5 py-2 shadow-lg backdrop-blur dark:border-amber-500 dark:bg-amber-950/95"
9
+ >
10
+ <span class="text-[10px] font-bold uppercase tracking-wider text-amber-600 dark:text-amber-400">
11
+ Steer&nbsp;debug
12
+ </span>
13
+ <input
14
+ v-model="debugText"
15
+ type="text"
16
+ placeholder="Test steer text…"
17
+ class="w-44 rounded-md border border-amber-300 bg-white px-2 py-1 text-xs text-gray-800 focus:outline-none focus:ring-1 focus:ring-amber-400 dark:border-amber-600 dark:bg-gray-800 dark:text-gray-100"
18
+ @keydown.enter.prevent="addTestSteer"
19
+ />
20
+ <button
21
+ type="button"
22
+ class="rounded-md bg-amber-500 px-2.5 py-1 text-xs font-semibold text-white transition-colors hover:bg-amber-600 active:bg-amber-700"
23
+ @click="addTestSteer"
24
+ >
25
+ Add steer
26
+ </button>
27
+ </div>
28
+ </template>
29
+
30
+ <script setup lang="ts">
31
+ import { ref } from 'vue';
32
+ import { useAgentStore } from './composables/useAgentStore';
33
+
34
+ const agentStore = useAgentStore();
35
+ const debugText = ref('');
36
+ let counter = 0;
37
+
38
+ function addTestSteer() {
39
+ counter += 1;
40
+ const text = debugText.value.trim() || `Test steer #${counter}`;
41
+ // enqueue() only pushes onto the client-side buffer — no /agent/steer, no send.
42
+ agentStore.enqueueSteerMessage(text);
43
+ debugText.value = '';
44
+ }
45
+ </script>
@@ -0,0 +1,54 @@
1
+ <template>
2
+ <div
3
+ v-if="agentStore.steerQueue.length"
4
+ class="absolute bottom-full left-0 w-full mb-2 flex flex-col gap-1.5 px-2 z-20"
5
+ >
6
+ <TransitionGroup
7
+ appear
8
+ enter-active-class="transition ease-out duration-150"
9
+ enter-from-class="opacity-0 translate-y-1"
10
+ enter-to-class="opacity-100 translate-y-0"
11
+ leave-active-class="transition ease-in duration-100"
12
+ leave-from-class="opacity-100 translate-y-0"
13
+ leave-to-class="opacity-0 translate-y-1"
14
+ >
15
+ <div
16
+ v-for="item in agentStore.steerQueue"
17
+ :key="item.id"
18
+ class="flex items-center gap-2 rounded-lg border border-gray-200 bg-gray-50 px-3 py-1.5 shadow-sm dark:border-gray-600 dark:bg-gray-800"
19
+ >
20
+ <span
21
+ class="flex-1 min-w-0 truncate text-sm text-lightInputText dark:text-darkInputText"
22
+ :title="item.text"
23
+ >
24
+ {{ item.text }}
25
+ </span>
26
+ <span class="shrink-0 text-xs text-gray-400 dark:text-gray-500">
27
+ {{ $t('queued') }}
28
+ </span>
29
+ <button
30
+ type="button"
31
+ class="shrink-0 rounded-md px-2 py-1 text-xs font-medium text-lightPrimary transition-colors hover:bg-lightPrimary/10 dark:text-darkPrimary dark:hover:bg-darkPrimary/10"
32
+ :title="$t('Steer into the current response')"
33
+ @click="agentStore.steerQueuedMessage(item.id)"
34
+ >
35
+ {{ $t('Steer') }}
36
+ </button>
37
+ <button
38
+ type="button"
39
+ class="shrink-0 flex h-6 w-6 items-center justify-center rounded-md text-lg leading-none text-gray-400 transition-colors hover:bg-gray-200 hover:text-gray-600 dark:hover:bg-gray-700 dark:hover:text-gray-300"
40
+ :title="$t('Remove from queue')"
41
+ @click="agentStore.removeQueuedMessage(item.id)"
42
+ >
43
+ &times;
44
+ </button>
45
+ </div>
46
+ </TransitionGroup>
47
+ </div>
48
+ </template>
49
+
50
+ <script setup lang="ts">
51
+ import { useAgentStore } from './composables/useAgentStore';
52
+
53
+ const agentStore = useAgentStore();
54
+ </script>
@@ -11,6 +11,7 @@ export const PLACEHOLDER_TYPING_DELAY_MS = 60;
11
11
  export const PLACEHOLDER_DELETING_DELAY_MS = 35;
12
12
  export const PLACEHOLDER_HOLD_DELAY_MS = 3000;
13
13
  export const PRE_SESSION_ID = 'pre-session';
14
+ export const STEER_PERSIST_PREFIX = '__adminforth_steer__:';
14
15
 
15
16
  export enum RESERVED_SYSTEM_MESSAGE_CONTENT {
16
17
  START_AUDIO_CHAT = 'START_AUDIO_CHAT',