@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,827 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2025 Amodal Labs, Inc.
4
+ * SPDX-License-Identifier: MIT
5
+ */
6
+ import { EXPLORE_TOOL_NAME, EXPLORE_TOOL_SCHEMA, validateExploreRequest, resolveExploreModel, FailoverProvider, storeToJsonSchema, storeToToolName, findStoreByToolName, } from '@amodalai/core';
7
+ import { resolveKey } from '../stores/key-resolver.js';
8
+ import { SSEEventType } from '../types.js';
9
+ import { makeApiRequest } from './request-helper.js';
10
+ import { buildToolContext } from './tool-context-builder.js';
11
+ import { LocalToolExecutor } from './tool-executor-local.js';
12
+ const MAX_TURNS = 15;
13
+ /**
14
+ * Runs an agent turn as a ReAct loop, yielding SSE events.
15
+ *
16
+ * Uses FailoverProvider for multi-provider support with retry + fallback.
17
+ */
18
+ export async function* runAgentTurn(session, message, signal) {
19
+ // Append user message
20
+ session.conversationHistory.push({ role: 'user', content: message });
21
+ const modelConfig = session.runtime.repo.config.models.main;
22
+ let provider;
23
+ try {
24
+ provider = new FailoverProvider(modelConfig);
25
+ }
26
+ catch (err) {
27
+ const errMsg = err instanceof Error ? err.message : String(err);
28
+ yield {
29
+ type: SSEEventType.Error,
30
+ message: `Provider initialization failed: ${errMsg}`,
31
+ timestamp: ts(),
32
+ };
33
+ yield { type: SSEEventType.Done, timestamp: ts() };
34
+ return;
35
+ }
36
+ const tools = buildTools(session);
37
+ const systemPrompt = buildSystemPrompt(session);
38
+ let turns = 0;
39
+ while (turns < MAX_TURNS) {
40
+ if (signal.aborted) {
41
+ yield { type: SSEEventType.Error, message: 'Request aborted', timestamp: ts() };
42
+ yield { type: SSEEventType.Done, timestamp: ts() };
43
+ return;
44
+ }
45
+ turns++;
46
+ try {
47
+ const chatRequest = {
48
+ model: modelConfig.model,
49
+ systemPrompt,
50
+ messages: session.conversationHistory,
51
+ tools,
52
+ maxTokens: 4096,
53
+ signal,
54
+ };
55
+ // Use streaming when available for real-time text delivery
56
+ if (provider.chatStream) {
57
+ const { content, hasToolUse, toolResults } = yield* processStream(provider.chatStream(chatRequest), session, signal);
58
+ // Store assistant message
59
+ session.conversationHistory.push({ role: 'assistant', content });
60
+ if (hasToolUse && toolResults.length > 0) {
61
+ session.conversationHistory.push(...toolResults);
62
+ continue;
63
+ }
64
+ break;
65
+ }
66
+ // Non-streaming fallback
67
+ const response = await provider.chat(chatRequest);
68
+ let hasToolUse = false;
69
+ const toolResults = [];
70
+ for (const block of response.content) {
71
+ if (block.type === 'text') {
72
+ const processed = processTextOutput(session, block.text);
73
+ yield { type: SSEEventType.TextDelta, content: processed, timestamp: ts() };
74
+ }
75
+ else if (block.type === 'tool_use') {
76
+ hasToolUse = true;
77
+ yield {
78
+ type: SSEEventType.ToolCallStart,
79
+ tool_name: block.name,
80
+ tool_id: block.id,
81
+ parameters: block.input,
82
+ timestamp: ts(),
83
+ };
84
+ // Emit ExploreStart before execution
85
+ if (block.name === EXPLORE_TOOL_NAME) {
86
+ yield {
87
+ type: SSEEventType.ExploreStart,
88
+ query: String(block.input['query'] ?? ''),
89
+ timestamp: ts(),
90
+ };
91
+ }
92
+ const startMs = Date.now();
93
+ const execResult = await executeTool(session, block.name, block.input, block.id, signal);
94
+ const durationMs = Date.now() - startMs;
95
+ // Emit subagent events from explore
96
+ if (execResult.subagentEvents) {
97
+ for (const evt of execResult.subagentEvents) {
98
+ yield evt;
99
+ }
100
+ }
101
+ // Emit ExploreEnd after execution
102
+ if (block.name === EXPLORE_TOOL_NAME && execResult.exploreResult) {
103
+ yield {
104
+ type: SSEEventType.ExploreEnd,
105
+ summary: execResult.exploreResult.summary,
106
+ tokens_used: execResult.exploreResult.tokensUsed,
107
+ timestamp: ts(),
108
+ };
109
+ }
110
+ yield {
111
+ type: SSEEventType.ToolCallResult,
112
+ tool_id: block.id,
113
+ status: execResult.result.error ? 'error' : 'success',
114
+ result: execResult.result.output,
115
+ error: execResult.result.error,
116
+ duration_ms: durationMs,
117
+ timestamp: ts(),
118
+ };
119
+ toolResults.push({
120
+ role: 'tool_result',
121
+ toolCallId: block.id,
122
+ content: execResult.result.error ?? execResult.result.output ?? '',
123
+ isError: !!execResult.result.error,
124
+ });
125
+ }
126
+ }
127
+ session.conversationHistory.push({ role: 'assistant', content: response.content });
128
+ if (hasToolUse && toolResults.length > 0) {
129
+ session.conversationHistory.push(...toolResults);
130
+ continue;
131
+ }
132
+ break;
133
+ }
134
+ catch (err) {
135
+ const errMsg = err instanceof Error ? err.message : String(err);
136
+ yield { type: SSEEventType.Error, message: `LLM error: ${errMsg}`, timestamp: ts() };
137
+ break;
138
+ }
139
+ }
140
+ if (turns >= MAX_TURNS) {
141
+ yield {
142
+ type: SSEEventType.Error,
143
+ message: `Agent loop exceeded max turns (${MAX_TURNS})`,
144
+ timestamp: ts(),
145
+ };
146
+ }
147
+ yield { type: SSEEventType.Done, timestamp: ts() };
148
+ }
149
+ // ---------------------------------------------------------------------------
150
+ // Helpers
151
+ // ---------------------------------------------------------------------------
152
+ function ts() {
153
+ return new Date().toISOString();
154
+ }
155
+ function buildSystemPrompt(session) {
156
+ const parts = [session.runtime.compiledContext.systemPrompt];
157
+ const planReminder = session.planModeManager.getPlanningReminder();
158
+ if (planReminder) {
159
+ parts.push(planReminder);
160
+ }
161
+ const approvedPlan = session.planModeManager.getApprovedPlanContext();
162
+ if (approvedPlan) {
163
+ parts.push(approvedPlan);
164
+ }
165
+ return parts.join('\n\n');
166
+ }
167
+ function buildTools(session) {
168
+ const tools = [];
169
+ // Request tool (for connected systems)
170
+ tools.push({
171
+ name: 'request',
172
+ description: 'Make HTTP requests to connected systems. Specify connection name, method, endpoint, and optional params/data.',
173
+ parameters: {
174
+ type: 'object',
175
+ properties: {
176
+ connection: { type: 'string', description: 'Connection name' },
177
+ method: { type: 'string', enum: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'] },
178
+ endpoint: { type: 'string', description: 'API endpoint path' },
179
+ params: { type: 'object', description: 'Query parameters' },
180
+ data: { description: 'Request body' },
181
+ intent: { type: 'string', enum: ['read', 'write', 'confirmed_write'] },
182
+ },
183
+ required: ['connection', 'method', 'endpoint', 'intent'],
184
+ },
185
+ });
186
+ // Explore tool
187
+ tools.push({
188
+ name: EXPLORE_TOOL_NAME,
189
+ description: EXPLORE_TOOL_SCHEMA.description,
190
+ parameters: {
191
+ type: 'object',
192
+ properties: {
193
+ query: { type: 'string', description: 'What to investigate' },
194
+ endpoint_hints: {
195
+ type: 'array',
196
+ items: { type: 'string' },
197
+ description: 'Optional endpoint paths to prioritize',
198
+ },
199
+ model: {
200
+ type: 'string',
201
+ description: 'Optional: "simple" for lightweight, "default" for standard, "advanced" for primary model, or "provider:model" for specific',
202
+ },
203
+ },
204
+ required: ['query'],
205
+ },
206
+ });
207
+ // Plan mode tools
208
+ tools.push({
209
+ name: 'enter_plan_mode',
210
+ description: 'Enter planning mode. Write operations will be blocked until a plan is approved.',
211
+ parameters: {
212
+ type: 'object',
213
+ properties: {
214
+ reason: { type: 'string', description: 'Why planning mode is needed' },
215
+ },
216
+ },
217
+ });
218
+ tools.push({
219
+ name: 'exit_plan_mode',
220
+ description: 'Exit planning mode.',
221
+ parameters: {
222
+ type: 'object',
223
+ properties: {},
224
+ },
225
+ });
226
+ // Custom tools from tools/ directory
227
+ for (const tool of session.runtime.repo.tools) {
228
+ // Skip tools marked as hidden from the LLM
229
+ if (tool.confirm === 'never') {
230
+ continue;
231
+ }
232
+ tools.push({
233
+ name: tool.name,
234
+ description: tool.description,
235
+ parameters: tool.parameters,
236
+ });
237
+ }
238
+ // MCP tools from connected MCP servers
239
+ if (session.mcpManager) {
240
+ for (const mcpTool of session.mcpManager.getDiscoveredTools()) {
241
+ tools.push({
242
+ name: mcpTool.name,
243
+ description: mcpTool.description,
244
+ parameters: mcpTool.parameters,
245
+ });
246
+ }
247
+ }
248
+ // Store write tools (one per store — schema as parameters for structured output)
249
+ for (const store of session.runtime.repo.stores) {
250
+ tools.push({
251
+ name: storeToToolName(store.name),
252
+ description: `Store a ${store.entity.name} to the ${store.name} collection.`,
253
+ parameters: storeToJsonSchema(store),
254
+ });
255
+ }
256
+ // Store query tool (single tool for reading from any store)
257
+ if (session.runtime.repo.stores.length > 0) {
258
+ tools.push({
259
+ name: 'query_store',
260
+ description: 'Query documents from a store collection. Use "key" for a single document or "filter" for a list.',
261
+ parameters: {
262
+ type: 'object',
263
+ properties: {
264
+ store: {
265
+ type: 'string',
266
+ enum: session.runtime.repo.stores.map((s) => s.name),
267
+ description: 'The store to query',
268
+ },
269
+ key: { type: 'string', description: 'Get a specific document by key' },
270
+ filter: { type: 'object', description: 'Filter by field values (equality match)' },
271
+ sort: { type: 'string', description: 'Sort field, prefix with - for descending' },
272
+ limit: { type: 'number', description: 'Max documents to return (default: 20)' },
273
+ },
274
+ required: ['store'],
275
+ },
276
+ });
277
+ }
278
+ // Shell execution tool (opt-in via config.sandbox.shellExec)
279
+ if (session.runtime.repo.config.sandbox?.shellExec) {
280
+ tools.push({
281
+ name: 'shell_exec',
282
+ description: 'Execute a shell command. Use for data transformation, computation, scripting, or anything that benefits from code execution.',
283
+ parameters: {
284
+ type: 'object',
285
+ properties: {
286
+ command: { type: 'string', description: 'The shell command to execute' },
287
+ },
288
+ required: ['command'],
289
+ },
290
+ });
291
+ }
292
+ return tools;
293
+ }
294
+ async function executeTool(session, toolName, args, toolId, signal) {
295
+ switch (toolName) {
296
+ case 'request':
297
+ return { result: await executeRequestTool(session, args, signal) };
298
+ case EXPLORE_TOOL_NAME:
299
+ return executeExploreTool(session, args, toolId, signal);
300
+ case 'enter_plan_mode':
301
+ return { result: await executePlanModeEnter(session, args) };
302
+ case 'exit_plan_mode':
303
+ return { result: await executePlanModeExit(session) };
304
+ case 'shell_exec':
305
+ return { result: await executeShellExecTool(session, args, signal) };
306
+ case 'query_store':
307
+ return { result: await executeQueryStore(session, args) };
308
+ default: {
309
+ // Check store write tools (store_* prefix)
310
+ if (toolName.startsWith('store_')) {
311
+ const store = findStoreByToolName(session.runtime.repo.stores, toolName);
312
+ if (store) {
313
+ return { result: await executeStorePut(session, store, args) };
314
+ }
315
+ }
316
+ // Check custom tools
317
+ const customTool = session.runtime.repo.tools.find((t) => t.name === toolName);
318
+ if (customTool) {
319
+ return { result: await executeCustomTool(session, customTool, args, signal) };
320
+ }
321
+ // Check MCP tools (namespaced as serverName__toolName)
322
+ if (session.mcpManager?.isMcpTool(toolName)) {
323
+ return { result: await executeMcpTool(session, toolName, args) };
324
+ }
325
+ return { result: { error: `Unknown tool: ${toolName}` } };
326
+ }
327
+ }
328
+ }
329
+ async function executeRequestTool(session, args, signal) {
330
+ const connectionName = String(args['connection'] ?? '');
331
+ const method = String(args['method'] ?? 'GET');
332
+ const endpoint = String(args['endpoint'] ?? '');
333
+ const intent = String(args['intent'] ?? 'read');
334
+ const params = args['params'];
335
+ const data = args['data'];
336
+ // Action gate for writes
337
+ if (intent === 'write' || intent === 'confirmed_write') {
338
+ if (session.planModeManager.isActive()) {
339
+ return { error: 'Write operations are blocked in plan mode. Present your plan for approval first.' };
340
+ }
341
+ const gateResult = session.runtime.actionGate.evaluate(endpoint, connectionName);
342
+ if (gateResult['decision'] === 'never') {
343
+ return { error: `Write to ${endpoint} is blocked: ${gateResult['reason'] ?? 'policy'}` };
344
+ }
345
+ if (gateResult['decision'] === 'confirm' && intent !== 'confirmed_write') {
346
+ return { error: `Write to ${endpoint} requires confirmation. Re-call with intent: "confirmed_write".` };
347
+ }
348
+ }
349
+ return makeApiRequest(session, connectionName, method, endpoint, params, data, signal);
350
+ }
351
+ // Lazy-initialized executor (shared across all custom tool calls)
352
+ let localToolExecutor = null;
353
+ async function executeCustomTool(session, tool, args, signal) {
354
+ // Confirmation gating for tools that require it
355
+ if (tool.confirm === true || tool.confirm === 'review') {
356
+ if (session.planModeManager.isActive()) {
357
+ return { error: 'Custom tool writes are blocked in plan mode. Present your plan for approval first.' };
358
+ }
359
+ }
360
+ const ctx = buildToolContext(session, tool, signal);
361
+ if (!localToolExecutor) {
362
+ localToolExecutor = new LocalToolExecutor();
363
+ }
364
+ const executor = session.toolExecutor ?? localToolExecutor;
365
+ try {
366
+ const result = await executor.execute(tool, args, ctx);
367
+ return { output: JSON.stringify(result) };
368
+ }
369
+ catch (err) {
370
+ const message = err instanceof Error ? err.message : String(err);
371
+ return { error: message };
372
+ }
373
+ }
374
+ async function executeMcpTool(session, toolName, args) {
375
+ if (!session.mcpManager) {
376
+ return { error: 'MCP is not configured' };
377
+ }
378
+ try {
379
+ const result = await session.mcpManager.callTool(toolName, args);
380
+ if (result.isError) {
381
+ const errorText = result.content
382
+ .filter((c) => c.type === 'text' && c.text)
383
+ .map((c) => c.text)
384
+ .join('\n');
385
+ return { error: errorText || 'MCP tool returned an error' };
386
+ }
387
+ const output = result.content
388
+ .map((c) => {
389
+ if (c.type === 'text' && c.text)
390
+ return c.text;
391
+ if (c.type === 'image' && c.data)
392
+ return `[image: ${c.mimeType ?? 'unknown'}]`;
393
+ return `[${c.type}]`;
394
+ })
395
+ .join('\n');
396
+ return { output };
397
+ }
398
+ catch (err) {
399
+ const message = err instanceof Error ? err.message : String(err);
400
+ return { error: `MCP tool call failed: ${message}` };
401
+ }
402
+ }
403
+ async function executeShellExecTool(session, args, signal) {
404
+ const command = String(args['command'] ?? '');
405
+ if (!command) {
406
+ return { error: 'Missing required parameter: command' };
407
+ }
408
+ const shellExecutor = session.shellExecutor;
409
+ if (!shellExecutor) {
410
+ return { error: 'Shell execution is not available' };
411
+ }
412
+ const maxTimeout = session.runtime.repo.config.sandbox?.maxTimeout ?? 30000;
413
+ try {
414
+ const result = await shellExecutor.exec(command, maxTimeout, signal);
415
+ const output = [result.stdout, result.stderr].filter(Boolean).join('\n');
416
+ return { output: `Exit code: ${result.exitCode}\n${output}` };
417
+ }
418
+ catch (err) {
419
+ if (signal.aborted) {
420
+ return { error: 'Shell execution aborted' };
421
+ }
422
+ return { error: err instanceof Error ? err.message : String(err) };
423
+ }
424
+ }
425
+ // ---------------------------------------------------------------------------
426
+ // Store tools
427
+ // ---------------------------------------------------------------------------
428
+ async function executeStorePut(session, store, args) {
429
+ if (!session.storeBackend) {
430
+ return { error: 'Store backend is not configured' };
431
+ }
432
+ // Block writes in plan mode
433
+ if (session.planModeManager.isActive()) {
434
+ return { error: 'Store writes are blocked in plan mode. Present your plan for approval first.' };
435
+ }
436
+ // Resolve key from template
437
+ let key;
438
+ try {
439
+ key = resolveKey(store.entity.key, args);
440
+ }
441
+ catch (err) {
442
+ return { error: err instanceof Error ? err.message : String(err) };
443
+ }
444
+ try {
445
+ const result = await session.storeBackend.put(session.tenantId, store.name, key, args, {});
446
+ return { output: JSON.stringify(result) };
447
+ }
448
+ catch (err) {
449
+ return { error: err instanceof Error ? err.message : String(err) };
450
+ }
451
+ }
452
+ async function executeQueryStore(session, args) {
453
+ if (!session.storeBackend) {
454
+ return { error: 'Store backend is not configured' };
455
+ }
456
+ const storeName = String(args['store'] ?? '');
457
+ if (!storeName) {
458
+ return { error: 'Missing required parameter: store' };
459
+ }
460
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- tool args from LLM
461
+ const key = args['key'];
462
+ try {
463
+ if (key) {
464
+ // Single document lookup
465
+ const doc = await session.storeBackend.get(session.tenantId, storeName, key);
466
+ if (!doc) {
467
+ return { output: JSON.stringify({ found: false, key }) };
468
+ }
469
+ return { output: JSON.stringify({ found: true, ...doc }) };
470
+ }
471
+ // List with optional filtering
472
+ const result = await session.storeBackend.list(session.tenantId, storeName, {
473
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
474
+ filter: args['filter'],
475
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- tool args from LLM
476
+ sort: args['sort'],
477
+ limit: typeof args['limit'] === 'number' ? args['limit'] : 20,
478
+ });
479
+ return { output: JSON.stringify(result) };
480
+ }
481
+ catch (err) {
482
+ return { error: err instanceof Error ? err.message : String(err) };
483
+ }
484
+ }
485
+ // ---------------------------------------------------------------------------
486
+ // Explore tool
487
+ // ---------------------------------------------------------------------------
488
+ async function executeExploreTool(session, args, parentToolId, signal) {
489
+ const query = String(args['query'] ?? '');
490
+ const endpointHints = Array.isArray(args['endpoint_hints'])
491
+ ? args['endpoint_hints'].filter((h) => typeof h === 'string')
492
+ : undefined;
493
+ const modelOverride = typeof args['model'] === 'string' ? args['model'] : undefined;
494
+ const validationError = validateExploreRequest({ query, endpointHints, parentDepth: 0 }, session.exploreConfig);
495
+ if (validationError) {
496
+ return { result: { error: validationError } };
497
+ }
498
+ // Resolve model override against available models
499
+ const effectiveModel = resolveExploreModel(session.exploreConfig, modelOverride);
500
+ const { result, events, tokensUsed } = await runExploreAgent(session, query, endpointHints, 0, parentToolId, signal, effectiveModel);
501
+ return {
502
+ result,
503
+ subagentEvents: events,
504
+ exploreResult: { summary: result.output ?? '', tokensUsed },
505
+ };
506
+ }
507
+ // ---------------------------------------------------------------------------
508
+ // Explore sub-agent
509
+ // ---------------------------------------------------------------------------
510
+ async function runExploreAgent(session, query, endpointHints, parentDepth, parentToolId, signal, modelOverride) {
511
+ const config = session.exploreConfig;
512
+ const events = [];
513
+ let tokensUsed = 0;
514
+ const effectiveModel = modelOverride ?? config.model;
515
+ let provider;
516
+ try {
517
+ provider = new FailoverProvider(effectiveModel);
518
+ }
519
+ catch (err) {
520
+ const errMsg = err instanceof Error ? err.message : String(err);
521
+ events.push({
522
+ type: SSEEventType.SubagentEvent,
523
+ parent_tool_id: parentToolId,
524
+ agent_name: 'explore',
525
+ event_type: 'error',
526
+ error: `Provider init failed: ${errMsg}`,
527
+ timestamp: ts(),
528
+ });
529
+ return { result: { error: `Explore provider failed: ${errMsg}` }, events, tokensUsed };
530
+ }
531
+ // Build sub-agent tools: always request (read-only)
532
+ const subTools = [{
533
+ name: 'request',
534
+ description: 'Make HTTP requests to connected systems (read-only).',
535
+ parameters: {
536
+ type: 'object',
537
+ properties: {
538
+ connection: { type: 'string', description: 'Connection name' },
539
+ method: { type: 'string', enum: ['GET'] },
540
+ endpoint: { type: 'string', description: 'API endpoint path' },
541
+ params: { type: 'object', description: 'Query parameters' },
542
+ intent: { type: 'string', enum: ['read'] },
543
+ },
544
+ required: ['connection', 'method', 'endpoint', 'intent'],
545
+ },
546
+ }];
547
+ // Allow nested explore if depth permits
548
+ if (parentDepth + 1 < config.maxDepth) {
549
+ subTools.push({
550
+ name: EXPLORE_TOOL_NAME,
551
+ description: EXPLORE_TOOL_SCHEMA.description,
552
+ parameters: {
553
+ type: 'object',
554
+ properties: {
555
+ query: { type: 'string', description: 'What to investigate' },
556
+ endpoint_hints: {
557
+ type: 'array',
558
+ items: { type: 'string' },
559
+ description: 'Optional endpoint paths to prioritize',
560
+ },
561
+ model: {
562
+ type: 'string',
563
+ description: 'Optional: "simple", "default", "complex", or "provider:model"',
564
+ },
565
+ },
566
+ required: ['query'],
567
+ },
568
+ });
569
+ }
570
+ // Build initial message
571
+ const hintsStr = endpointHints?.length ? `\nEndpoint hints: ${endpointHints.join(', ')}` : '';
572
+ const conversation = [
573
+ { role: 'user', content: `Investigate: ${query}${hintsStr}` },
574
+ ];
575
+ let summaryParts = [];
576
+ for (let turn = 0; turn < config.maxTurns; turn++) {
577
+ if (signal.aborted) {
578
+ events.push({
579
+ type: SSEEventType.SubagentEvent,
580
+ parent_tool_id: parentToolId,
581
+ agent_name: 'explore',
582
+ event_type: 'error',
583
+ error: 'Aborted',
584
+ timestamp: ts(),
585
+ });
586
+ return { result: { error: 'Explore sub-agent aborted' }, events, tokensUsed };
587
+ }
588
+ let response;
589
+ try {
590
+ response = await provider.chat({
591
+ model: effectiveModel.model,
592
+ systemPrompt: config.systemPrompt,
593
+ messages: conversation,
594
+ tools: subTools,
595
+ maxTokens: 4096,
596
+ signal,
597
+ });
598
+ }
599
+ catch (err) {
600
+ const errMsg = err instanceof Error ? err.message : String(err);
601
+ events.push({
602
+ type: SSEEventType.SubagentEvent,
603
+ parent_tool_id: parentToolId,
604
+ agent_name: 'explore',
605
+ event_type: 'error',
606
+ error: errMsg,
607
+ timestamp: ts(),
608
+ });
609
+ return { result: { error: `Explore LLM error: ${errMsg}` }, events, tokensUsed };
610
+ }
611
+ if (response.usage) {
612
+ tokensUsed += response.usage.inputTokens + response.usage.outputTokens;
613
+ }
614
+ let hasToolUse = false;
615
+ const toolResults = [];
616
+ for (const block of response.content) {
617
+ if (block.type === 'text') {
618
+ summaryParts.push(block.text);
619
+ events.push({
620
+ type: SSEEventType.SubagentEvent,
621
+ parent_tool_id: parentToolId,
622
+ agent_name: 'explore',
623
+ event_type: 'thought',
624
+ text: block.text,
625
+ timestamp: ts(),
626
+ });
627
+ }
628
+ else if (block.type === 'tool_use') {
629
+ hasToolUse = true;
630
+ events.push({
631
+ type: SSEEventType.SubagentEvent,
632
+ parent_tool_id: parentToolId,
633
+ agent_name: 'explore',
634
+ event_type: 'tool_call_start',
635
+ tool_name: block.name,
636
+ tool_args: block.input,
637
+ timestamp: ts(),
638
+ });
639
+ let toolResult;
640
+ if (block.name === 'request') {
641
+ toolResult = await executeSubAgentRequest(session, block.input, signal);
642
+ }
643
+ else if (block.name === EXPLORE_TOOL_NAME) {
644
+ // Nested explore
645
+ const nestedQuery = String(block.input['query'] ?? '');
646
+ const nestedHints = Array.isArray(block.input['endpoint_hints'])
647
+ ? block.input['endpoint_hints'].filter((h) => typeof h === 'string')
648
+ : undefined;
649
+ const nestedModelParam = typeof block.input['model'] === 'string' ? block.input['model'] : undefined;
650
+ const nestedModel = resolveExploreModel(config, nestedModelParam);
651
+ const nested = await runExploreAgent(session, nestedQuery, nestedHints, parentDepth + 1, parentToolId, signal, nestedModel);
652
+ events.push(...nested.events);
653
+ tokensUsed += nested.tokensUsed;
654
+ toolResult = nested.result;
655
+ }
656
+ else {
657
+ toolResult = { error: `Unknown tool in explore: ${block.name}` };
658
+ }
659
+ events.push({
660
+ type: SSEEventType.SubagentEvent,
661
+ parent_tool_id: parentToolId,
662
+ agent_name: 'explore',
663
+ event_type: 'tool_call_end',
664
+ tool_name: block.name,
665
+ result: toolResult.error ?? toolResult.output,
666
+ timestamp: ts(),
667
+ });
668
+ toolResults.push({
669
+ role: 'tool_result',
670
+ toolCallId: block.id,
671
+ content: toolResult.error ?? toolResult.output ?? '',
672
+ isError: !!toolResult.error,
673
+ });
674
+ }
675
+ }
676
+ conversation.push({ role: 'assistant', content: response.content });
677
+ if (hasToolUse && toolResults.length > 0) {
678
+ conversation.push(...toolResults);
679
+ summaryParts = []; // Reset — final text is the real summary
680
+ continue;
681
+ }
682
+ // No tool use — sub-agent is done
683
+ break;
684
+ }
685
+ const summary = summaryParts.join('\n').trim() || 'No findings.';
686
+ events.push({
687
+ type: SSEEventType.SubagentEvent,
688
+ parent_tool_id: parentToolId,
689
+ agent_name: 'explore',
690
+ event_type: 'complete',
691
+ result: summary,
692
+ timestamp: ts(),
693
+ });
694
+ return { result: { output: summary }, events, tokensUsed };
695
+ }
696
+ async function executeSubAgentRequest(session, args, signal) {
697
+ // Force read-only intent
698
+ const readOnlyArgs = { ...args, intent: 'read', method: 'GET' };
699
+ return executeRequestTool(session, readOnlyArgs, signal);
700
+ }
701
+ function executePlanModeEnter(session, args) {
702
+ const reason = typeof args['reason'] === 'string' ? args['reason'] : undefined;
703
+ session.planModeManager.enter(reason);
704
+ return Promise.resolve({ output: 'Plan mode activated. Present your plan for approval.' });
705
+ }
706
+ function executePlanModeExit(session) {
707
+ session.planModeManager.exit();
708
+ return Promise.resolve({ output: 'Plan mode deactivated.' });
709
+ }
710
+ /**
711
+ * Process a streaming LLM response, yielding SSE events and accumulating
712
+ * the complete response for conversation history.
713
+ */
714
+ async function* processStream(stream, session, signal) {
715
+ const content = [];
716
+ const toolResults = [];
717
+ let hasToolUse = false;
718
+ // Accumulate text and tool call data from stream events
719
+ let currentText = '';
720
+ const toolInputBuffers = new Map();
721
+ for await (const event of stream) {
722
+ switch (event.type) {
723
+ case 'text_delta': {
724
+ currentText += event.text;
725
+ const processed = processTextOutput(session, event.text);
726
+ yield { type: SSEEventType.TextDelta, content: processed, timestamp: ts() };
727
+ break;
728
+ }
729
+ case 'tool_use_start':
730
+ hasToolUse = true;
731
+ toolInputBuffers.set(event.id, { name: event.name, json: '' });
732
+ yield {
733
+ type: SSEEventType.ToolCallStart,
734
+ tool_name: event.name,
735
+ tool_id: event.id,
736
+ parameters: {},
737
+ timestamp: ts(),
738
+ };
739
+ break;
740
+ case 'tool_use_delta': {
741
+ const buf = toolInputBuffers.get(event.id);
742
+ if (buf) {
743
+ buf.json += event.inputDelta;
744
+ }
745
+ break;
746
+ }
747
+ case 'tool_use_end': {
748
+ // Flush any accumulated text
749
+ if (currentText) {
750
+ content.push({ type: 'text', text: currentText });
751
+ currentText = '';
752
+ }
753
+ const toolName = toolInputBuffers.get(event.id)?.name ?? '';
754
+ content.push({ type: 'tool_use', id: event.id, name: toolName, input: event.input });
755
+ toolInputBuffers.delete(event.id);
756
+ // Emit ExploreStart before execution
757
+ if (toolName === EXPLORE_TOOL_NAME) {
758
+ yield {
759
+ type: SSEEventType.ExploreStart,
760
+ query: String(event.input['query'] ?? ''),
761
+ timestamp: ts(),
762
+ };
763
+ }
764
+ // Execute the tool
765
+ const startMs = Date.now();
766
+ const execResult = await executeTool(session, toolName, event.input, event.id, signal);
767
+ const durationMs = Date.now() - startMs;
768
+ // Emit subagent events from explore
769
+ if (execResult.subagentEvents) {
770
+ for (const evt of execResult.subagentEvents) {
771
+ yield evt;
772
+ }
773
+ }
774
+ // Emit ExploreEnd after execution
775
+ if (toolName === EXPLORE_TOOL_NAME && execResult.exploreResult) {
776
+ yield {
777
+ type: SSEEventType.ExploreEnd,
778
+ summary: execResult.exploreResult.summary,
779
+ tokens_used: execResult.exploreResult.tokensUsed,
780
+ timestamp: ts(),
781
+ };
782
+ }
783
+ yield {
784
+ type: SSEEventType.ToolCallResult,
785
+ tool_id: event.id,
786
+ status: execResult.result.error ? 'error' : 'success',
787
+ result: execResult.result.output,
788
+ error: execResult.result.error,
789
+ duration_ms: durationMs,
790
+ timestamp: ts(),
791
+ };
792
+ toolResults.push({
793
+ role: 'tool_result',
794
+ toolCallId: event.id,
795
+ content: execResult.result.error ?? execResult.result.output ?? '',
796
+ isError: !!execResult.result.error,
797
+ });
798
+ break;
799
+ }
800
+ case 'message_end':
801
+ // Flush any remaining text
802
+ if (currentText) {
803
+ content.push({ type: 'text', text: currentText });
804
+ currentText = '';
805
+ }
806
+ break;
807
+ default:
808
+ break;
809
+ }
810
+ }
811
+ // Flush any remaining text not flushed by message_end
812
+ if (currentText) {
813
+ content.push({ type: 'text', text: currentText });
814
+ }
815
+ return { content, hasToolUse, toolResults };
816
+ }
817
+ function processTextOutput(session, text) {
818
+ const result = session.runtime.outputPipeline.process(text);
819
+ session.runtime.telemetry.logGuard({
820
+ output: result.output,
821
+ modified: result.modified,
822
+ blocked: result.blocked,
823
+ findings: result.findings,
824
+ });
825
+ return result.output;
826
+ }
827
+ //# sourceMappingURL=agent-runner.js.map