@amodalai/runtime 0.1.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 (277) hide show
  1. package/LICENSE +21 -0
  2. package/dist/.last_build +0 -0
  3. package/dist/src/agent/agent-runner.d.ts +13 -0
  4. package/dist/src/agent/agent-runner.js +827 -0
  5. package/dist/src/agent/agent-runner.js.map +1 -0
  6. package/dist/src/agent/agent-runner.test.d.ts +6 -0
  7. package/dist/src/agent/agent-runner.test.js +552 -0
  8. package/dist/src/agent/agent-runner.test.js.map +1 -0
  9. package/dist/src/agent/agent-types.d.ts +57 -0
  10. package/dist/src/agent/agent-types.js +17 -0
  11. package/dist/src/agent/agent-types.js.map +1 -0
  12. package/dist/src/agent/agent-types.test.d.ts +6 -0
  13. package/dist/src/agent/agent-types.test.js +44 -0
  14. package/dist/src/agent/agent-types.test.js.map +1 -0
  15. package/dist/src/agent/automation-bridge.d.ts +24 -0
  16. package/dist/src/agent/automation-bridge.js +24 -0
  17. package/dist/src/agent/automation-bridge.js.map +1 -0
  18. package/dist/src/agent/automation-bridge.test.d.ts +6 -0
  19. package/dist/src/agent/automation-bridge.test.js +67 -0
  20. package/dist/src/agent/automation-bridge.test.js.map +1 -0
  21. package/dist/src/agent/config-watcher.d.ts +20 -0
  22. package/dist/src/agent/config-watcher.js +68 -0
  23. package/dist/src/agent/config-watcher.js.map +1 -0
  24. package/dist/src/agent/config-watcher.test.d.ts +6 -0
  25. package/dist/src/agent/config-watcher.test.js +83 -0
  26. package/dist/src/agent/config-watcher.test.js.map +1 -0
  27. package/dist/src/agent/custom-tools-e2e.test.d.ts +6 -0
  28. package/dist/src/agent/custom-tools-e2e.test.js +566 -0
  29. package/dist/src/agent/custom-tools-e2e.test.js.map +1 -0
  30. package/dist/src/agent/local-server.d.ts +15 -0
  31. package/dist/src/agent/local-server.js +158 -0
  32. package/dist/src/agent/local-server.js.map +1 -0
  33. package/dist/src/agent/local-server.test.d.ts +6 -0
  34. package/dist/src/agent/local-server.test.js +126 -0
  35. package/dist/src/agent/local-server.test.js.map +1 -0
  36. package/dist/src/agent/proactive/delivery.d.ts +21 -0
  37. package/dist/src/agent/proactive/delivery.js +68 -0
  38. package/dist/src/agent/proactive/delivery.js.map +1 -0
  39. package/dist/src/agent/proactive/delivery.test.d.ts +6 -0
  40. package/dist/src/agent/proactive/delivery.test.js +65 -0
  41. package/dist/src/agent/proactive/delivery.test.js.map +1 -0
  42. package/dist/src/agent/proactive/proactive-runner.d.ts +76 -0
  43. package/dist/src/agent/proactive/proactive-runner.js +201 -0
  44. package/dist/src/agent/proactive/proactive-runner.js.map +1 -0
  45. package/dist/src/agent/proactive/proactive-runner.test.d.ts +6 -0
  46. package/dist/src/agent/proactive/proactive-runner.test.js +265 -0
  47. package/dist/src/agent/proactive/proactive-runner.test.js.map +1 -0
  48. package/dist/src/agent/request-helper.d.ts +16 -0
  49. package/dist/src/agent/request-helper.js +87 -0
  50. package/dist/src/agent/request-helper.js.map +1 -0
  51. package/dist/src/agent/routes/automations.d.ts +19 -0
  52. package/dist/src/agent/routes/automations.js +58 -0
  53. package/dist/src/agent/routes/automations.js.map +1 -0
  54. package/dist/src/agent/routes/automations.test.d.ts +6 -0
  55. package/dist/src/agent/routes/automations.test.js +117 -0
  56. package/dist/src/agent/routes/automations.test.js.map +1 -0
  57. package/dist/src/agent/routes/chat.d.ts +35 -0
  58. package/dist/src/agent/routes/chat.js +88 -0
  59. package/dist/src/agent/routes/chat.js.map +1 -0
  60. package/dist/src/agent/routes/chat.test.d.ts +6 -0
  61. package/dist/src/agent/routes/chat.test.js +115 -0
  62. package/dist/src/agent/routes/chat.test.js.map +1 -0
  63. package/dist/src/agent/routes/inspect.d.ts +12 -0
  64. package/dist/src/agent/routes/inspect.js +40 -0
  65. package/dist/src/agent/routes/inspect.js.map +1 -0
  66. package/dist/src/agent/routes/inspect.test.d.ts +6 -0
  67. package/dist/src/agent/routes/inspect.test.js +80 -0
  68. package/dist/src/agent/routes/inspect.test.js.map +1 -0
  69. package/dist/src/agent/routes/stores.d.ts +20 -0
  70. package/dist/src/agent/routes/stores.js +137 -0
  71. package/dist/src/agent/routes/stores.js.map +1 -0
  72. package/dist/src/agent/routes/stores.test.d.ts +6 -0
  73. package/dist/src/agent/routes/stores.test.js +191 -0
  74. package/dist/src/agent/routes/stores.test.js.map +1 -0
  75. package/dist/src/agent/routes/task.d.ts +11 -0
  76. package/dist/src/agent/routes/task.js +116 -0
  77. package/dist/src/agent/routes/task.js.map +1 -0
  78. package/dist/src/agent/routes/task.test.d.ts +6 -0
  79. package/dist/src/agent/routes/task.test.js +91 -0
  80. package/dist/src/agent/routes/task.test.js.map +1 -0
  81. package/dist/src/agent/routes/webhooks.d.ts +17 -0
  82. package/dist/src/agent/routes/webhooks.js +53 -0
  83. package/dist/src/agent/routes/webhooks.js.map +1 -0
  84. package/dist/src/agent/routes/webhooks.test.d.ts +6 -0
  85. package/dist/src/agent/routes/webhooks.test.js +100 -0
  86. package/dist/src/agent/routes/webhooks.test.js.map +1 -0
  87. package/dist/src/agent/session-manager.d.ts +72 -0
  88. package/dist/src/agent/session-manager.js +214 -0
  89. package/dist/src/agent/session-manager.js.map +1 -0
  90. package/dist/src/agent/session-manager.test.d.ts +6 -0
  91. package/dist/src/agent/session-manager.test.js +145 -0
  92. package/dist/src/agent/session-manager.test.js.map +1 -0
  93. package/dist/src/agent/shell-executor-local.d.ts +16 -0
  94. package/dist/src/agent/shell-executor-local.js +51 -0
  95. package/dist/src/agent/shell-executor-local.js.map +1 -0
  96. package/dist/src/agent/shell-executor-local.test.d.ts +6 -0
  97. package/dist/src/agent/shell-executor-local.test.js +46 -0
  98. package/dist/src/agent/shell-executor-local.test.js.map +1 -0
  99. package/dist/src/agent/snapshot-server.d.ts +38 -0
  100. package/dist/src/agent/snapshot-server.js +114 -0
  101. package/dist/src/agent/snapshot-server.js.map +1 -0
  102. package/dist/src/agent/stores-e2e.test.d.ts +6 -0
  103. package/dist/src/agent/stores-e2e.test.js +433 -0
  104. package/dist/src/agent/stores-e2e.test.js.map +1 -0
  105. package/dist/src/agent/tool-context-builder.d.ts +11 -0
  106. package/dist/src/agent/tool-context-builder.js +84 -0
  107. package/dist/src/agent/tool-context-builder.js.map +1 -0
  108. package/dist/src/agent/tool-context-builder.test.d.ts +6 -0
  109. package/dist/src/agent/tool-context-builder.test.js +152 -0
  110. package/dist/src/agent/tool-context-builder.test.js.map +1 -0
  111. package/dist/src/agent/tool-executor-local.d.ts +15 -0
  112. package/dist/src/agent/tool-executor-local.js +74 -0
  113. package/dist/src/agent/tool-executor-local.js.map +1 -0
  114. package/dist/src/agent/tool-executor-local.test.d.ts +6 -0
  115. package/dist/src/agent/tool-executor-local.test.js +116 -0
  116. package/dist/src/agent/tool-executor-local.test.js.map +1 -0
  117. package/dist/src/agent/tool-harness-template.d.ts +23 -0
  118. package/dist/src/agent/tool-harness-template.js +94 -0
  119. package/dist/src/agent/tool-harness-template.js.map +1 -0
  120. package/dist/src/agent/user-context-fetcher.d.ts +25 -0
  121. package/dist/src/agent/user-context-fetcher.js +79 -0
  122. package/dist/src/agent/user-context-fetcher.js.map +1 -0
  123. package/dist/src/agent/user-context-fetcher.test.d.ts +6 -0
  124. package/dist/src/agent/user-context-fetcher.test.js +121 -0
  125. package/dist/src/agent/user-context-fetcher.test.js.map +1 -0
  126. package/dist/src/audit/audit-client.d.ts +46 -0
  127. package/dist/src/audit/audit-client.js +83 -0
  128. package/dist/src/audit/audit-client.js.map +1 -0
  129. package/dist/src/cron/heartbeat-runner.d.ts +24 -0
  130. package/dist/src/cron/heartbeat-runner.js +87 -0
  131. package/dist/src/cron/heartbeat-runner.js.map +1 -0
  132. package/dist/src/cron/heartbeat-runner.test.d.ts +6 -0
  133. package/dist/src/cron/heartbeat-runner.test.js +120 -0
  134. package/dist/src/cron/heartbeat-runner.test.js.map +1 -0
  135. package/dist/src/cron/heartbeat-scheduler.d.ts +26 -0
  136. package/dist/src/cron/heartbeat-scheduler.js +54 -0
  137. package/dist/src/cron/heartbeat-scheduler.js.map +1 -0
  138. package/dist/src/cron/heartbeat-scheduler.test.d.ts +6 -0
  139. package/dist/src/cron/heartbeat-scheduler.test.js +61 -0
  140. package/dist/src/cron/heartbeat-scheduler.test.js.map +1 -0
  141. package/dist/src/index.d.ts +24 -0
  142. package/dist/src/index.js +118 -0
  143. package/dist/src/index.js.map +1 -0
  144. package/dist/src/middleware/auth.d.ts +40 -0
  145. package/dist/src/middleware/auth.js +135 -0
  146. package/dist/src/middleware/auth.js.map +1 -0
  147. package/dist/src/middleware/auth.test.d.ts +6 -0
  148. package/dist/src/middleware/auth.test.js +268 -0
  149. package/dist/src/middleware/auth.test.js.map +1 -0
  150. package/dist/src/middleware/error-handler.d.ts +20 -0
  151. package/dist/src/middleware/error-handler.js +48 -0
  152. package/dist/src/middleware/error-handler.js.map +1 -0
  153. package/dist/src/middleware/error-handler.test.d.ts +6 -0
  154. package/dist/src/middleware/error-handler.test.js +68 -0
  155. package/dist/src/middleware/error-handler.test.js.map +1 -0
  156. package/dist/src/middleware/request-validation.d.ts +13 -0
  157. package/dist/src/middleware/request-validation.js +26 -0
  158. package/dist/src/middleware/request-validation.js.map +1 -0
  159. package/dist/src/middleware/request-validation.test.d.ts +6 -0
  160. package/dist/src/middleware/request-validation.test.js +57 -0
  161. package/dist/src/middleware/request-validation.test.js.map +1 -0
  162. package/dist/src/output/email-output.d.ts +10 -0
  163. package/dist/src/output/email-output.js +12 -0
  164. package/dist/src/output/email-output.js.map +1 -0
  165. package/dist/src/output/output-router.d.ts +12 -0
  166. package/dist/src/output/output-router.js +36 -0
  167. package/dist/src/output/output-router.js.map +1 -0
  168. package/dist/src/output/output-router.test.d.ts +6 -0
  169. package/dist/src/output/output-router.test.js +132 -0
  170. package/dist/src/output/output-router.test.js.map +1 -0
  171. package/dist/src/output/slack-output.d.ts +10 -0
  172. package/dist/src/output/slack-output.js +54 -0
  173. package/dist/src/output/slack-output.js.map +1 -0
  174. package/dist/src/output/webhook-output.d.ts +10 -0
  175. package/dist/src/output/webhook-output.js +25 -0
  176. package/dist/src/output/webhook-output.js.map +1 -0
  177. package/dist/src/routes/ai-stream.d.ts +159 -0
  178. package/dist/src/routes/ai-stream.js +309 -0
  179. package/dist/src/routes/ai-stream.js.map +1 -0
  180. package/dist/src/routes/ai-stream.test.d.ts +6 -0
  181. package/dist/src/routes/ai-stream.test.js +586 -0
  182. package/dist/src/routes/ai-stream.test.js.map +1 -0
  183. package/dist/src/routes/ask-user-response.d.ts +30 -0
  184. package/dist/src/routes/ask-user-response.js +61 -0
  185. package/dist/src/routes/ask-user-response.js.map +1 -0
  186. package/dist/src/routes/ask-user-response.test.d.ts +6 -0
  187. package/dist/src/routes/ask-user-response.test.js +88 -0
  188. package/dist/src/routes/ask-user-response.test.js.map +1 -0
  189. package/dist/src/routes/chat-stream.d.ts +14 -0
  190. package/dist/src/routes/chat-stream.js +84 -0
  191. package/dist/src/routes/chat-stream.js.map +1 -0
  192. package/dist/src/routes/chat-stream.test.d.ts +6 -0
  193. package/dist/src/routes/chat-stream.test.js +155 -0
  194. package/dist/src/routes/chat-stream.test.js.map +1 -0
  195. package/dist/src/routes/chat.d.ts +13 -0
  196. package/dist/src/routes/chat.js +55 -0
  197. package/dist/src/routes/chat.js.map +1 -0
  198. package/dist/src/routes/chat.test.d.ts +6 -0
  199. package/dist/src/routes/chat.test.js +99 -0
  200. package/dist/src/routes/chat.test.js.map +1 -0
  201. package/dist/src/routes/health.d.ts +13 -0
  202. package/dist/src/routes/health.js +23 -0
  203. package/dist/src/routes/health.js.map +1 -0
  204. package/dist/src/routes/health.test.d.ts +6 -0
  205. package/dist/src/routes/health.test.js +45 -0
  206. package/dist/src/routes/health.test.js.map +1 -0
  207. package/dist/src/routes/sessions.d.ts +14 -0
  208. package/dist/src/routes/sessions.js +82 -0
  209. package/dist/src/routes/sessions.js.map +1 -0
  210. package/dist/src/routes/webhooks.d.ts +13 -0
  211. package/dist/src/routes/webhooks.js +43 -0
  212. package/dist/src/routes/webhooks.js.map +1 -0
  213. package/dist/src/routes/webhooks.test.d.ts +6 -0
  214. package/dist/src/routes/webhooks.test.js +80 -0
  215. package/dist/src/routes/webhooks.test.js.map +1 -0
  216. package/dist/src/routes/widget-actions.d.ts +49 -0
  217. package/dist/src/routes/widget-actions.js +78 -0
  218. package/dist/src/routes/widget-actions.js.map +1 -0
  219. package/dist/src/server.d.ts +31 -0
  220. package/dist/src/server.js +129 -0
  221. package/dist/src/server.js.map +1 -0
  222. package/dist/src/server.test.d.ts +6 -0
  223. package/dist/src/server.test.js +153 -0
  224. package/dist/src/server.test.js.map +1 -0
  225. package/dist/src/session/history-converter.d.ts +21 -0
  226. package/dist/src/session/history-converter.js +59 -0
  227. package/dist/src/session/history-converter.js.map +1 -0
  228. package/dist/src/session/history-converter.test.d.ts +6 -0
  229. package/dist/src/session/history-converter.test.js +130 -0
  230. package/dist/src/session/history-converter.test.js.map +1 -0
  231. package/dist/src/session/session-manager.d.ts +117 -0
  232. package/dist/src/session/session-manager.js +480 -0
  233. package/dist/src/session/session-manager.js.map +1 -0
  234. package/dist/src/session/session-manager.test.d.ts +6 -0
  235. package/dist/src/session/session-manager.test.js +586 -0
  236. package/dist/src/session/session-manager.test.js.map +1 -0
  237. package/dist/src/session/session-runner.d.ts +30 -0
  238. package/dist/src/session/session-runner.js +771 -0
  239. package/dist/src/session/session-runner.js.map +1 -0
  240. package/dist/src/session/session-runner.test.d.ts +6 -0
  241. package/dist/src/session/session-runner.test.js +842 -0
  242. package/dist/src/session/session-runner.test.js.map +1 -0
  243. package/dist/src/stores/index.d.ts +8 -0
  244. package/dist/src/stores/index.js +9 -0
  245. package/dist/src/stores/index.js.map +1 -0
  246. package/dist/src/stores/key-resolver.d.ts +21 -0
  247. package/dist/src/stores/key-resolver.js +30 -0
  248. package/dist/src/stores/key-resolver.js.map +1 -0
  249. package/dist/src/stores/key-resolver.test.d.ts +6 -0
  250. package/dist/src/stores/key-resolver.test.js +31 -0
  251. package/dist/src/stores/key-resolver.test.js.map +1 -0
  252. package/dist/src/stores/pglite-store-backend.d.ts +36 -0
  253. package/dist/src/stores/pglite-store-backend.js +227 -0
  254. package/dist/src/stores/pglite-store-backend.js.map +1 -0
  255. package/dist/src/stores/pglite-store-backend.test.d.ts +6 -0
  256. package/dist/src/stores/pglite-store-backend.test.js +150 -0
  257. package/dist/src/stores/pglite-store-backend.test.js.map +1 -0
  258. package/dist/src/stores/ttl-resolver.d.ts +24 -0
  259. package/dist/src/stores/ttl-resolver.js +64 -0
  260. package/dist/src/stores/ttl-resolver.js.map +1 -0
  261. package/dist/src/stores/ttl-resolver.test.d.ts +6 -0
  262. package/dist/src/stores/ttl-resolver.test.js +68 -0
  263. package/dist/src/stores/ttl-resolver.test.js.map +1 -0
  264. package/dist/src/types.d.ts +227 -0
  265. package/dist/src/types.js +50 -0
  266. package/dist/src/types.js.map +1 -0
  267. package/dist/src/types.test.d.ts +6 -0
  268. package/dist/src/types.test.js +68 -0
  269. package/dist/src/types.test.js.map +1 -0
  270. package/dist/src/utils/jwt-verify.d.ts +20 -0
  271. package/dist/src/utils/jwt-verify.js +34 -0
  272. package/dist/src/utils/jwt-verify.js.map +1 -0
  273. package/dist/src/utils/jwt-verify.test.d.ts +6 -0
  274. package/dist/src/utils/jwt-verify.test.js +156 -0
  275. package/dist/src/utils/jwt-verify.test.js.map +1 -0
  276. package/dist/tsconfig.tsbuildinfo +1 -0
  277. package/package.json +51 -0
