groundswell 0.0.3 → 1.0.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 (292) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +26 -9
  3. package/dist/cache/cache-key.d.ts +20 -0
  4. package/dist/cache/cache-key.d.ts.map +1 -1
  5. package/dist/cache/cache-key.js +9 -0
  6. package/dist/cache/cache-key.js.map +1 -1
  7. package/dist/core/agent.d.ts +120 -29
  8. package/dist/core/agent.d.ts.map +1 -1
  9. package/dist/core/agent.js +584 -177
  10. package/dist/core/agent.js.map +1 -1
  11. package/dist/core/mcp-handler.d.ts +63 -5
  12. package/dist/core/mcp-handler.d.ts.map +1 -1
  13. package/dist/core/mcp-handler.js +184 -4
  14. package/dist/core/mcp-handler.js.map +1 -1
  15. package/dist/core/workflow-context.d.ts +6 -2
  16. package/dist/core/workflow-context.d.ts.map +1 -1
  17. package/dist/core/workflow-context.js +99 -4
  18. package/dist/core/workflow-context.js.map +1 -1
  19. package/dist/core/workflow.d.ts +315 -13
  20. package/dist/core/workflow.d.ts.map +1 -1
  21. package/dist/core/workflow.js +552 -30
  22. package/dist/core/workflow.js.map +1 -1
  23. package/dist/debugger/event-replayer.d.ts +422 -0
  24. package/dist/debugger/event-replayer.d.ts.map +1 -0
  25. package/dist/debugger/event-replayer.js +639 -0
  26. package/dist/debugger/event-replayer.js.map +1 -0
  27. package/dist/debugger/tree-debugger.d.ts +170 -1
  28. package/dist/debugger/tree-debugger.d.ts.map +1 -1
  29. package/dist/debugger/tree-debugger.js +423 -1
  30. package/dist/debugger/tree-debugger.js.map +1 -1
  31. package/dist/decorators/step.d.ts.map +1 -1
  32. package/dist/decorators/step.js +129 -47
  33. package/dist/decorators/step.js.map +1 -1
  34. package/dist/harnesses/claude-code-harness.d.ts +391 -0
  35. package/dist/harnesses/claude-code-harness.d.ts.map +1 -0
  36. package/dist/harnesses/claude-code-harness.js +1076 -0
  37. package/dist/harnesses/claude-code-harness.js.map +1 -0
  38. package/dist/harnesses/harness-registry.d.ts +440 -0
  39. package/dist/harnesses/harness-registry.d.ts.map +1 -0
  40. package/dist/harnesses/harness-registry.js +543 -0
  41. package/dist/harnesses/harness-registry.js.map +1 -0
  42. package/dist/harnesses/index.d.ts +12 -0
  43. package/dist/harnesses/index.d.ts.map +1 -0
  44. package/dist/harnesses/index.js +11 -0
  45. package/dist/harnesses/index.js.map +1 -0
  46. package/dist/harnesses/pi-harness.d.ts +219 -0
  47. package/dist/harnesses/pi-harness.d.ts.map +1 -0
  48. package/dist/harnesses/pi-harness.js +676 -0
  49. package/dist/harnesses/pi-harness.js.map +1 -0
  50. package/dist/harnesses/pi-schema-converter.d.ts +24 -0
  51. package/dist/harnesses/pi-schema-converter.d.ts.map +1 -0
  52. package/dist/harnesses/pi-schema-converter.js +81 -0
  53. package/dist/harnesses/pi-schema-converter.js.map +1 -0
  54. package/dist/harnesses/register-defaults.d.ts +24 -0
  55. package/dist/harnesses/register-defaults.d.ts.map +1 -0
  56. package/dist/harnesses/register-defaults.js +40 -0
  57. package/dist/harnesses/register-defaults.js.map +1 -0
  58. package/dist/harnesses/session-store.d.ts +201 -0
  59. package/dist/harnesses/session-store.d.ts.map +1 -0
  60. package/dist/harnesses/session-store.js +254 -0
  61. package/dist/harnesses/session-store.js.map +1 -0
  62. package/dist/index.d.ts +12 -2
  63. package/dist/index.d.ts.map +1 -1
  64. package/dist/index.js +17 -0
  65. package/dist/index.js.map +1 -1
  66. package/dist/reflection/reflection.d.ts.map +1 -1
  67. package/dist/reflection/reflection.js +19 -4
  68. package/dist/reflection/reflection.js.map +1 -1
  69. package/dist/types/agent.d.ts +1253 -2
  70. package/dist/types/agent.d.ts.map +1 -1
  71. package/dist/types/agent.js +418 -1
  72. package/dist/types/agent.js.map +1 -1
  73. package/dist/types/decorators.d.ts +10 -1
  74. package/dist/types/decorators.d.ts.map +1 -1
  75. package/dist/types/events.d.ts +26 -0
  76. package/dist/types/events.d.ts.map +1 -1
  77. package/dist/types/harnesses.d.ts +474 -0
  78. package/dist/types/harnesses.d.ts.map +1 -0
  79. package/dist/types/harnesses.js +2 -0
  80. package/dist/types/harnesses.js.map +1 -0
  81. package/dist/types/index.d.ts +9 -1
  82. package/dist/types/index.d.ts.map +1 -1
  83. package/dist/types/index.js +6 -0
  84. package/dist/types/index.js.map +1 -1
  85. package/dist/types/providers.d.ts +691 -0
  86. package/dist/types/providers.d.ts.map +1 -0
  87. package/dist/types/providers.js +14 -0
  88. package/dist/types/providers.js.map +1 -0
  89. package/dist/types/restart.d.ts +132 -0
  90. package/dist/types/restart.d.ts.map +1 -0
  91. package/dist/types/restart.js +2 -0
  92. package/dist/types/restart.js.map +1 -0
  93. package/dist/types/streaming.d.ts +194 -0
  94. package/dist/types/streaming.d.ts.map +1 -0
  95. package/dist/types/streaming.js +67 -0
  96. package/dist/types/streaming.js.map +1 -0
  97. package/dist/types/workflow-context.d.ts +137 -1
  98. package/dist/types/workflow-context.d.ts.map +1 -1
  99. package/dist/utils/agent-validation.d.ts +88 -0
  100. package/dist/utils/agent-validation.d.ts.map +1 -0
  101. package/dist/utils/agent-validation.js +87 -0
  102. package/dist/utils/agent-validation.js.map +1 -0
  103. package/dist/utils/delay.d.ts +7 -0
  104. package/dist/utils/delay.d.ts.map +1 -0
  105. package/dist/utils/delay.js +9 -0
  106. package/dist/utils/delay.js.map +1 -0
  107. package/dist/utils/harness-config.d.ts +180 -0
  108. package/dist/utils/harness-config.d.ts.map +1 -0
  109. package/dist/utils/harness-config.js +311 -0
  110. package/dist/utils/harness-config.js.map +1 -0
  111. package/dist/utils/index.d.ts +9 -1
  112. package/dist/utils/index.d.ts.map +1 -1
  113. package/dist/utils/index.js +8 -1
  114. package/dist/utils/index.js.map +1 -1
  115. package/dist/utils/model-spec.d.ts +110 -0
  116. package/dist/utils/model-spec.d.ts.map +1 -0
  117. package/dist/utils/model-spec.js +149 -0
  118. package/dist/utils/model-spec.js.map +1 -0
  119. package/dist/utils/provider-config.d.ts +10 -0
  120. package/dist/utils/provider-config.d.ts.map +1 -0
  121. package/dist/utils/provider-config.js +10 -0
  122. package/dist/utils/provider-config.js.map +1 -0
  123. package/dist/utils/restart-analysis.d.ts +202 -0
  124. package/dist/utils/restart-analysis.d.ts.map +1 -0
  125. package/dist/utils/restart-analysis.js +426 -0
  126. package/dist/utils/restart-analysis.js.map +1 -0
  127. package/dist/utils/session-serialization.d.ts +118 -0
  128. package/dist/utils/session-serialization.d.ts.map +1 -0
  129. package/dist/utils/session-serialization.js +217 -0
  130. package/dist/utils/session-serialization.js.map +1 -0
  131. package/package.json +31 -5
  132. package/CHANGELOG.md +0 -188
  133. package/dist/__tests__/adversarial/attachChild-performance.test.d.ts +0 -16
  134. package/dist/__tests__/adversarial/attachChild-performance.test.d.ts.map +0 -1
  135. package/dist/__tests__/adversarial/attachChild-performance.test.js +0 -187
  136. package/dist/__tests__/adversarial/attachChild-performance.test.js.map +0 -1
  137. package/dist/__tests__/adversarial/circular-reference.test.d.ts +0 -13
  138. package/dist/__tests__/adversarial/circular-reference.test.d.ts.map +0 -1
  139. package/dist/__tests__/adversarial/circular-reference.test.js +0 -92
  140. package/dist/__tests__/adversarial/circular-reference.test.js.map +0 -1
  141. package/dist/__tests__/adversarial/complex-circular-reference.test.d.ts +0 -16
  142. package/dist/__tests__/adversarial/complex-circular-reference.test.d.ts.map +0 -1
  143. package/dist/__tests__/adversarial/complex-circular-reference.test.js +0 -127
  144. package/dist/__tests__/adversarial/complex-circular-reference.test.js.map +0 -1
  145. package/dist/__tests__/adversarial/concurrent-task-failures.test.d.ts +0 -21
  146. package/dist/__tests__/adversarial/concurrent-task-failures.test.d.ts.map +0 -1
  147. package/dist/__tests__/adversarial/concurrent-task-failures.test.js +0 -667
  148. package/dist/__tests__/adversarial/concurrent-task-failures.test.js.map +0 -1
  149. package/dist/__tests__/adversarial/deep-analysis.test.d.ts +0 -6
  150. package/dist/__tests__/adversarial/deep-analysis.test.d.ts.map +0 -1
  151. package/dist/__tests__/adversarial/deep-analysis.test.js +0 -877
  152. package/dist/__tests__/adversarial/deep-analysis.test.js.map +0 -1
  153. package/dist/__tests__/adversarial/deep-hierarchy-stress.test.d.ts +0 -13
  154. package/dist/__tests__/adversarial/deep-hierarchy-stress.test.d.ts.map +0 -1
  155. package/dist/__tests__/adversarial/deep-hierarchy-stress.test.js +0 -186
  156. package/dist/__tests__/adversarial/deep-hierarchy-stress.test.js.map +0 -1
  157. package/dist/__tests__/adversarial/e2e-prd-validation.test.d.ts +0 -6
  158. package/dist/__tests__/adversarial/e2e-prd-validation.test.d.ts.map +0 -1
  159. package/dist/__tests__/adversarial/e2e-prd-validation.test.js +0 -626
  160. package/dist/__tests__/adversarial/e2e-prd-validation.test.js.map +0 -1
  161. package/dist/__tests__/adversarial/edge-case.test.d.ts +0 -6
  162. package/dist/__tests__/adversarial/edge-case.test.d.ts.map +0 -1
  163. package/dist/__tests__/adversarial/edge-case.test.js +0 -857
  164. package/dist/__tests__/adversarial/edge-case.test.js.map +0 -1
  165. package/dist/__tests__/adversarial/error-merge-strategy.test.d.ts +0 -20
  166. package/dist/__tests__/adversarial/error-merge-strategy.test.d.ts.map +0 -1
  167. package/dist/__tests__/adversarial/error-merge-strategy.test.js +0 -907
  168. package/dist/__tests__/adversarial/error-merge-strategy.test.js.map +0 -1
  169. package/dist/__tests__/adversarial/incremental-performance.test.d.ts +0 -2
  170. package/dist/__tests__/adversarial/incremental-performance.test.d.ts.map +0 -1
  171. package/dist/__tests__/adversarial/incremental-performance.test.js +0 -113
  172. package/dist/__tests__/adversarial/incremental-performance.test.js.map +0 -1
  173. package/dist/__tests__/adversarial/node-map-update-benchmarks.test.d.ts +0 -22
  174. package/dist/__tests__/adversarial/node-map-update-benchmarks.test.d.ts.map +0 -1
  175. package/dist/__tests__/adversarial/node-map-update-benchmarks.test.js +0 -383
  176. package/dist/__tests__/adversarial/node-map-update-benchmarks.test.js.map +0 -1
  177. package/dist/__tests__/adversarial/observer-propagation.test.d.ts +0 -21
  178. package/dist/__tests__/adversarial/observer-propagation.test.d.ts.map +0 -1
  179. package/dist/__tests__/adversarial/observer-propagation.test.js +0 -404
  180. package/dist/__tests__/adversarial/observer-propagation.test.js.map +0 -1
  181. package/dist/__tests__/adversarial/parent-validation.test.d.ts +0 -13
  182. package/dist/__tests__/adversarial/parent-validation.test.d.ts.map +0 -1
  183. package/dist/__tests__/adversarial/parent-validation.test.js +0 -128
  184. package/dist/__tests__/adversarial/parent-validation.test.js.map +0 -1
  185. package/dist/__tests__/adversarial/prd-12-2-compliance.test.d.ts +0 -20
  186. package/dist/__tests__/adversarial/prd-12-2-compliance.test.d.ts.map +0 -1
  187. package/dist/__tests__/adversarial/prd-12-2-compliance.test.js +0 -482
  188. package/dist/__tests__/adversarial/prd-12-2-compliance.test.js.map +0 -1
  189. package/dist/__tests__/adversarial/prd-compliance.test.d.ts +0 -6
  190. package/dist/__tests__/adversarial/prd-compliance.test.d.ts.map +0 -1
  191. package/dist/__tests__/adversarial/prd-compliance.test.js +0 -886
  192. package/dist/__tests__/adversarial/prd-compliance.test.js.map +0 -1
  193. package/dist/__tests__/compatibility/backward-compatibility.test.d.ts +0 -22
  194. package/dist/__tests__/compatibility/backward-compatibility.test.d.ts.map +0 -1
  195. package/dist/__tests__/compatibility/backward-compatibility.test.js +0 -1843
  196. package/dist/__tests__/compatibility/backward-compatibility.test.js.map +0 -1
  197. package/dist/__tests__/helpers/index.d.ts +0 -10
  198. package/dist/__tests__/helpers/index.d.ts.map +0 -1
  199. package/dist/__tests__/helpers/index.js +0 -10
  200. package/dist/__tests__/helpers/index.js.map +0 -1
  201. package/dist/__tests__/helpers/tree-verification.d.ts +0 -90
  202. package/dist/__tests__/helpers/tree-verification.d.ts.map +0 -1
  203. package/dist/__tests__/helpers/tree-verification.js +0 -202
  204. package/dist/__tests__/helpers/tree-verification.js.map +0 -1
  205. package/dist/__tests__/integration/agent-workflow.test.d.ts +0 -2
  206. package/dist/__tests__/integration/agent-workflow.test.d.ts.map +0 -1
  207. package/dist/__tests__/integration/agent-workflow.test.js +0 -256
  208. package/dist/__tests__/integration/agent-workflow.test.js.map +0 -1
  209. package/dist/__tests__/integration/bidirectional-consistency.test.d.ts +0 -14
  210. package/dist/__tests__/integration/bidirectional-consistency.test.d.ts.map +0 -1
  211. package/dist/__tests__/integration/bidirectional-consistency.test.js +0 -668
  212. package/dist/__tests__/integration/bidirectional-consistency.test.js.map +0 -1
  213. package/dist/__tests__/integration/observer-logging.test.d.ts +0 -2
  214. package/dist/__tests__/integration/observer-logging.test.d.ts.map +0 -1
  215. package/dist/__tests__/integration/observer-logging.test.js +0 -517
  216. package/dist/__tests__/integration/observer-logging.test.js.map +0 -1
  217. package/dist/__tests__/integration/tree-mirroring.test.d.ts +0 -2
  218. package/dist/__tests__/integration/tree-mirroring.test.d.ts.map +0 -1
  219. package/dist/__tests__/integration/tree-mirroring.test.js +0 -117
  220. package/dist/__tests__/integration/tree-mirroring.test.js.map +0 -1
  221. package/dist/__tests__/integration/workflow-reparenting.test.d.ts +0 -12
  222. package/dist/__tests__/integration/workflow-reparenting.test.d.ts.map +0 -1
  223. package/dist/__tests__/integration/workflow-reparenting.test.js +0 -239
  224. package/dist/__tests__/integration/workflow-reparenting.test.js.map +0 -1
  225. package/dist/__tests__/unit/agent.test.d.ts +0 -2
  226. package/dist/__tests__/unit/agent.test.d.ts.map +0 -1
  227. package/dist/__tests__/unit/agent.test.js +0 -143
  228. package/dist/__tests__/unit/agent.test.js.map +0 -1
  229. package/dist/__tests__/unit/cache-key.test.d.ts +0 -5
  230. package/dist/__tests__/unit/cache-key.test.d.ts.map +0 -1
  231. package/dist/__tests__/unit/cache-key.test.js +0 -145
  232. package/dist/__tests__/unit/cache-key.test.js.map +0 -1
  233. package/dist/__tests__/unit/cache.test.d.ts +0 -5
  234. package/dist/__tests__/unit/cache.test.d.ts.map +0 -1
  235. package/dist/__tests__/unit/cache.test.js +0 -132
  236. package/dist/__tests__/unit/cache.test.js.map +0 -1
  237. package/dist/__tests__/unit/context.test.d.ts +0 -2
  238. package/dist/__tests__/unit/context.test.d.ts.map +0 -1
  239. package/dist/__tests__/unit/context.test.js +0 -220
  240. package/dist/__tests__/unit/context.test.js.map +0 -1
  241. package/dist/__tests__/unit/decorators.test.d.ts +0 -2
  242. package/dist/__tests__/unit/decorators.test.d.ts.map +0 -1
  243. package/dist/__tests__/unit/decorators.test.js +0 -162
  244. package/dist/__tests__/unit/decorators.test.js.map +0 -1
  245. package/dist/__tests__/unit/introspection-tools.test.d.ts +0 -5
  246. package/dist/__tests__/unit/introspection-tools.test.d.ts.map +0 -1
  247. package/dist/__tests__/unit/introspection-tools.test.js +0 -191
  248. package/dist/__tests__/unit/introspection-tools.test.js.map +0 -1
  249. package/dist/__tests__/unit/logger.test.d.ts +0 -2
  250. package/dist/__tests__/unit/logger.test.d.ts.map +0 -1
  251. package/dist/__tests__/unit/logger.test.js +0 -241
  252. package/dist/__tests__/unit/logger.test.js.map +0 -1
  253. package/dist/__tests__/unit/observable.test.d.ts +0 -2
  254. package/dist/__tests__/unit/observable.test.d.ts.map +0 -1
  255. package/dist/__tests__/unit/observable.test.js +0 -251
  256. package/dist/__tests__/unit/observable.test.js.map +0 -1
  257. package/dist/__tests__/unit/prompt.test.d.ts +0 -2
  258. package/dist/__tests__/unit/prompt.test.d.ts.map +0 -1
  259. package/dist/__tests__/unit/prompt.test.js +0 -113
  260. package/dist/__tests__/unit/prompt.test.js.map +0 -1
  261. package/dist/__tests__/unit/reflection.test.d.ts +0 -5
  262. package/dist/__tests__/unit/reflection.test.d.ts.map +0 -1
  263. package/dist/__tests__/unit/reflection.test.js +0 -160
  264. package/dist/__tests__/unit/reflection.test.js.map +0 -1
  265. package/dist/__tests__/unit/tree-debugger-incremental.test.d.ts +0 -2
  266. package/dist/__tests__/unit/tree-debugger-incremental.test.d.ts.map +0 -1
  267. package/dist/__tests__/unit/tree-debugger-incremental.test.js +0 -136
  268. package/dist/__tests__/unit/tree-debugger-incremental.test.js.map +0 -1
  269. package/dist/__tests__/unit/tree-debugger.test.d.ts +0 -2
  270. package/dist/__tests__/unit/tree-debugger.test.d.ts.map +0 -1
  271. package/dist/__tests__/unit/tree-debugger.test.js +0 -69
  272. package/dist/__tests__/unit/tree-debugger.test.js.map +0 -1
  273. package/dist/__tests__/unit/utils/workflow-error-utils.test.d.ts +0 -2
  274. package/dist/__tests__/unit/utils/workflow-error-utils.test.d.ts.map +0 -1
  275. package/dist/__tests__/unit/utils/workflow-error-utils.test.js +0 -154
  276. package/dist/__tests__/unit/utils/workflow-error-utils.test.js.map +0 -1
  277. package/dist/__tests__/unit/workflow-detachChild.test.d.ts +0 -2
  278. package/dist/__tests__/unit/workflow-detachChild.test.d.ts.map +0 -1
  279. package/dist/__tests__/unit/workflow-detachChild.test.js +0 -76
  280. package/dist/__tests__/unit/workflow-detachChild.test.js.map +0 -1
  281. package/dist/__tests__/unit/workflow-emitEvent-childDetached.test.d.ts +0 -2
  282. package/dist/__tests__/unit/workflow-emitEvent-childDetached.test.d.ts.map +0 -1
  283. package/dist/__tests__/unit/workflow-emitEvent-childDetached.test.js +0 -122
  284. package/dist/__tests__/unit/workflow-emitEvent-childDetached.test.js.map +0 -1
  285. package/dist/__tests__/unit/workflow-isDescendantOf.test.d.ts +0 -2
  286. package/dist/__tests__/unit/workflow-isDescendantOf.test.d.ts.map +0 -1
  287. package/dist/__tests__/unit/workflow-isDescendantOf.test.js +0 -140
  288. package/dist/__tests__/unit/workflow-isDescendantOf.test.js.map +0 -1
  289. package/dist/__tests__/unit/workflow.test.d.ts +0 -2
  290. package/dist/__tests__/unit/workflow.test.d.ts.map +0 -1
  291. package/dist/__tests__/unit/workflow.test.js +0 -330
  292. package/dist/__tests__/unit/workflow.test.js.map +0 -1
@@ -1,16 +1,21 @@
1
1
  /**
2
- * Agent - Lightweight wrapper around Anthropic's Agent SDK
2
+ * Agent - Multi-provider agent for LLM prompt execution
3
3
  *
4
- * Agents execute prompts and manage tool invocation cycles.
5
- * All configuration properties map 1:1 to Anthropic SDK.
4
+ * Agents execute prompts via provider abstraction layer, supporting
5
+ * multiple LLM providers (Anthropic, Claude Code, etc.) with unified
6
+ * configuration cascade and tool delegation.
6
7
  */
7
- import Anthropic from '@anthropic-ai/sdk';
8
+ import { createSuccessResponse, createErrorResponse, } from '../types/index.js';
8
9
  import { MCPHandler } from './mcp-handler.js';
9
10
  import { generateId } from '../utils/id.js';
11
+ import { validateAgentResponse } from '../utils/agent-validation.js';
10
12
  import { getExecutionContext } from './context.js';
11
13
  import { generateCacheKey, defaultCache } from '../cache/index.js';
14
+ import { HarnessRegistry, registerDefaultHarnesses } from '../harnesses/index.js';
15
+ import { getGlobalHarnessConfig, resolveHarnessConfig } from '../utils/harness-config.js';
16
+ import { parseModelSpec } from '../utils/model-spec.js';
12
17
  /**
13
- * Agent class - executes prompts via Anthropic SDK
18
+ * Agent class - executes prompts via Anthropic Agent SDK
14
19
  */