@@ -0,0 +1,771 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2025 Amodal Labs, Inc.
4
+ * SPDX-License-Identifier: MIT
5
+ */
6
+ import { GeminiEventType, ToolErrorType, PRESENT_TOOL_NAME, ACTIVATE_SKILL_TOOL_NAME, ASK_USER_TOOL_NAME, } from '@amodalai/core';
7
+ /**
8
+ * Custom message bus event key for subagent activity.
9
+ * Upstream MessageBusType doesn't include this — our dispatch tool emits on
10
+ * this string key, which works because MessageBus extends EventEmitter.
11
+ */
12
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- custom event key for our dispatch tool
13
+ const SUBAGENT_ACTIVITY_EVENT = 'subagent-activity';
14
+ import { SSEEventType, } from '../types.js';
15
+ const MAX_TURNS = 50;
16
+ const MAX_RESULT_LENGTH = 2000;
17
+ /**
18
+ * Extract text from tool response parts, truncated for audit logging.
19
+ * Handles both plain text parts and functionResponse parts (from task agents).
20
+ */
21
+ function extractResultText(parts) {
22
+ if (!parts || parts.length === 0)
23
+ return undefined;
24
+ const segments = [];
25
+ for (const p of parts) {
26
+ if (p.text) {
27
+ segments.push(p.text);
28
+ }
29
+ else if (p.functionResponse?.response) {
30
+ try {
31
+ segments.push(JSON.stringify(p.functionResponse.response));
32
+ }
33
+ catch {
34
+ segments.push('[unserializable response]');
35
+ }
36
+ }
37
+ }
38
+ const text = segments.join('');
39
+ if (!text)
40
+ return undefined;
41
+ return text.length > MAX_RESULT_LENGTH
42
+ ? text.slice(0, MAX_RESULT_LENGTH) + '...[truncated]'
43
+ : text;
44
+ }
45
+ const MAX_SUBAGENT_RESULT_LENGTH = 300;
46
+ /**
47
+ * Map SubagentActivityEvent type strings to SSE event_type values.
48
+ */
49
+ function mapSubagentEventType(type) {
50
+ switch (type) {
51
+ case 'TOOL_CALL_START': return 'tool_call_start';
52
+ case 'TOOL_CALL_END': return 'tool_call_end';
53
+ case 'THOUGHT_CHUNK': return 'thought';
54
+ case 'COMPLETE': return 'complete';
55
+ case 'ERROR': return 'error';
56
+ default: return 'error';
57
+ }
58
+ }
59
+ /**
60
+ * Truncate a string to the given max length, appending '...' if truncated.
61
+ */
62
+ function truncateSubagentResult(text, maxLen) {
63
+ if (!text)
64
+ return undefined;
65
+ return text.length > maxLen ? text.slice(0, maxLen) + '...' : text;
66
+ }
67
+ /**
68
+ * Convert a real-time SubagentActivityMessage to an SSESubagentEvent.
69
+ */
70
+ function subagentMessageToSSE(msg, parentToolId) {
71
+ return {
72
+ type: SSEEventType.SubagentEvent,
73
+ parent_tool_id: parentToolId,
74
+ agent_name: msg.agentName,
75
+ event_type: mapSubagentEventType(msg.eventType),
76
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- name from subagent activity data
77
+ tool_name: msg.data['name'],
78
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- args from subagent activity data
79
+ tool_args: msg.data['args'],
80
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- output from subagent activity data
81
+ result: truncateSubagentResult(msg.data['output'], MAX_SUBAGENT_RESULT_LENGTH),
82
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- text from subagent COMPLETE event
83
+ text: msg.data['text'],
84
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- error from subagent activity data
85
+ error: msg.data['error'],
86
+ timestamp: new Date().toISOString(),
87
+ };
88
+ }
89
+ /**
90
+ * Fire-and-forget POST to platform-api to report usage.
91
+ * Never blocks the chat stream — errors are logged to stderr.
92
+ */
93
+ function reportUsage(audit, model, taskAgentRuns, tokens) {
94
+ if (!audit.platformApiUrl || !audit.orgId)
95
+ return;
96
+ const url = `${audit.platformApiUrl}/api/orgs/${audit.orgId}/usage`;
97
+ const body = JSON.stringify({
98
+ model,
99
+ api_calls: 1,
100
+ chat_sessions: 1,
101
+ task_agent_runs: taskAgentRuns,
102
+ input_tokens: tokens.inputTokens,
103
+ output_tokens: tokens.outputTokens,
104
+ cached_tokens: tokens.cachedTokens,
105
+ });
106
+ fetch(url, {
107
+ method: 'POST',
108
+ headers: {
109
+ 'Content-Type': 'application/json',
110
+ ...(process.env['INTERNAL_API_KEY']
111
+ ? { 'X-Internal-Key': process.env['INTERNAL_API_KEY'] }
112
+ : {}),
113
+ },
114
+ body,
115
+ }).catch((err) => {
116
+ process.stderr.write(`[USAGE] Failed to report usage for org ${audit.orgId}: ${err instanceof Error ? err.message : String(err)}\n`);
117
+ });
118
+ }
119
+ /**
120
+ * Fire-and-forget POST to platform-api to persist session history.
121
+ * Never blocks the chat stream — errors are logged to stderr.
122
+ */
123
+ function saveSessionHistory(audit, sessionId, messages, status, sessionMeta) {
124
+ if (!audit.platformApiUrl || !audit.tenantId)
125
+ return;
126
+ const url = `${audit.platformApiUrl}/api/tenants/${audit.tenantId}/sessions`;
127
+ const body = JSON.stringify({
128
+ id: sessionId,
129
+ tenant_id: audit.tenantId,
130
+ app_id: audit.appId,
131
+ actor: audit.actor,
132
+ messages,
133
+ status,
134
+ // Persist model/provider so hydrated sessions use the same model
135
+ ...(sessionMeta?.model ? { model: sessionMeta.model } : {}),
136
+ ...(sessionMeta?.provider ? { provider: sessionMeta.provider } : {}),
137
+ });
138
+ fetch(url, {
139
+ method: 'POST',
140
+ headers: {
141
+ 'Content-Type': 'application/json',
142
+ Authorization: `Bearer ${audit.token}`,
143
+ },
144
+ body,
145
+ }).catch((err) => {
146
+ process.stderr.write(`[SESSION-HISTORY] Failed to save session ${sessionId}: ${err instanceof Error ? err.message : String(err)}\n`);
147
+ });
148
+ }
149
+ /**
150
+ * Run a synchronous (non-streaming) message through the agentic loop.
151
+ * Collects all text and tool calls, returns a ChatResponse.
152
+ */
153
+ export async function runMessage(session, message, signal, audit) {
154
+ const { geminiClient, scheduler, config } = session;
155
+ const promptId = `msg-${Date.now()}`;
156
+ const sessionStartMs = Date.now();
157
+ let currentMessages = [
158
+ { role: 'user', parts: [{ text: message }] },
159
+ ];
160
+ let responseText = '';
161
+ const toolCalls = [];
162
+ const skillsActivated = [];
163
+ let turnCount = 0;
164
+ let status = 'completed';
165
+ let errorMessage;
166
+ const tokens = { inputTokens: 0, outputTokens: 0, cachedTokens: 0 };
167
+ try {
168
+ while (true) {
169
+ turnCount++;
170
+ if (turnCount > MAX_TURNS) {
171
+ status = 'max_turns';
172
+ break;
173
+ }
174
+ if (signal.aborted)
175
+ break;
176
+ const toolCallRequests = [];
177
+ const responseStream = geminiClient.sendMessageStream(currentMessages[0]?.parts ?? [], signal, promptId, undefined, false, turnCount === 1 ? message : undefined);
178
+ for await (const event of responseStream) {
179
+ if (signal.aborted)
180
+ break;
181
+ if (event.type === GeminiEventType.Content) {
182
+ responseText += event.value;
183
+ }
184
+ else if (event.type === GeminiEventType.ToolCallRequest) {
185
+ toolCallRequests.push(event.value);
186
+ }
187
+ else if (event.type === GeminiEventType.Finished) {
188
+ const meta = event.value.usageMetadata;
189
+ if (meta) {
190
+ tokens.inputTokens += meta.promptTokenCount ?? 0;
191
+ tokens.outputTokens += meta.candidatesTokenCount ?? 0;
192
+ tokens.cachedTokens += meta.cachedContentTokenCount ?? 0;
193
+ }
194
+ }
195
+ else if (event.type === GeminiEventType.Error) {
196
+ status = 'error';
197
+ const errObj = event.value.error;
198
+ const errMsg = errObj instanceof Error ? errObj.message : (typeof errObj === 'object' && errObj !== null && 'message' in errObj) ? String(errObj['message']) : String(errObj);
199
+ errorMessage = errMsg;
200
+ throw new Error(errMsg);
201
+ }
202
+ else if (event.type === GeminiEventType.AgentExecutionStopped) {
203
+ return { session_id: session.id, response: responseText, tool_calls: toolCalls };
204
+ }
205
+ }
206
+ if (toolCallRequests.length > 0) {
207
+ const completedToolCalls = await scheduler.schedule(toolCallRequests, signal);
208
+ const toolResponseParts = [];
209
+ for (const completed of completedToolCalls) {
210
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- accessing optional field on CompletedToolCall union
211
+ const duration = 'durationMs' in completed ? completed['durationMs'] : undefined;
212
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- inner_tool_calls from subagent data
213
+ const innerCalls = completed.response.data?.['inner_tool_calls'];
214
+ toolCalls.push({
215
+ tool_name: completed.request.name,
216
+ tool_id: completed.request.callId,
217
+ args: completed.request.args,
218
+ status: completed.response.error ? 'error' : 'success',
219
+ duration_ms: duration,
220
+ error: completed.response.error?.message,
221
+ result: extractResultText(completed.response.responseParts),
222
+ inner_tool_calls: innerCalls,
223
+ });
224
+ // Track skill activations
225
+ if (completed.request.name === ACTIVATE_SKILL_TOOL_NAME &&
226
+ !completed.response.error) {
227
+ const skillName = String(completed.request.args['name'] ?? '');
228
+ if (skillName) {
229
+ skillsActivated.push(skillName);
230
+ }
231
+ }
232
+ if (completed.response.responseParts) {
233
+ toolResponseParts.push(...completed.response.responseParts);
234
+ }
235
+ // Record tool calls
236
+ try {
237
+ const currentModel = geminiClient.getCurrentSequenceModel() ?? config.getModel();
238
+ geminiClient
239
+ .getChat()
240
+ .recordCompletedToolCalls(currentModel, completedToolCalls);
241
+ }
242
+ catch {
243
+ // Non-critical — continue
244
+ }
245
+ }
246
+ // Check for stop execution
247
+ const stopTool = completedToolCalls.find((tc) => tc.response.errorType === ToolErrorType.STOP_EXECUTION);
248
+ if (stopTool) {
249
+ return { session_id: session.id, response: responseText, tool_calls: toolCalls };
250
+ }
251
+ currentMessages = [{ role: 'user', parts: toolResponseParts }];
252
+ }
253
+ else {
254
+ break;
255
+ }
256
+ }
257
+ return { session_id: session.id, response: responseText, tool_calls: toolCalls };
258
+ }
259
+ catch (err) {
260
+ status = 'error';
261
+ errorMessage = err instanceof Error ? err.message : String(err);
262
+ throw err;
263
+ }
264
+ finally {
265
+ if (audit) {
266
+ const model = geminiClient.getCurrentSequenceModel() ?? config.getModel();
267
+ logSessionCompleted(audit, session.id, message, responseText, turnCount, toolCalls, skillsActivated, status, errorMessage, sessionStartMs, model, tokens);
268
+ }
269
+ }
270
+ }
271
+ /**
272
+ * Stream a message through the agentic loop, yielding SSE events.
273
+ */
274
+ export async function* streamMessage(session, message, signal, audit, sessionManager) {
275
+ const { geminiClient, scheduler, config } = session;
276
+ const promptId = `msg-${Date.now()}`;
277
+ const sessionStartMs = Date.now();
278
+ let currentMessages = [
279
+ { role: 'user', parts: [{ text: message }] },
280
+ ];
281
+ // Accumulators for the consolidated audit event
282
+ const auditToolCalls = [];
283
+ const skillsActivated = [];
284
+ const widgetEvents = [];
285
+ let responseText = '';
286
+ let auditStatus = 'completed';
287
+ const tokens = { inputTokens: 0, outputTokens: 0, cachedTokens: 0 };
288
+ // Track content block ordering so history can reconstruct the correct interleave
289
+ const contentBlockOrder = [];
290
+ function trackText(text) {
291
+ const last = contentBlockOrder[contentBlockOrder.length - 1];
292
+ if (last && last['type'] === 'text') {
293
+ last['text'] = String(last['text'] ?? '') + text;
294
+ }
295
+ else {
296
+ contentBlockOrder.push({ type: 'text', text });
297
+ }
298
+ }
299
+ function trackToolCall(callId) {
300
+ const last = contentBlockOrder[contentBlockOrder.length - 1];
301
+ if (last && last['type'] === 'tool_calls') {
302
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- extending callIds array
303
+ last['callIds'].push(callId);
304
+ }
305
+ else {
306
+ contentBlockOrder.push({ type: 'tool_calls', callIds: [callId] });
307
+ }
308
+ }
309
+ let auditError;
310
+ // Accumulate user message for session history
311
+ const userMsg = {
312
+ type: 'user',
313
+ id: `msg-${Date.now()}`,
314
+ text: message,
315
+ timestamp: new Date().toISOString(),
316
+ };
317
+ session.accumulatedMessages.push(userMsg);
318
+ yield {
319
+ type: SSEEventType.Init,
320
+ session_id: session.id,
321
+ timestamp: new Date().toISOString(),
322
+ };
323
+ let turnCount = 0;
324
+ while (true) {
325
+ turnCount++;
326
+ if (turnCount > MAX_TURNS) {
327
+ auditStatus = 'max_turns';
328
+ auditError = 'Maximum turns exceeded';
329
+ yield {
330
+ type: SSEEventType.Error,
331
+ message: 'Maximum turns exceeded',
332
+ timestamp: new Date().toISOString(),
333
+ };
334
+ break;
335
+ }
336
+ if (signal.aborted)
337
+ break;
338
+ const toolCallRequests = [];
339
+ const responseStream = geminiClient.sendMessageStream(currentMessages[0]?.parts ?? [], signal, promptId, undefined, false, turnCount === 1 ? message : undefined);
340
+ for await (const event of responseStream) {
341
+ if (signal.aborted)
342
+ break;
343
+ if (event.type === GeminiEventType.Content) {
344
+ responseText += event.value;
345
+ trackText(event.value);
346
+ yield {
347
+ type: SSEEventType.TextDelta,
348
+ content: event.value,
349
+ timestamp: new Date().toISOString(),
350
+ };
351
+ }
352
+ else if (event.type === GeminiEventType.ToolCallRequest) {
353
+ // Suppress tool_call_start for present (widget events replace it) and ask_user (intercepted below)
354
+ if (event.value.name !== PRESENT_TOOL_NAME && event.value.name !== ASK_USER_TOOL_NAME) {
355
+ yield {
356
+ type: SSEEventType.ToolCallStart,
357
+ tool_name: event.value.name,
358
+ tool_id: event.value.callId,
359
+ parameters: event.value.args,
360
+ timestamp: new Date().toISOString(),
361
+ };
362
+ }
363
+ toolCallRequests.push(event.value);
364
+ }
365
+ else if (event.type === GeminiEventType.Finished) {
366
+ const meta = event.value.usageMetadata;
367
+ if (meta) {
368
+ tokens.inputTokens += meta.promptTokenCount ?? 0;
369
+ tokens.outputTokens += meta.candidatesTokenCount ?? 0;
370
+ tokens.cachedTokens += meta.cachedContentTokenCount ?? 0;
371
+ }
372
+ }
373
+ else if (event.type === GeminiEventType.Error) {
374
+ auditStatus = 'error';
375
+ const errObj = event.value.error;
376
+ const errMsg = errObj instanceof Error ? errObj.message : (typeof errObj === 'object' && errObj !== null && 'message' in errObj) ? String(errObj['message']) : String(errObj);
377
+ auditError = errMsg;
378
+ yield {
379
+ type: SSEEventType.Error,
380
+ message: errMsg,
381
+ timestamp: new Date().toISOString(),
382
+ };
383
+ logSessionCompleted(audit, session.id, message, responseText, turnCount, auditToolCalls, skillsActivated, auditStatus, auditError, sessionStartMs, geminiClient.getCurrentSequenceModel() ?? config.getModel(), tokens);
384
+ accumulateAssistantAndSave(session, audit, responseText, auditToolCalls, skillsActivated, widgetEvents, contentBlockOrder, 'error');
385
+ yield {
386
+ type: SSEEventType.Done,
387
+ timestamp: new Date().toISOString(),
388
+ usage: {
389
+ input_tokens: tokens.inputTokens,
390
+ output_tokens: tokens.outputTokens,
391
+ cached_tokens: tokens.cachedTokens,
392
+ total_tokens: tokens.inputTokens + tokens.outputTokens,
393
+ },
394
+ };
395
+ return;
396
+ }
397
+ else if (event.type === GeminiEventType.AgentExecutionStopped) {
398
+ logSessionCompleted(audit, session.id, message, responseText, turnCount, auditToolCalls, skillsActivated, auditStatus, auditError, sessionStartMs, geminiClient.getCurrentSequenceModel() ?? config.getModel(), tokens);
399
+ accumulateAssistantAndSave(session, audit, responseText, auditToolCalls, skillsActivated, widgetEvents, contentBlockOrder, 'completed');
400
+ yield {
401
+ type: SSEEventType.Done,
402
+ timestamp: new Date().toISOString(),
403
+ usage: {
404
+ input_tokens: tokens.inputTokens,
405
+ output_tokens: tokens.outputTokens,
406
+ cached_tokens: tokens.cachedTokens,
407
+ total_tokens: tokens.inputTokens + tokens.outputTokens,
408
+ },
409
+ };
410
+ return;
411
+ }
412
+ }
413
+ if (toolCallRequests.length > 0) {
414
+ // Partition: separate ask_user requests from regular tool calls
415
+ const askUserRequests = toolCallRequests.filter((req) => req.name === ASK_USER_TOOL_NAME);
416
+ const otherRequests = toolCallRequests.filter((req) => req.name !== ASK_USER_TOOL_NAME);
417
+ const toolResponseParts = [];
418
+ // Handle ask_user requests: yield SSE event, wait for user response
419
+ if (askUserRequests.length > 0 && sessionManager) {
420
+ for (const askReq of askUserRequests) {
421
+ const askId = askReq.callId;
422
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- LLM-provided questions array
423
+ const questions = askReq.args['questions'] ?? [];
424
+ const askStartMs = Date.now();
425
+ // Emit tool_call_start for the ask_user tool
426
+ yield {
427
+ type: SSEEventType.ToolCallStart,
428
+ tool_name: askReq.name,
429
+ tool_id: askId,
430
+ parameters: askReq.args,
431
+ timestamp: new Date().toISOString(),
432
+ };
433
+ // Track ask_user in content block ordering
434
+ contentBlockOrder.push({ type: 'ask_user', askId });
435
+ // Emit ask_user SSE event
436
+ yield {
437
+ type: SSEEventType.AskUser,
438
+ ask_id: askId,
439
+ questions,
440
+ timestamp: new Date().toISOString(),
441
+ };
442
+ try {
443
+ const answers = await sessionManager.waitForAskUserResponse(session, askId, signal);
444
+ const askDuration = Date.now() - askStartMs;
445
+ const resultOutput = JSON.stringify({ answers });
446
+ // Audit
447
+ auditToolCalls.push({
448
+ tool_name: askReq.name,
449
+ tool_id: askId,
450
+ args: askReq.args,
451
+ status: 'success',
452
+ duration_ms: askDuration,
453
+ });
454
+ // Emit tool_call_result
455
+ yield {
456
+ type: SSEEventType.ToolCallResult,
457
+ tool_id: askId,
458
+ status: 'success',
459
+ result: resultOutput.slice(0, 500),
460
+ duration_ms: askDuration,
461
+ timestamp: new Date().toISOString(),
462
+ };
463
+ // Build function response part
464
+ toolResponseParts.push({
465
+ functionResponse: {
466
+ id: askId,
467
+ name: ASK_USER_TOOL_NAME,
468
+ response: { output: resultOutput },
469
+ },
470
+ });
471
+ }
472
+ catch (err) {
473
+ const askDuration = Date.now() - askStartMs;
474
+ const errorMsg = err instanceof Error ? err.message : 'ask_user failed';
475
+ auditToolCalls.push({
476
+ tool_name: askReq.name,
477
+ tool_id: askId,
478
+ args: askReq.args,
479
+ status: 'error',
480
+ duration_ms: askDuration,
481
+ error: errorMsg,
482
+ });
483
+ yield {
484
+ type: SSEEventType.ToolCallResult,
485
+ tool_id: askId,
486
+ status: 'error',
487
+ error: errorMsg,
488
+ duration_ms: askDuration,
489
+ timestamp: new Date().toISOString(),
490
+ };
491
+ toolResponseParts.push({
492
+ functionResponse: {
493
+ id: askId,
494
+ name: ASK_USER_TOOL_NAME,
495
+ response: { output: JSON.stringify({ error: errorMsg }) },
496
+ },
497
+ });
498
+ }
499
+ }
500
+ }
501
+ else if (askUserRequests.length > 0) {
502
+ // No sessionManager — return fallback response
503
+ for (const askReq of askUserRequests) {
504
+ auditToolCalls.push({
505
+ tool_name: askReq.name,
506
+ tool_id: askReq.callId,
507
+ args: askReq.args,
508
+ status: 'error',
509
+ error: 'ask_user not supported in this session mode',
510
+ });
511
+ toolResponseParts.push({
512
+ functionResponse: {
513
+ id: askReq.callId,
514
+ name: ASK_USER_TOOL_NAME,
515
+ response: { output: JSON.stringify({ error: 'ask_user not supported in this session mode' }) },
516
+ },
517
+ });
518
+ }
519
+ }
520
+ // Handle other tool calls through the scheduler normally
521
+ if (otherRequests.length > 0) {
522
+ // --- Real-time subagent event streaming ---
523
+ // Subscribe to SUBAGENT_ACTIVITY on the message bus so we can yield
524
+ // subagent events as they happen (instead of waiting for dispatch to finish).
525
+ const messageBus = config.getMessageBus();
526
+ const subagentEventQueue = [];
527
+ let notifyNewEvent = null;
528
+ // Track dispatch tool call IDs for correlation
529
+ const unmatchedDispatchCallIds = new Set();
530
+ const dispatchIdToCallId = new Map();
531
+ for (const req of otherRequests) {
532
+ if (req.name === 'dispatch') {
533
+ unmatchedDispatchCallIds.add(req.callId);
534
+ }
535
+ }
536
+ const hasDispatchCalls = unmatchedDispatchCallIds.size > 0;
537
+ const subagentListener = (msg) => {
538
+ // Map dispatchId to the parent tool callId
539
+ let parentToolId = dispatchIdToCallId.get(msg.dispatchId);
540
+ if (!parentToolId && unmatchedDispatchCallIds.size > 0) {
541
+ // Assign to the first unmatched dispatch call
542
+ const firstUnmatched = unmatchedDispatchCallIds.values().next().value;
543
+ if (firstUnmatched) {
544
+ unmatchedDispatchCallIds.delete(firstUnmatched);
545
+ dispatchIdToCallId.set(msg.dispatchId, firstUnmatched);
546
+ parentToolId = firstUnmatched;
547
+ }
548
+ }
549
+ if (!parentToolId)
550
+ return;
551
+ subagentEventQueue.push(subagentMessageToSSE(msg, parentToolId));
552
+ // Wake up the yield loop
553
+ notifyNewEvent?.();
554
+ notifyNewEvent = null;
555
+ };
556
+ if (hasDispatchCalls) {
557
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion, @typescript-eslint/no-explicit-any -- custom event not in upstream Message union; EventEmitter accepts any string
558
+ messageBus.subscribe(SUBAGENT_ACTIVITY_EVENT, subagentListener);
559
+ }
560
+ // Start the scheduler (don't await yet — we'll yield events concurrently)
561
+ let completedToolCalls;
562
+ const schedulerPromise = scheduler.schedule(otherRequests, signal).then((result) => {
563
+ completedToolCalls = result;
564
+ // Wake up the yield loop so it can exit
565
+ notifyNewEvent?.();
566
+ notifyNewEvent = null;
567
+ });
568
+ // Yield subagent events in real-time while the scheduler is running
569
+ if (hasDispatchCalls) {
570
+ while (completedToolCalls === undefined || subagentEventQueue.length > 0) {
571
+ // Drain any pending events
572
+ while (subagentEventQueue.length > 0) {
573
+ yield subagentEventQueue.shift();
574
+ }
575
+ // If scheduler isn't done, wait for a new event or completion
576
+ if (completedToolCalls === undefined) {
577
+ await new Promise((resolve) => {
578
+ notifyNewEvent = resolve;
579
+ // Also resolve if the scheduler finishes while we're waiting
580
+ schedulerPromise.then(() => {
581
+ resolve();
582
+ return;
583
+ }, () => {
584
+ resolve();
585
+ return;
586
+ });
587
+ });
588
+ }
589
+ }
590
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion, @typescript-eslint/no-explicit-any -- custom event not in upstream Message union; EventEmitter accepts any string
591
+ messageBus.unsubscribe(SUBAGENT_ACTIVITY_EVENT, subagentListener);
592
+ }
593
+ // Ensure scheduler has completed (no-op if already resolved)
594
+ await schedulerPromise;
595
+ // Process completed tool calls
596
+ for (const completed of completedToolCalls) {
597
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- accessing optional field on CompletedToolCall union
598
+ const duration = 'durationMs' in completed ? completed['durationMs'] : undefined;
599
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- inner_tool_calls from subagent data
600
+ const innerCalls = completed.response.data?.['inner_tool_calls'];
601
+ // Accumulate tool call summary for audit
602
+ auditToolCalls.push({
603
+ tool_name: completed.request.name,
604
+ tool_id: completed.request.callId,
605
+ args: completed.request.args,
606
+ status: completed.response.error ? 'error' : 'success',
607
+ duration_ms: duration,
608
+ error: completed.response.error?.message,
609
+ result: extractResultText(completed.response.responseParts),
610
+ inner_tool_calls: innerCalls,
611
+ });
612
+ // Most subagent events were streamed in real-time above.
613
+ // The COMPLETE event (agent's final summary) may be missed due to race
614
+ // conditions, so emit it from the batch data as a fallback.
615
+ if (completed.request.name === 'dispatch' && completed.response.data) {
616
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- subagent_events from dispatch tool data
617
+ const batchEvents = completed.response.data['subagent_events'];
618
+ if (batchEvents) {
619
+ for (const evt of batchEvents) {
620
+ if (evt.type === 'COMPLETE' && typeof evt.data['text'] === 'string') {
621
+ yield {
622
+ type: SSEEventType.SubagentEvent,
623
+ parent_tool_id: completed.request.callId,
624
+ agent_name: evt.agentName,
625
+ event_type: 'complete',
626
+ text: evt.data['text'],
627
+ timestamp: new Date().toISOString(),
628
+ };
629
+ }
630
+ }
631
+ }
632
+ }
633
+ if (completed.request.name === PRESENT_TOOL_NAME && !completed.response.error) {
634
+ // Emit a widget event instead of tool_call_result for the present tool
635
+ const args = completed.request.args;
636
+ const widgetType = String(args['widget'] ?? 'unknown');
637
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- LLM-provided data object
638
+ const widgetData = args['data'] ?? {};
639
+ widgetEvents.push({ widgetType, data: widgetData });
640
+ contentBlockOrder.push({ type: 'widget', widgetType, data: widgetData });
641
+ yield {
642
+ type: SSEEventType.Widget,
643
+ widget_type: widgetType,
644
+ data: widgetData,
645
+ timestamp: new Date().toISOString(),
646
+ };
647
+ }
648
+ else {
649
+ trackToolCall(completed.request.callId);
650
+ yield {
651
+ type: SSEEventType.ToolCallResult,
652
+ tool_id: completed.request.callId,
653
+ status: completed.response.error ? 'error' : 'success',
654
+ result: extractResultText(completed.response.responseParts)?.slice(0, 500),
655
+ duration_ms: duration,
656
+ error: completed.response.error?.message,
657
+ timestamp: new Date().toISOString(),
658
+ };
659
+ }
660
+ // Track skill activations
661
+ if (completed.request.name === ACTIVATE_SKILL_TOOL_NAME &&
662
+ !completed.response.error) {
663
+ const skillName = String(completed.request.args['name'] ?? '');
664
+ if (skillName) {
665
+ skillsActivated.push(skillName);
666
+ yield {
667
+ type: SSEEventType.SkillActivated,
668
+ skill_name: skillName,
669
+ timestamp: new Date().toISOString(),
670
+ };
671
+ }
672
+ }
673
+ if (completed.response.responseParts) {
674
+ toolResponseParts.push(...completed.response.responseParts);
675
+ }
676
+ }
677
+ // Record tool calls
678
+ try {
679
+ const currentModel = geminiClient.getCurrentSequenceModel() ?? config.getModel();
680
+ geminiClient
681
+ .getChat()
682
+ .recordCompletedToolCalls(currentModel, completedToolCalls);
683
+ }
684
+ catch {
685
+ // Non-critical
686
+ }
687
+ const stopTool = completedToolCalls.find((tc) => tc.response.errorType === ToolErrorType.STOP_EXECUTION);
688
+ if (stopTool) {
689
+ logSessionCompleted(audit, session.id, message, responseText, turnCount, auditToolCalls, skillsActivated, auditStatus, auditError, sessionStartMs, geminiClient.getCurrentSequenceModel() ?? config.getModel(), tokens);
690
+ accumulateAssistantAndSave(session, audit, responseText, auditToolCalls, skillsActivated, widgetEvents, contentBlockOrder, 'completed');
691
+ yield {
692
+ type: SSEEventType.Done,
693
+ timestamp: new Date().toISOString(),
694
+ };
695
+ return;
696
+ }
697
+ }
698
+ currentMessages = [{ role: 'user', parts: toolResponseParts }];
699
+ }
700
+ else {
701
+ break;
702
+ }
703
+ }
704
+ logSessionCompleted(audit, session.id, message, responseText, turnCount, auditToolCalls, skillsActivated, auditStatus, auditError, sessionStartMs, geminiClient.getCurrentSequenceModel() ?? config.getModel(), tokens);
705
+ accumulateAssistantAndSave(session, audit, responseText, auditToolCalls, skillsActivated, widgetEvents, contentBlockOrder, auditStatus === 'completed' ? 'completed' : 'error');
706
+ yield {
707
+ type: SSEEventType.Done,
708
+ timestamp: new Date().toISOString(),
709
+ };
710
+ }
711
+ /**
712
+ * Accumulate the assistant response message and fire-and-forget save to platform-api.
713
+ */
714
+ function accumulateAssistantAndSave(session, audit, responseText, toolCalls, skillsActivated, widgetEvents, contentBlocks, status) {
715
+ const assistantMsg = {
716
+ type: 'assistant_text',
717
+ id: `msg-${Date.now()}`,
718
+ text: responseText,
719
+ timestamp: new Date().toISOString(),
720
+ toolCalls: toolCalls.map((tc) => ({
721
+ toolName: tc.tool_name,
722
+ toolId: tc.tool_id,
723
+ args: tc.args,
724
+ status: tc.status,
725
+ duration_ms: tc.duration_ms,
726
+ error: tc.error,
727
+ result: tc.result,
728
+ inner_tool_calls: tc.inner_tool_calls,
729
+ })),
730
+ skillActivations: skillsActivated,
731
+ widgets: widgetEvents.map((w) => ({
732
+ widgetType: w.widgetType,
733
+ data: w.data,
734
+ })),
735
+ contentBlocks,
736
+ };
737
+ session.accumulatedMessages.push(assistantMsg);
738
+ if (audit) {
739
+ saveSessionHistory(audit, session.id, session.accumulatedMessages, status, {
740
+ model: session.model,
741
+ provider: session.provider,
742
+ });
743
+ }
744
+ }
745
+ function logSessionCompleted(audit, sessionId, message, response, turns, toolCalls, skillsActivated, status, error, startMs, model, tokens) {
746
+ if (!audit)
747
+ return;
748
+ const details = {
749
+ message,
750
+ response,
751
+ tenant_id: audit.tenantId,
752
+ org_id: audit.orgId,
753
+ turns,
754
+ duration_ms: Date.now() - startMs,
755
+ status,
756
+ tool_calls: toolCalls,
757
+ skills_activated: skillsActivated,
758
+ };
759
+ if (error) {
760
+ details['error'] = error;
761
+ }
762
+ audit.auditClient.log(audit.appId, audit.token, {
763
+ event: 'session_completed',
764
+ resource_name: sessionId,
765
+ details,
766
+ });
767
+ // Report usage to platform API
768
+ const taskAgentRuns = toolCalls.filter((tc) => tc.tool_name === 'dispatch').length;
769
+ reportUsage(audit, model ?? 'unknown', taskAgentRuns, tokens ?? { inputTokens: 0, outputTokens: 0, cachedTokens: 0 });
770
+ }
771
+ //# sourceMappingURL=session-runner.js.map