15
20
  export class Agent {
16
21
  /** Unique identifier for this agent instance */
@@ -19,27 +24,55 @@ export class Agent {
19
24
  name;
20
25
  /** Stored configuration */
21
26
  config;
22
- /** Anthropic client instance */
23
- client;
24
27
  /** MCP handler for tool management */
25
28
  mcpHandler;
26
29
  /** Direct MCPHandler instances for delegated execution */
27
30
  mcpHandlers = [];
28
31
  /** Default model to use */
29
32
  model;
33
+ /** Harness to use for this agent (resolved at construction) */
34
+ harnessId;
35
+ /** Harness-specific options for this agent */
36
+ harnessOptions;
37
+ /** Harness instance from registry (resolved at construction) */
38
+ harness;
30
39
  /**
31
40
  * Create a new Agent instance
32
- * @param config Agent configuration
41
+ * @param config Agent configuration (default: { name: 'Agent', model: 'claude-sonnet-4-20250514' })
33
42
  */
34
43
  constructor(config = {}) {
35
44
  this.id = generateId();
36
45
  this.name = config.name ?? 'Agent';
37
46
  this.config = config;
38
47
  this.model = config.model ?? 'claude-sonnet-4-20250514';
39
- // Create Anthropic client
40
- this.client = new Anthropic({
41
- apiKey: process.env.ANTHROPIC_API_KEY,
42
- });
48
+ // Store harness configuration from AgentConfig (PRD §7.9).
49
+ // Backward-compat bridge: prefer the new `harness` field; fall back to the legacy `provider`
50
+ // field so existing callers (`new Agent({ provider: 'anthropic' })`) keep working during the
51
+ // v1.2 migration. The fallback + legacy global-config singleton are removed by T2 (P3.M1.T2)
52
+ // when executePrompt/stream + the test suite move to configureHarnesses/getGlobalHarnessConfig.
53
+ this.harnessId = config.harness ?? config.provider;
54
+ this.harnessOptions = config.harnessOptions ?? config.providerOptions;
55
+ // Resolve the effective harness via the configuration cascade (PRD §7.7).
56
+ // getGlobalHarnessConfig reads the correct singleton (default 'pi') written by configureHarnesses().
57
+ const globalConfig = getGlobalHarnessConfig();
58
+ const resolved = resolveHarnessConfig(globalConfig, this.harnessId, this.harnessOptions);
59
+ const effectiveHarness = resolved.harness;
60
+ // Fetch the harness instance from HarnessRegistry (the v1.2 rename of ProviderRegistry).
61
+ // The cast bridges the legacy Provider return type to the Harness contract — structurally
62
+ // identical at runtime; the cast exists only because Provider.id is a wider type than Harness.id.
63
+ const registry = HarnessRegistry.getInstance();
64
+ let harnessInstance = registry.get(effectiveHarness);
65
+ // Lazy auto-registration safety net (PRD §7.6 / Issue 4 h3.3): if the resolved harness is a
66
+ // built-in default ('pi' | 'claude-code') that isn't registered yet, materialize the defaults
67
+ // once. registerDefaultHarnesses is idempotent (has() guards) → never overwrites a test's mock.
68
+ if (!harnessInstance && (effectiveHarness === 'pi' || effectiveHarness === 'claude-code')) {
69
+ registerDefaultHarnesses(registry);
70
+ harnessInstance = registry.get(effectiveHarness);
71
+ }
72
+ if (!harnessInstance) {
73
+ throw new Error(`Harness '${effectiveHarness}' is not registered`);
74
+ }
75
+ this.harness = harnessInstance;
43
76
  // Initialize MCP handler
44
77
  this.mcpHandler = new MCPHandler();
45
78
  // Register MCP servers
@@ -55,30 +88,133 @@ export class Agent {
55
88
  }
56
89
  }
57
90
  }
91
+ /**
92
+ * Execute tool via MCPHandler delegation
93
+ *
94
+ * This method implements the ToolExecutor callback signature for provider
95
+ * integration. Providers delegate tool execution back to the Agent's
96
+ * MCPHandler, maintaining centralized tool management.
97
+ *
98
+ * Tool names use the serverName__toolName format (double underscore)
99
+ * created during MCP server registration. The full name is passed
100
+ * directly to MCPHandler without parsing.
101
+ *
102
+ * ## Tool Resolution Order
103
+ *
104
+ * 1. Delegated handlers (this.mcpHandlers[]) - Custom MCPHandler instances
105
+ * 2. Main handler (this.mcpHandler) - Primary tool registry
106
+ *
107
+ * ## Error Handling
108
+ *
109
+ * Tool errors are returned in ToolExecutionResult format with isError: true.
110
+ * The method never throws - errors are wrapped in result objects.
111
+ *
112
+ * @param req - Tool execution request with name (serverName__toolName) and input
113
+ * @returns Promise resolving to tool execution result with content and error flag
114
+ * @private
115
+ * @remarks
116
+ * Used internally by provider.execute() for tool delegation.
117
+ * Tool execution flow: Provider → Agent.toolExecutor → MCPHandler.executeTool()
118
+ */
119
+ async toolExecutor(req) {
120
+ try {
121
+ // Check delegated MCPHandlers first (preserve custom executors)
122
+ for (const handler of this.mcpHandlers) {
123
+ if (handler.hasTool(req.name)) {
124
+ const toolResult = await handler.executeTool(req.name, req.input);
125
+ return this.convertToToolExecutionResult(toolResult);
126
+ }
127
+ }
128
+ // Check main MCPHandler
129
+ if (this.mcpHandler.hasTool(req.name)) {
130
+ const toolResult = await this.mcpHandler.executeTool(req.name, req.input);
131
+ return this.convertToToolExecutionResult(toolResult);
132
+ }
133
+ // Tool not found in any handler
134
+ return {
135
+ content: `Tool '${req.name}' not found`,
136
+ isError: true,
137
+ };
138
+ }
139
+ catch (error) {
140
+ // Handle unexpected errors (defensive programming)
141
+ const message = error instanceof Error ? error.message : 'Unknown error';
142
+ return {
143
+ content: `Tool execution error: ${message}`,
144
+ isError: true,
145
+ };
146
+ }
147
+ }
148
+ /**
149
+ * Convert MCPHandler ToolResult to ToolExecutionResult
150
+ *
151
+ * Maps the MCPHandler's internal ToolResult format to the
152
+ * provider-facing ToolExecutionResult format.
153
+ *
154
+ * Tries to parse JSON strings back to objects for better usability.
155
+ *
156
+ * @param toolResult - Result from MCPHandler.executeTool()
157
+ * @returns ToolExecutionResult with content and isError flag
158
+ * @private
159
+ */
160
+ convertToToolExecutionResult(toolResult) {
161
+ let content = toolResult.content;
162
+ // If content is a string, try to parse it as JSON
163
+ // This restores objects that were stringified by MCPHandler.executeTool()
164
+ if (typeof content === 'string') {
165
+ try {
166
+ const parsed = JSON.parse(content);
167
+ // Only use parsed value if it's an object or array (not primitive)
168
+ if (typeof parsed === 'object' && parsed !== null) {
169
+ content = parsed;
170
+ }
171
+ }
172
+ catch {
173
+ // Content is not valid JSON, keep original string
174
+ }
175
+ }
176
+ return {
177
+ content,
178
+ isError: toolResult.is_error ?? false,
179
+ };
180
+ }
58
181
  /**
59
182
  * Execute a prompt and return validated response
60
- * @param prompt Prompt to execute
61
- * @param overrides Optional overrides for this execution
62
- * @returns Validated response of type T
183
+ * @param prompt Prompt to execute (required)
184
+ * @param overrides Optional overrides for this execution (default: undefined)
185
+ * @returns AgentResponse containing validated response or error
63
186
  */
64
187
  async prompt(prompt, overrides) {
65
- const result = await this.executePrompt(prompt, overrides);
66
- return result.data;
188
+ return this.executePrompt(prompt, overrides);
67
189
  }
68
190
  /**
69
191
  * Execute a prompt with full result metadata
70
192
  * @param prompt Prompt to execute
71
193
  * @param overrides Optional overrides for this execution
72
194
  * @returns Full result including metadata
195
+ * @deprecated Use prompt() which now returns AgentResponse with metadata
73
196
  */
74
197
  async promptWithMetadata(prompt, overrides) {
75
- return this.executePrompt(prompt, overrides);
198
+ const response = await this.executePrompt(prompt, overrides);
199
+ // Convert AgentResponse back to PromptResult for backward compatibility
200
+ if (response.status === 'error') {
201
+ throw new Error(response.error?.message ?? 'Unknown error');
202
+ }
203
+ return {
204
+ data: response.data, // Type assertion: data is T when status is not 'error'
205
+ usage: response.metadata.usage ?? { input_tokens: 0, output_tokens: 0 },
206
+ duration: response.metadata.duration ?? 0,
207
+ toolCalls: response.metadata.toolCalls ?? 0,
208
+ };
76
209
  }
77
210
  /**
78
211
  * Execute a prompt with reflection capabilities
79
- * @param prompt Prompt to execute
80
- * @param overrides Optional overrides for this execution
81
- * @returns Validated response of type T
212
+ * @param prompt Prompt to execute (required)
213
+ * @param overrides Optional overrides for this execution (default: undefined)
214
+ * @returns AgentResponse containing validated response or error
215
+ * @remarks Reflection follows opt-out pattern: enabled by default unless explicitly disabled.
216
+ * When reflection is enabled (prompt.enableReflection, overrides.enableReflection, or
217
+ * config.enableReflection), prefixes system prompt with reflection instructions.
82
218
  */
83
219
  async reflect(prompt, overrides) {
84
220
  // Add reflection system prefix if reflection is enabled
@@ -93,8 +229,210 @@ export class Agent {
93
229
  system: systemPrefix +
94
230
  (prompt.systemOverride ?? overrides?.system ?? this.config.system ?? ''),
95
231
  };
96
- const result = await this.executePrompt(prompt, effectiveOverrides);
97
- return result.data;
232
+ return this.executePrompt(prompt, effectiveOverrides);
233
+ }
234
+ /**
235
+ * Execute a prompt with streaming response
236
+ *
237
+ * Returns an AsyncStream that yields StreamEvent objects during execution.
238
+ * Enables real-time response generation with text deltas, tool calls, and metadata.
239
+ *
240
+ * @param prompt Prompt to execute
241
+ * @param overrides Optional overrides for this execution
242
+ * @returns AsyncStream with AsyncGenerator for for-await...of consumption
243
+ *
244
+ * @example
245
+ * ```ts
246
+ * const agent = new Agent({ provider: 'anthropic' });
247
+ * const prompt = new Prompt({ user: 'Tell me a story' });
248
+ *
249
+ * const streamResult = agent.stream(prompt);
250
+ *
251
+ * for await (const event of streamResult.stream) {
252
+ * switch (event.type) {
253
+ * case 'text_delta':
254
+ * process.stdout.write(event.delta);
255
+ * break;
256
+ * case 'tool_call_start':
257
+ * console.log(`Tool: ${event.name}`);
258
+ * break;
259
+ * case 'done':
260
+ * console.log('Complete!');
261
+ * break;
262
+ * case 'error':
263
+ * console.error('Error:', event.error.message);
264
+ * break;
265
+ * }
266
+ * }
267
+ * ```
268
+ */
269
+ stream(prompt, overrides) {
270
+ // Extract prompt-level harness overrides (PRD §7.7, §7.9).
271
+ // Backward-compat bridge: prefer the new `harness` field; fall back to the legacy `provider`
272
+ // field so existing callers (`agent.stream(p, { provider: 'claude-code' })`) keep working during
273
+ // the v1.2 migration. The fallback + legacy global-config singleton are removed once
274
+ // PromptOverrides + the test suite are fully on harness vocabulary (later lockstep milestone).
275
+ const promptHarness = overrides?.harness ?? overrides?.provider;
276
+ const promptHarnessOptions = overrides?.harnessOptions ?? overrides?.providerOptions;
277
+ // Resolve the effective harness via the configuration cascade (PRD §7.7): global → agent → prompt.
278
+ // getGlobalHarnessConfig reads the correct singleton (default 'pi') written by configureHarnesses().
279
+ const globalConfig = getGlobalHarnessConfig();
280
+ const { harness: resolvedHarness, options: resolvedHarnessOptions } = resolveHarnessConfig(globalConfig, this.harnessId, this.harnessOptions, promptHarness, promptHarnessOptions);
281
+ // Fetch the harness instance from HarnessRegistry (may differ from this.harness when a prompt
282
+ // override is supplied). The cast bridges the legacy Provider return type to the Harness contract
283
+ // — structurally identical at runtime; the cast exists only because Provider.id is wider than Harness.id.
284
+ const registry = HarnessRegistry.getInstance();
285
+ let harnessInstance = registry.get(resolvedHarness);
286
+ // Lazy auto-registration safety net (PRD §7.6 / Issue 4 h3.3): if the resolved harness is a
287
+ // built-in default ('pi' | 'claude-code') that isn't registered yet, materialize the defaults
288
+ // once. registerDefaultHarnesses is idempotent (has() guards) → never overwrites a test's mock.
289
+ if (!harnessInstance && (resolvedHarness === 'pi' || resolvedHarness === 'claude-code')) {
290
+ registerDefaultHarnesses(registry);
291
+ harnessInstance = registry.get(resolvedHarness);
292
+ }
293
+ if (!harnessInstance) {
294
+ // THROW (synchronous at call time, before the generator is created) — preserves the existing
295
+ // .rejects.toThrow(...) contract. Reworded to harness vocab; message still contains the id +
296
+ // 'is not registered' so the updated legacy-test regex still matches.
297
+ throw new Error(`Harness '${resolvedHarness}' is not registered`);
298
+ }
299
+ // Capture non-null harness instance for use in closure (TypeScript strict mode requirement)
300
+ const harness = harnessInstance;
301
+ // Merge configuration: Prompt > Overrides > Config
302
+ const effectiveSystem = prompt.systemOverride ?? overrides?.system ?? this.config.system;
303
+ const effectiveModel = overrides?.model ?? this.model;
304
+ const effectiveMaxTokens = overrides?.maxTokens ?? this.config.maxTokens ?? 4096;
305
+ const effectiveTemperature = overrides?.temperature ?? this.config.temperature;
306
+ const effectiveTools = this.mergeTools(prompt.toolsOverride ?? overrides?.tools ?? this.config.tools);
307
+ const effectiveHooks = this.mergeHooks(prompt.hooksOverride, overrides?.hooks, this.config.hooks);
308
+ // Build user message
309
+ const userMessage = prompt.buildUserMessage();
310
+ // Convert Agent.hooks to HarnessHookEvents
311
+ const harnessHooks = {};
312
+ if (effectiveHooks.preToolUse && effectiveHooks.preToolUse.length > 0) {
313
+ harnessHooks.onToolStart = async (tool) => {
314
+ for (const hook of effectiveHooks.preToolUse) {
315
+ await hook({
316
+ toolName: tool.name,
317
+ toolInput: tool.input,
318
+ agentId: this.id,
319
+ });
320
+ }
321
+ };
322
+ }
323
+ if (effectiveHooks.postToolUse && effectiveHooks.postToolUse.length > 0) {
324
+ harnessHooks.onToolEnd = async (tool, result, duration) => {
325
+ for (const hook of effectiveHooks.postToolUse) {
326
+ await hook({
327
+ toolName: tool.name,
328
+ toolInput: tool.input,
329
+ toolOutput: result.content,
330
+ agentId: this.id,
331
+ duration,
332
+ });
333
+ }
334
+ };
335
+ }
336
+ if (effectiveHooks.sessionStart && effectiveHooks.sessionStart.length > 0) {
337
+ harnessHooks.onSessionStart = async () => {
338
+ for (const hook of effectiveHooks.sessionStart) {
339
+ await hook({
340
+ agentId: this.id,
341
+ agentName: this.name,
342
+ });
343
+ }
344
+ };
345
+ }
346
+ if (effectiveHooks.sessionEnd && effectiveHooks.sessionEnd.length > 0) {
347
+ harnessHooks.onSessionEnd = async (totalDuration) => {
348
+ for (const hook of effectiveHooks.sessionEnd) {
349
+ await hook({
350
+ agentId: this.id,
351
+ agentName: this.name,
352
+ totalDuration,
353
+ });
354
+ }
355
+ };
356
+ }
357
+ // Create AbortController for cancellation support
358
+ const controller = new AbortController();
359
+ // Build HarnessRequest with streaming enabled (PRD §7.3, §7.4). Identical shape to the legacy
360
+ // ProviderRequest — the swap is a type rename (ProviderRequest = HarnessRequest alias).
361
+ // streaming: true flips Harness.execute into AsyncGenerator mode.
362
+ const harnessRequest = {
363
+ prompt: userMessage,
364
+ options: {
365
+ model: effectiveModel,
366
+ systemPrompt: effectiveSystem,
367
+ tools: effectiveTools,
368
+ sessionId: resolvedHarnessOptions.sessionId,
369
+ hooks: harnessHooks,
370
+ streaming: true, // CRITICAL: Enable streaming mode
371
+ },
372
+ };
373
+ // Create async generator that wraps harness streaming
374
+ const self = this;
375
+ async function* streamGenerator() {
376
+ try {
377
+ // Call harness with streaming enabled
378
+ // Harness returns: Promise<AgentResponse<T>> | AsyncGenerator<StreamEvent, AgentResponse<T>>
379
+ const harnessResult = harness.execute(harnessRequest, self.toolExecutor.bind(self), harnessHooks);
380
+ // Check if harness returned an AsyncGenerator (streaming mode) directly
381
+ if (Symbol.asyncIterator in harnessResult) {
382
+ // Harness is in streaming mode - iterate and yield events
383
+ const harnessStream = harnessResult;
384
+ let finalValue;
385
+ for await (const event of harnessStream) {
386
+ // Check for cancellation
387
+ if (controller.signal.aborted) {
388
+ yield {
389
+ type: 'error',
390
+ error: new Error('Stream cancelled'),
391
+ code: 'CANCELLED',
392
+ retryable: false,
393
+ };
394
+ // Cancellation: return error response
395
+ return createErrorResponse('CANCELLED', 'Stream cancelled by user', {}, false);
396
+ }
397
+ // Yield event from harness
398
+ yield event;
399
+ }
400
+ // After loop completes, the AsyncGenerator's return value is the final AgentResponse<T>
401
+ // We need to get it by calling next() one more time
402
+ const finalResult = await harnessStream.next();
403
+ // The value should be AgentResponse<T> when done=true, but TypeScript sees it as StreamEvent | AgentResponse<T>
404
+ finalValue = finalResult.value;
405
+ // Return the final response
406
+ return finalValue;
407
+ }
408
+ else {
409
+ // Provider returned a Promise<AgentResponse<T>> (non-streaming mode)
410
+ // This shouldn't happen with streaming: true, but handle it gracefully
411
+ const responsePromise = harnessResult;
412
+ const response = await responsePromise;
413
+ yield {
414
+ type: 'done',
415
+ finishReason: response.status === 'error' ? 'error' : 'stop',
416
+ };
417
+ return response;
418
+ }
419
+ }
420
+ catch (error) {
421
+ // Yield error event instead of throwing
422
+ yield {
423
+ type: 'error',
424
+ error: error instanceof Error ? error : new Error(String(error)),
425
+ code: 'STREAM_ERROR',
426
+ retryable: false,
427
+ };
428
+ // Return error response for AsyncGenerator completion
429
+ return createErrorResponse('STREAM_ERROR', error instanceof Error ? error.message : String(error), {}, false);
430
+ }
431
+ }
432
+ return {
433
+ stream: streamGenerator.call(this),
434
+ controller,
435
+ };
98
436
  }
99
437
  /**
100
438
  * Get the MCP handler for custom tool registration
@@ -112,14 +450,44 @@ export class Agent {
112
450
  }
113
451
  }
114
452
  /**
115
- * Internal prompt execution with full flow
453
+ * Internal prompt execution with full flow using provider abstraction
454
+ * @side effects May emit workflow events, may read from/write to cache if enabled,
455
+ * may modify environment variables temporarily, validates response against schema,
456
+ * and stores result in cache if enabled.
116
457
  */
117
458
  async executePrompt(prompt, overrides) {
118
459
  const startTime = Date.now();
119
- let toolCallCount = 0;
120
- let totalUsage = { input_tokens: 0, output_tokens: 0 };
460
+ const requestId = generateId();
121
461
  // Get execution context for event emission
122
462
  const ctx = getExecutionContext();
463
+ // Extract prompt-level harness overrides (PRD §7.7, §7.9).
464
+ // Backward-compat bridge: prefer the new `harness` field; fall back to the legacy `provider`
465
+ // field so existing callers (`agent.prompt(p, { provider: 'claude-code' })`) keep working during
466
+ // the v1.2 migration. The fallback + legacy global-config singleton are removed once
467
+ // PromptOverrides + the test suite are fully on harness vocabulary (later lockstep milestone).
468
+ const promptHarness = overrides?.harness ?? overrides?.provider;
469
+ const promptHarnessOptions = overrides?.harnessOptions ?? overrides?.providerOptions;
470
+ // Resolve the effective harness via the configuration cascade (PRD §7.7): global → agent → prompt.
471
+ // getGlobalHarnessConfig reads the correct singleton (default 'pi') written by configureHarnesses().
472
+ const globalConfig = getGlobalHarnessConfig();
473
+ const { harness: resolvedHarness, options: resolvedHarnessOptions } = resolveHarnessConfig(globalConfig, this.harnessId, this.harnessOptions, promptHarness, promptHarnessOptions);
474
+ // Fetch the harness instance from HarnessRegistry (may differ from this.harness when a prompt
475
+ // override is supplied). The cast bridges the legacy Provider return type to the Harness contract
476
+ // — structurally identical at runtime; the cast exists only because Provider.id is wider than Harness.id.
477
+ const registry = HarnessRegistry.getInstance();
478
+ let harnessInstance = registry.get(resolvedHarness);
479
+ // Lazy auto-registration safety net (PRD §7.6 / Issue 4 h3.3): if the resolved harness is a
480
+ // built-in default ('pi' | 'claude-code') that isn't registered yet, materialize the defaults
481
+ // once. registerDefaultHarnesses is idempotent (has() guards) → never overwrites a test's mock.
482
+ if (!harnessInstance && (resolvedHarness === 'pi' || resolvedHarness === 'claude-code')) {
483
+ registerDefaultHarnesses(registry);
484
+ harnessInstance = registry.get(resolvedHarness);
485
+ }
486
+ if (!harnessInstance) {
487
+ return createErrorResponse('PROVIDER_NOT_FOUND', `Harness '${resolvedHarness}' is not registered`, { harnessId: resolvedHarness }, false);
488
+ }
489
+ // Capture non-null harness instance for use in closure (TypeScript strict mode requirement)
490
+ const harness = harnessInstance;
123
491
  // Merge configuration: Prompt > Overrides > Config
124
492
  const effectiveSystem = prompt.systemOverride ?? overrides?.system ?? this.config.system;
125
493
  const effectiveModel = overrides?.model ?? this.model;
@@ -129,11 +497,20 @@ export class Agent {
129
497
  const cacheEnabled = this.config.enableCache && !overrides?.disableCache;
130
498
  let cacheKey;
131
499
  if (cacheEnabled) {
500
+ // PRD §7.14.5: isolate cache entries per (harness, provider, model).
501
+ // - harness: the resolved HarnessId (PRD §7.7 cascade, resolved above).
502
+ // - provider: the LLM host parsed from the effective model spec (PRD §7.8). Bare models
503
+ // resolve against the global defaultModelProvider (defaults to 'anthropic' when unset).
504
+ // NOTE: parseModelSpec throws on invalid model strings — intentional fail-fast.
505
+ const defaultModelProvider = getGlobalHarnessConfig().defaultModelProvider;
506
+ const modelSpec = parseModelSpec(effectiveModel, defaultModelProvider);
132
507
  const cacheInputs = {
133
508
  user: prompt.buildUserMessage(),
134
509
  data: prompt.getData(),
135
510
  system: effectiveSystem,
136
511
  model: effectiveModel,
512
+ harness: resolvedHarness, // PRD §7.14.5 — harness axis (ProviderId ⊃ HarnessId; cast safe)
513
+ provider: modelSpec.provider, // PRD §7.14.5 — LLM provider axis (from ModelSpec, §7.8)
137
514
  temperature: effectiveTemperature,
138
515
  maxTokens: effectiveMaxTokens,
139
516
  tools: this.config.tools,
@@ -143,7 +520,8 @@ export class Agent {
143
520
  };
144
521
  cacheKey = generateCacheKey(cacheInputs);
145
522
  const cached = await defaultCache.get(cacheKey);
146
- if (cached) {
523
+ if (cached && 'status' in cached) {
524
+ // New AgentResponse format - has 'status' field
147
525
  // Emit cache hit event
148
526
  if (ctx) {
149
527
  this.emitWorkflowEvent({
@@ -154,6 +532,7 @@ export class Agent {
154
532
  }
155
533
  return cached;
156
534
  }
535
+ // Old PromptResult format or undefined - re-execute
157
536
  // Emit cache miss event
158
537
  if (ctx) {
159
538
  this.emitWorkflowEvent({
@@ -175,91 +554,143 @@ export class Agent {
175
554
  }
176
555
  const effectiveTools = this.mergeTools(prompt.toolsOverride ?? overrides?.tools ?? this.config.tools);
177
556
  const effectiveHooks = this.mergeHooks(prompt.hooksOverride, overrides?.hooks, this.config.hooks);
178
- const effectiveStop = overrides?.stop;
179
557
  // Set up environment variables
180
558
  const originalEnv = this.setupEnvironment(overrides?.env ?? this.config.env);
181
559
  try {
182
- // Call session start hooks
183
- await this.callHooks(effectiveHooks?.sessionStart, {
184
- agentId: this.id,
185
- agentName: this.name,
186
- });
187
- // Build initial messages
188
- const messages = [
189
- { role: 'user', content: prompt.buildUserMessage() },
190
- ];
191
- // Execute conversation loop
192
- let response = await this.callApi(messages, effectiveSystem, effectiveTools, effectiveModel, effectiveMaxTokens, effectiveTemperature, effectiveStop);
193
- totalUsage = this.addUsage(totalUsage, response.usage);
194
- // Handle tool use loop
195
- while (response.stop_reason === 'tool_use') {
196
- const toolUseBlocks = response.content.filter((block) => block.type === 'tool_use');
197
- const toolResults = [];
198
- for (const toolUse of toolUseBlocks) {
199
- toolCallCount++;
200
- // Call pre-tool hooks
201
- await this.callHooks(effectiveHooks?.preToolUse, {
202
- toolName: toolUse.name,
203
- toolInput: toolUse.input,
204
- agentId: this.id,
205
- });
206
- const toolStartTime = Date.now();
207
- // Execute tool
208
- const result = await this.executeTool(toolUse.name, toolUse.input);
209
- const toolDuration = Date.now() - toolStartTime;
210
- // Emit tool invocation event if in workflow context
211
- if (ctx) {
212
- this.emitWorkflowEvent({
213
- type: 'toolInvocation',
214
- toolName: toolUse.name,
215
- input: toolUse.input,
216
- output: result,
217
- duration: toolDuration,
218
- node: ctx.workflowNode,
560
+ // Build user message
561
+ const userMessage = prompt.buildUserMessage();
562
+ // Convert AgentHooks → HarnessHookEvents (identical wiring, retyped).
563
+ const harnessHooks = {};
564
+ if (effectiveHooks.preToolUse && effectiveHooks.preToolUse.length > 0) {
565
+ harnessHooks.onToolStart = async (tool) => {
566
+ for (const hook of effectiveHooks.preToolUse) {
567
+ await hook({
568
+ toolName: tool.name,
569
+ toolInput: tool.input,
570
+ agentId: this.id,
571
+ });
572
+ }
573
+ };
574
+ }
575
+ if (effectiveHooks.postToolUse && effectiveHooks.postToolUse.length > 0) {
576
+ harnessHooks.onToolEnd = async (tool, result, duration) => {
577
+ for (const hook of effectiveHooks.postToolUse) {
578
+ await hook({
579
+ toolName: tool.name,
580
+ toolInput: tool.input,
581
+ toolOutput: result.content,
582
+ agentId: this.id,
583
+ duration,
584
+ });
585
+ }
586
+ };
587
+ }
588
+ if (effectiveHooks.sessionStart && effectiveHooks.sessionStart.length > 0) {
589
+ harnessHooks.onSessionStart = async () => {
590
+ for (const hook of effectiveHooks.sessionStart) {
591
+ await hook({
592
+ agentId: this.id,
593
+ agentName: this.name,
219
594
  });
220
595
  }
221
- // Call post-tool hooks
222
- await this.callHooks(effectiveHooks?.postToolUse, {
223
- toolName: toolUse.name,
224
- toolInput: toolUse.input,
225
- toolOutput: result,
596
+ };
597
+ }
598
+ if (effectiveHooks.sessionEnd && effectiveHooks.sessionEnd.length > 0) {
599
+ harnessHooks.onSessionEnd = async (totalDuration) => {
600
+ for (const hook of effectiveHooks.sessionEnd) {
601
+ await hook({
602
+ agentId: this.id,
603
+ agentName: this.name,
604
+ totalDuration,
605
+ });
606
+ }
607
+ };
608
+ }
609
+ // Build HarnessRequest with nested structure (PRD §7.3). Identical shape to the legacy
610
+ // ProviderRequest — the swap is a type rename (ProviderRequest = HarnessRequest alias).
611
+ const harnessRequest = {
612
+ prompt: userMessage,
613
+ options: {
614
+ model: effectiveModel,
615
+ systemPrompt: effectiveSystem,
616
+ tools: effectiveTools,
617
+ sessionId: resolvedHarnessOptions.sessionId,
618
+ hooks: harnessHooks,
619
+ },
620
+ };
621
+ // Execute via the Harness abstraction (PRD §7.3).
622
+ // Harness returns: Promise<AgentResponse<T>> | AsyncGenerator<StreamEvent, AgentResponse<T>>
623
+ // For non-streaming mode, it returns Promise<AgentResponse<T>>.
624
+ const harnessResult = harness.execute(harnessRequest, this.toolExecutor.bind(this), harnessHooks);
625
+ // Handle the union return type
626
+ const response = Symbol.asyncIterator in harnessResult
627
+ ? (await (async () => {
628
+ // Harness returned AsyncGenerator (shouldn't happen without streaming: true, but handle gracefully)
629
+ const generator = harnessResult;
630
+ // Consume all events
631
+ for await (const _event of generator) {
632
+ // Discard events, we just want the final response
633
+ }
634
+ const finalResult = await generator.next();
635
+ // The value should be AgentResponse<T> when done=true
636
+ return finalResult.value;
637
+ })())
638
+ : await harnessResult;
639
+ const duration = Date.now() - startTime;
640
+ // Handle error response from provider
641
+ if (response.status === 'error') {
642
+ // Emit prompt end event if in workflow context
643
+ if (ctx) {
644
+ this.emitWorkflowEvent({
645
+ type: 'agentPromptEnd',
226
646
  agentId: this.id,
227
- duration: toolDuration,
228
- });
229
- toolResults.push({
230
- type: 'tool_result',
231
- tool_use_id: toolUse.id,
232
- content: typeof result === 'string' ? result : JSON.stringify(result),
647
+ agentName: this.name,
648
+ promptId: prompt.id,
649
+ node: ctx.workflowNode,
650
+ duration,
233
651
  });
234
652
  }
235
- // Add assistant message with tool uses
236
- messages.push({ role: 'assistant', content: response.content });
237
- // Add tool results
238
- messages.push({ role: 'user', content: toolResults });
239
- // Continue conversation
240
- response = await this.callApi(messages, effectiveSystem, effectiveTools, effectiveModel, effectiveMaxTokens, effectiveTemperature, effectiveStop);
241
- totalUsage = this.addUsage(totalUsage, response.usage);
653
+ return response;
242
654
  }
243
- // Extract text response
244
- const textContent = response.content.find((block) => block.type === 'text');
245
- if (!textContent) {
246
- throw new Error('No text response received from API');
655
+ // Validate structured output if prompt has schema
656
+ let validatedResponse;
657
+ if (prompt.getResponseFormat()) {
658
+ const validationResult = prompt.safeValidateResponse(response.data);
659
+ if (validationResult.success) {
660
+ // Update metadata with agent ID instead of provider ID
661
+ const metadata = {
662
+ ...response.metadata,
663
+ agentId: this.id,
664
+ };
665
+ validatedResponse = createSuccessResponse(validationResult.data, metadata);
666
+ }
667
+ else {
668
+ const zodError = validationResult.error;
669
+ const errorSummary = zodError.errors
670
+ .map((err) => {
671
+ const field = err.path.length > 0 ? err.path.join('.') : 'response';
672
+ return `${field}: ${err.message}`;
673
+ })
674
+ .join('; ');
675
+ validatedResponse = createErrorResponse('VALIDATION_ERROR', `Response validation failed: ${errorSummary}`, {
676
+ validationErrors: zodError.errors.map((err) => ({
677
+ field: err.path.join('.') || 'root',
678
+ message: err.message,
679
+ code: err.code,
680
+ })),
681
+ }, false);
682
+ }
247
683
  }
248
- // Parse JSON from response
249
- const jsonMatch = textContent.text.match(/\{[\s\S]*\}/);
250
- if (!jsonMatch) {
251
- throw new Error('No JSON object found in response');
684
+ else {
685
+ // No validation schema - use provider response as-is
686
+ validatedResponse = {
687
+ ...response,
688
+ metadata: {
689
+ ...response.metadata,
690
+ agentId: this.id,
691
+ },
692
+ };
252
693
  }
253
- const parsed = JSON.parse(jsonMatch[0]);
254
- // Validate with schema
255
- const validated = prompt.validateResponse(parsed);
256
- // Call session end hooks
257
- await this.callHooks(effectiveHooks?.sessionEnd, {
258
- agentId: this.id,
259
- agentName: this.name,
260
- totalDuration: Date.now() - startTime,
261
- });
262
- const duration = Date.now() - startTime;
263
694
  // Emit prompt end event if in workflow context
264
695
  if (ctx) {
265
696
  this.emitWorkflowEvent({
@@ -269,20 +700,22 @@ export class Agent {
269
700
  promptId: prompt.id,
270
701
  node: ctx.workflowNode,
271
702
  duration,
272
- tokenUsage: totalUsage,
703
+ tokenUsage: validatedResponse.metadata.usage,
273
704
  });
274
705
  }
275
- const result = {
276
- data: validated,
277
- usage: totalUsage,
278
- duration,
279
- toolCalls: toolCallCount,
280
- };
706
+ // Validate before returning (defense-in-depth)
707
+ const finalResponse = this.validateResponse(validatedResponse, prompt.responseFormat);
281
708
  // Store in cache if enabled
282
709
  if (cacheEnabled && cacheKey) {
283
- await defaultCache.set(cacheKey, result, { prefix: this.id });
710
+ await defaultCache.set(cacheKey, finalResponse, { prefix: this.id });
284
711
  }
285
- return result;
712
+ return finalResponse;
713
+ }
714
+ catch (error) {
715
+ const duration = Date.now() - startTime;
716
+ const message = error instanceof Error ? error.message : 'Unknown error';
717
+ return createErrorResponse('PROVIDER_EXECUTION_FAILED', `Harness execution error: ${message}`, { duration, harnessId: resolvedHarness }, true // Provider errors are typically recoverable
718
+ );
286
719
  }
287
720
  finally {
288
721
  // Restore environment
@@ -290,56 +723,47 @@ export class Agent {
290
723
  }
291
724
  }
292
725
  /**
293
- * Call the Anthropic API
726
+ * Validates an AgentResponse against the schema before returning
727
+ *
728
+ * This provides defense-in-depth validation to ensure all returned responses
729
+ * conform to the AgentResponse schema, even if factory helpers have bugs.
730
+ *
731
+ * @template T - The type of response data
732
+ * @param response - The response to validate (required)
733
+ * @param dataSchema - The Zod schema for the response data (required from Prompt.responseFormat)
734
+ * @returns The validated response, or an INTERNAL_ERROR response if validation fails
735
+ *
736
+ * @private
294
737
  */
295
- async callApi(messages, system, tools, model, maxTokens, temperature, stop) {
296
- const params = {
297
- model,
298
- max_tokens: maxTokens,
299
- messages,
300
- };
301
- if (system) {
302
- params.system = system;
303
- }
304
- if (tools && tools.length > 0) {
305
- params.tools = tools.map((tool) => ({
306
- name: tool.name,
307
- description: tool.description,
308
- input_schema: tool.input_schema,
309
- }));
738
+ validateResponse(response, dataSchema) {
739
+ // Call shared utility for validation
740
+ const result = validateAgentResponse(response, dataSchema);
741
+ if (result.valid) {
742
+ // Response is valid, return it unchanged
743
+ return response;
310
744
  }
311
- if (temperature !== undefined) {
312
- params.temperature = temperature;
313
- }
314
- if (stop && stop.length > 0) {
315
- params.stop_sequences = stop;
316
- }
317
- return this.client.messages.create(params);
318
- }
319
- /**
320
- * Execute a tool (either direct or via MCP)
321
- */
322
- async executeTool(name, input) {
323
- // First, check stored MCPHandler instances (they have registered executors)
324
- for (const handler of this.mcpHandlers) {
325
- if (handler.hasTool(name)) {
326
- const result = await handler.executeTool(name, input);
327
- if (result.is_error) {
328
- throw new Error(result.content);
329
- }
330
- return result.content;
331
- }
332
- }
333
- // Fall back to main mcpHandler (for non-MCPHandler MCPServers)
334
- if (this.mcpHandler.hasTool(name)) {
335
- const result = await this.mcpHandler.executeTool(name, input);
336
- if (result.is_error) {
337
- throw new Error(result.content);
338
- }
339
- return result.content;
340
- }
341
- // Look for direct tool handler - this would be set by subclasses
342
- throw new Error(`No handler found for tool '${name}'`);
745
+ // Validation failed - this indicates a bug in our code
746
+ // Log detailed error information for debugging
747
+ console.error('Agent response validation failed', {
748
+ agentId: this.id, // Agent-specific logging (not in utility)
749
+ timestamp: Date.now(),
750
+ errorCount: result.errors?.errors.length ?? 0,
751
+ errors: result.errors?.errors.map((err) => ({
752
+ path: err.path.join('.'),
753
+ message: err.message,
754
+ code: err.code,
755
+ })) ?? [],
756
+ });
757
+ // Return INTERNAL_ERROR response
758
+ // Use createErrorResponse which is already imported
759
+ return createErrorResponse('INTERNAL_ERROR', 'Internal response validation failed', {
760
+ validationErrors: result.errors?.errors.map((err) => ({
761
+ path: err.path.join('.'),
762
+ message: err.message,
763
+ code: err.code,
764
+ })) ?? [],
765
+ }, false // Non-recoverable - indicates system bug
766
+ );
343
767
  }
344
768
  /**
345
769
  * Merge tools from config and MCP servers
@@ -377,18 +801,10 @@ export class Agent {
377
801
  ],
378
802
  };
379
803
  }
380
- /**
381
- * Call hooks of a specific type
382
- */
383
- async callHooks(hooks, context) {
384
- if (!hooks)
385
- return;
386
- for (const hook of hooks) {
387
- await hook(context);
388
- }
389
- }
390
804
  /**
391
805
  * Set up environment variables
806
+ * @side effects Modifies process.env with provided values and returns original values for restoration.
807
+ * Restores environment in finally block of executePrompt.
392
808
  */
393
809
  setupEnvironment(env) {
394
810
  if (!env)
@@ -413,14 +829,5 @@ export class Agent {
413
829
  }
414
830
  }
415
831
  }
416
- /**
417
- * Add token usage from response
418
- */
419
- addUsage(total, usage) {
420
- return {
421
- input_tokens: total.input_tokens + usage.input_tokens,
422
- output_tokens: total.output_tokens + usage.output_tokens,
423
- };
424
- }
425
832
  }
426
833
  //# sourceMappingURL=agent.js.map