@noetic-tools/core 0.0.1 → 0.1.1

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 (254) hide show
  1. package/README.md +31 -11
  2. package/dist/adapters/in-memory-fs-adapter.d.ts +1 -1
  3. package/dist/adapters/in-memory-fs-adapter.js +1 -1
  4. package/dist/adapters/in-memory-shell-adapter.d.ts +1 -1
  5. package/dist/adapters/in-memory-subprocess/manifest-persistence.d.ts +8 -0
  6. package/dist/adapters/in-memory-subprocess/manifest-persistence.js +56 -0
  7. package/dist/adapters/in-memory-subprocess/metadata.d.ts +12 -0
  8. package/dist/adapters/in-memory-subprocess/metadata.js +58 -0
  9. package/dist/adapters/in-memory-subprocess/step-completion.d.ts +3 -0
  10. package/dist/adapters/in-memory-subprocess/step-completion.js +86 -0
  11. package/dist/adapters/in-memory-subprocess/types.d.ts +88 -0
  12. package/dist/adapters/in-memory-subprocess/types.js +2 -0
  13. package/dist/adapters/in-memory-subprocess-adapter.d.ts +12 -4
  14. package/dist/adapters/in-memory-subprocess-adapter.js +164 -55
  15. package/dist/adapters/openrouter.d.ts +16 -7
  16. package/dist/adapters/openrouter.js +68 -7
  17. package/dist/builders/channel-builder.d.ts +1 -1
  18. package/dist/builders/control-flow-builders.d.ts +3 -3
  19. package/dist/builders/control-flow-builders.js +9 -4
  20. package/dist/builders/every.d.ts +3 -3
  21. package/dist/builders/every.js +5 -2
  22. package/dist/builders/layer-provides-builders.d.ts +1 -32
  23. package/dist/builders/layer-provides-builders.js +1 -33
  24. package/dist/builders/loop-builder.d.ts +2 -2
  25. package/dist/builders/loop-builder.js +5 -2
  26. package/dist/builders/memory-builder.d.ts +1 -1
  27. package/dist/builders/memory-builder.js +1 -1
  28. package/dist/builders/provide-builder.d.ts +2 -2
  29. package/dist/builders/provide-builder.js +5 -2
  30. package/dist/builders/spawn-builder.d.ts +22 -9
  31. package/dist/builders/spawn-builder.js +17 -3
  32. package/dist/builders/step-builders.d.ts +51 -30
  33. package/dist/builders/step-builders.js +31 -12
  34. package/dist/builders/tool-builder.d.ts +7 -3
  35. package/dist/builders/tool-builder.js +3 -1
  36. package/dist/builders/workflow-hydrator.d.ts +38 -0
  37. package/dist/builders/workflow-hydrator.js +336 -0
  38. package/dist/conditions/conditions.d.ts +4 -4
  39. package/dist/conditions/conditions.js +2 -2
  40. package/dist/errors/noetic-error.d.ts +1 -1
  41. package/dist/errors/noetic-error.js +2 -0
  42. package/dist/{runtime → harness}/agent-harness.d.ts +65 -45
  43. package/dist/harness/agent-harness.js +576 -0
  44. package/dist/harness/deps/adapters.d.ts +4 -0
  45. package/dist/harness/deps/adapters.js +4 -0
  46. package/dist/harness/deps/interpreter.d.ts +5 -0
  47. package/dist/harness/deps/interpreter.js +4 -0
  48. package/dist/harness/deps/memory.d.ts +5 -0
  49. package/dist/harness/deps/memory.js +4 -0
  50. package/dist/harness/deps/runtime.d.ts +10 -0
  51. package/dist/harness/deps/runtime.js +7 -0
  52. package/dist/harness/deps/types.d.ts +17 -0
  53. package/dist/harness/deps/types.js +1 -0
  54. package/dist/harness/model-call.d.ts +50 -0
  55. package/dist/harness/model-call.js +694 -0
  56. package/dist/harness/model-schema.d.ts +19 -0
  57. package/dist/harness/model-schema.js +46 -0
  58. package/dist/harness/step-bootstrap.d.ts +25 -0
  59. package/dist/harness/step-bootstrap.js +170 -0
  60. package/dist/index.d.ts +114 -76
  61. package/dist/index.js +74 -50
  62. package/dist/internal-test.d.ts +4 -4
  63. package/dist/internal-test.js +4 -4
  64. package/dist/interpreter/action-deps.d.ts +12 -0
  65. package/dist/interpreter/action-deps.js +10 -0
  66. package/dist/interpreter/action-types.d.ts +8 -0
  67. package/dist/interpreter/action-types.js +1 -0
  68. package/dist/interpreter/collect-tools.d.ts +2 -2
  69. package/dist/interpreter/collect-tools.js +6 -2
  70. package/dist/interpreter/execute-action.d.ts +22 -0
  71. package/dist/interpreter/execute-action.js +527 -0
  72. package/dist/interpreter/{execute-every.d.ts → execute-control.d.ts} +8 -2
  73. package/dist/interpreter/execute-control.js +530 -0
  74. package/dist/interpreter/execute.d.ts +16 -3
  75. package/dist/interpreter/execute.js +154 -51
  76. package/dist/interpreter/message-helpers.d.ts +1 -9
  77. package/dist/interpreter/message-helpers.js +1 -42
  78. package/dist/interpreter/step-dispatcher.d.ts +61 -0
  79. package/dist/interpreter/step-dispatcher.js +67 -0
  80. package/dist/interpreter/typeguards.d.ts +8 -15
  81. package/dist/interpreter/typeguards.js +4 -21
  82. package/dist/memory/budget.d.ts +2 -2
  83. package/dist/memory/budget.js +1 -1
  84. package/dist/{runtime → memory}/exec-context-factory.d.ts +2 -2
  85. package/dist/{runtime → memory}/exec-context-factory.js +1 -1
  86. package/dist/memory/flow-schema.d.ts +52 -0
  87. package/dist/memory/flow-schema.js +102 -0
  88. package/dist/memory/function-call-utils.d.ts +1 -1
  89. package/dist/memory/function-call-utils.js +4 -2
  90. package/dist/memory/layer-api.d.ts +4 -4
  91. package/dist/memory/layer-api.js +2 -2
  92. package/dist/memory/layer-lifecycle.d.ts +6 -6
  93. package/dist/memory/layer-lifecycle.js +6 -6
  94. package/dist/memory/layer-provides.d.ts +32 -0
  95. package/dist/memory/layer-provides.js +33 -0
  96. package/dist/memory/layer-usage.d.ts +4 -3
  97. package/dist/memory/layer-usage.js +9 -3
  98. package/dist/memory/layers/durable-task-state.d.ts +8 -8
  99. package/dist/memory/layers/durable-task-state.js +2 -2
  100. package/dist/memory/layers/file-reference.d.ts +1 -1
  101. package/dist/memory/layers/file-reference.js +303 -247
  102. package/dist/memory/layers/history-window.d.ts +1 -1
  103. package/dist/memory/layers/history-window.js +4 -4
  104. package/dist/memory/layers/observational-memory.d.ts +5 -5
  105. package/dist/memory/layers/observational-memory.js +2 -3
  106. package/dist/memory/layers/plan.d.ts +2 -2
  107. package/dist/memory/layers/plan.js +15 -10
  108. package/dist/memory/layers/static-content.d.ts +2 -2
  109. package/dist/memory/layers/static-content.js +1 -1
  110. package/dist/memory/layers/steering.d.ts +7 -7
  111. package/dist/memory/layers/steering.js +5 -5
  112. package/dist/memory/layers/tool-memory-layer.d.ts +2 -2
  113. package/dist/memory/layers/tool-memory-layer.js +1 -1
  114. package/dist/memory/layers/working-memory.d.ts +7 -7
  115. package/dist/memory/layers/working-memory.js +4 -4
  116. package/dist/memory/projector.d.ts +2 -2
  117. package/dist/memory/scope.d.ts +1 -1
  118. package/dist/memory/strip-unresolved.d.ts +1 -1
  119. package/dist/observability/span-impl.d.ts +1 -1
  120. package/dist/observability/trace-exporter.d.ts +2 -2
  121. package/dist/observability/trace-exporter.js +1 -1
  122. package/dist/patterns/dynamic-workflow.d.ts +55 -0
  123. package/dist/patterns/dynamic-workflow.js +194 -0
  124. package/dist/patterns/flow.d.ts +2 -52
  125. package/dist/patterns/flow.js +1 -103
  126. package/dist/patterns/interview.d.ts +2 -2
  127. package/dist/patterns/interview.js +5 -5
  128. package/dist/patterns/plans.d.ts +6 -6
  129. package/dist/patterns/plans.js +4 -4
  130. package/dist/patterns/ralph-wiggum.d.ts +4 -4
  131. package/dist/patterns/ralph-wiggum.js +5 -5
  132. package/dist/patterns/react.d.ts +3 -3
  133. package/dist/patterns/react.js +5 -5
  134. package/dist/portable/adapters.d.ts +6 -0
  135. package/dist/portable/adapters.js +6 -0
  136. package/dist/portable/ask-user.d.ts +4 -0
  137. package/dist/portable/ask-user.js +2 -0
  138. package/dist/portable/builders.d.ts +12 -0
  139. package/dist/portable/builders.js +12 -0
  140. package/dist/portable/memory-layers.d.ts +12 -0
  141. package/dist/portable/memory-layers.js +12 -0
  142. package/dist/portable/runtime.d.ts +4 -0
  143. package/dist/portable/runtime.js +4 -0
  144. package/dist/portable/until.d.ts +4 -0
  145. package/dist/portable/until.js +4 -0
  146. package/dist/portable.d.ts +8 -26
  147. package/dist/portable.js +8 -34
  148. package/dist/runtime/broadcaster-utils.d.ts +7 -3
  149. package/dist/runtime/broadcaster-utils.js +11 -6
  150. package/dist/runtime/channel-store.d.ts +1 -1
  151. package/dist/runtime/channel-store.js +2 -2
  152. package/dist/runtime/context-deps.d.ts +4 -0
  153. package/dist/runtime/context-deps.js +2 -0
  154. package/dist/runtime/context-impl.d.ts +42 -12
  155. package/dist/runtime/context-impl.js +52 -5
  156. package/dist/runtime/context-type-deps.d.ts +14 -0
  157. package/dist/runtime/context-type-deps.js +1 -0
  158. package/dist/runtime/cwd-helpers.d.ts +1 -1
  159. package/dist/runtime/detached-handle.d.ts +52 -4
  160. package/dist/runtime/detached-handle.js +218 -14
  161. package/dist/runtime/durable/checkpoint-store.d.ts +38 -0
  162. package/dist/runtime/durable/checkpoint-store.js +101 -0
  163. package/dist/runtime/durable/detached-signal.d.ts +37 -0
  164. package/dist/runtime/durable/detached-signal.js +52 -0
  165. package/dist/runtime/durable/harness-checkpoints.d.ts +51 -0
  166. package/dist/runtime/durable/harness-checkpoints.js +104 -0
  167. package/dist/runtime/durable/index.d.ts +10 -0
  168. package/dist/runtime/durable/index.js +5 -0
  169. package/dist/runtime/durable/runnable-loop.d.ts +100 -0
  170. package/dist/runtime/durable/runnable-loop.js +90 -0
  171. package/dist/runtime/durable/session-seed.d.ts +28 -0
  172. package/dist/runtime/durable/session-seed.js +27 -0
  173. package/dist/runtime/durable/stall-nudge.d.ts +82 -0
  174. package/dist/runtime/durable/stall-nudge.js +87 -0
  175. package/dist/runtime/event-broadcaster.d.ts +1 -1
  176. package/dist/runtime/in-memory-storage.d.ts +1 -1
  177. package/dist/runtime/in-memory-storage.js +1 -1
  178. package/dist/runtime/item-log-impl.d.ts +3 -3
  179. package/dist/runtime/item-log-impl.js +1 -1
  180. package/dist/runtime/message-queue.d.ts +2 -2
  181. package/dist/runtime/session-runner.d.ts +7 -7
  182. package/dist/runtime/session-runner.js +3 -3
  183. package/dist/runtime/session-streams.d.ts +3 -3
  184. package/dist/runtime/session-streams.js +1 -1
  185. package/dist/runtime/step-registry.d.ts +42 -0
  186. package/dist/runtime/step-registry.js +77 -0
  187. package/dist/runtime/tool-memory.d.ts +3 -3
  188. package/dist/schemas/item.d.ts +1 -1
  189. package/dist/schemas/workflow.d.ts +139 -0
  190. package/dist/schemas/workflow.js +232 -0
  191. package/dist/types/ask-user-types.d.ts +57 -0
  192. package/dist/types/ask-user-types.js +74 -0
  193. package/dist/types/checkpoint.d.ts +98 -0
  194. package/dist/types/checkpoint.js +81 -0
  195. package/dist/types/common.d.ts +1 -50
  196. package/dist/types/context-parts/item-log.d.ts +6 -0
  197. package/dist/types/context-parts/item-log.js +1 -0
  198. package/dist/types/context-parts/layer-usage.d.ts +18 -0
  199. package/dist/types/context-parts/layer-usage.js +1 -0
  200. package/dist/types/context.d.ts +140 -31
  201. package/dist/types/error.d.ts +15 -0
  202. package/dist/types/harness-result.d.ts +2 -2
  203. package/dist/types/items.d.ts +18 -2
  204. package/dist/types/memory-context.d.ts +39 -0
  205. package/dist/types/memory-context.js +1 -0
  206. package/dist/types/memory.d.ts +38 -68
  207. package/dist/types/mutable-context.d.ts +3 -3
  208. package/dist/types/runtime.d.ts +14 -13
  209. package/dist/types/steering.d.ts +2 -2
  210. package/dist/types/step-registrar.d.ts +46 -0
  211. package/dist/types/step-registrar.js +56 -0
  212. package/dist/types/step.d.ts +34 -9
  213. package/dist/types/subprocess-adapter.d.ts +119 -3
  214. package/dist/types/subprocess-adapter.js +2 -0
  215. package/dist/types/tool-context.d.ts +6 -6
  216. package/dist/types/tool.d.ts +64 -0
  217. package/dist/types/tool.js +1 -0
  218. package/dist/unstable.d.ts +9 -9
  219. package/dist/unstable.js +7 -7
  220. package/dist/until/combinators.d.ts +1 -1
  221. package/dist/until/never.d.ts +1 -1
  222. package/dist/until/predicates.d.ts +13 -4
  223. package/dist/until/predicates.js +16 -2
  224. package/dist/util/message-helpers.d.ts +17 -0
  225. package/dist/util/message-helpers.js +64 -0
  226. package/package.json +6 -4
  227. package/dist/adapters/local-fs-adapter.d.ts +0 -3
  228. package/dist/adapters/local-fs-adapter.js +0 -43
  229. package/dist/adapters/local-shell-adapter.d.ts +0 -51
  230. package/dist/adapters/local-shell-adapter.js +0 -167
  231. package/dist/adapters/local-subprocess-adapter.d.ts +0 -16
  232. package/dist/adapters/local-subprocess-adapter.js +0 -187
  233. package/dist/conditions/index.d.ts +0 -3
  234. package/dist/conditions/index.js +0 -2
  235. package/dist/interpreter/execute-branch.d.ts +0 -3
  236. package/dist/interpreter/execute-branch.js +0 -10
  237. package/dist/interpreter/execute-every.js +0 -143
  238. package/dist/interpreter/execute-fork.d.ts +0 -3
  239. package/dist/interpreter/execute-fork.js +0 -215
  240. package/dist/interpreter/execute-llm.d.ts +0 -4
  241. package/dist/interpreter/execute-llm.js +0 -217
  242. package/dist/interpreter/execute-loop.d.ts +0 -3
  243. package/dist/interpreter/execute-loop.js +0 -171
  244. package/dist/interpreter/execute-provide.d.ts +0 -10
  245. package/dist/interpreter/execute-provide.js +0 -64
  246. package/dist/interpreter/execute-run.d.ts +0 -3
  247. package/dist/interpreter/execute-run.js +0 -39
  248. package/dist/interpreter/execute-spawn.d.ts +0 -11
  249. package/dist/interpreter/execute-spawn.js +0 -113
  250. package/dist/interpreter/execute-tool.d.ts +0 -5
  251. package/dist/interpreter/execute-tool.js +0 -81
  252. package/dist/runtime/agent-harness.js +0 -1170
  253. /package/dist/{interpreter → util}/framework-cast.d.ts +0 -0
  254. /package/dist/{interpreter → util}/framework-cast.js +0 -0
@@ -1,1170 +0,0 @@
1
- import { OpenRouter } from '@openrouter/agent';
2
- import { z } from 'zod';
3
- import { createInMemoryFsAdapter } from '../adapters/in-memory-fs-adapter';
4
- import { createInMemoryShellAdapter } from '../adapters/in-memory-shell-adapter';
5
- import { createInMemorySubprocessAdapter } from '../adapters/in-memory-subprocess-adapter';
6
- import { convertTools, executeToolCall, extractOutputItems, extractSystemInstruction, extractUsage, itemsToInput, } from '../adapters/openrouter';
7
- import { NoeticConfigError } from '../errors/noetic-config-error';
8
- import { collectAllTools, deduplicateTools } from '../interpreter/collect-tools';
9
- import { execute } from '../interpreter/execute';
10
- import { frameworkCast } from '../interpreter/framework-cast';
11
- import { isFunctionCall } from '../interpreter/typeguards';
12
- import { resolveLayerTools } from '../memory/layer-api';
13
- import { afterModelCallLayers, beforeToolCallLayers, createLayerStateStore, disposeLayers, executeRerender, initLayers, projectHistoryLayers, recallLayers, resolveLayerBudgets, runAppendPipeline, storeLayers, } from '../memory/layer-lifecycle';
14
- import { assembleView } from '../memory/projector';
15
- import { SpanImpl } from '../observability/span-impl';
16
- import { NoopExporter } from '../observability/trace-exporter';
17
- import { ItemSchemaRegistry, mergeExtensions } from '../schemas/item';
18
- import { SteeringAction } from '../types/steering';
19
- import { emitFrameworkEvent, getBroadcaster, shouldEmit } from './broadcaster-utils';
20
- import { ChannelStore } from './channel-store';
21
- import { ContextImpl } from './context-impl';
22
- import { snapshotCwdState } from './cwd-helpers';
23
- import { DetachedHandleImpl } from './detached-handle';
24
- import { contextToExecCtx } from './exec-context-factory';
25
- import { createInMemoryStorage } from './in-memory-storage';
26
- import { SessionRunner } from './session-runner';
27
- import { buildItemStream, filterReasoningStream, filterTextStream } from './session-streams';
28
- //#endregion
29
- const MAX_TOOL_ROUNDS = 32;
30
- const MAX_RECOVERY_CONTINUATIONS = 3;
31
- const EPHEMERAL_CONTINUE_INPUT = 'continue';
32
- const DEFAULT_THREAD_ID = '__default__';
33
- /** Default idle-timeout for a single model call's streaming response. Chosen to be
34
- * long enough that slow models aren't falsely aborted, but short enough that a
35
- * stalled SSE becomes a visible error rather than a silent hang. */
36
- const DEFAULT_STREAM_IDLE_TIMEOUT_MS = 120e3;
37
- //#region Helpers
38
- /**
39
- * Pick the right `cwdState` for a new Context.
40
- *
41
- * - `cwdInit` explicitly overrides everything (worktree isolation).
42
- * - Parent snapshot if present (snapshot, not shared reference — child `cd`
43
- * does not leak to parent).
44
- * - Otherwise share the harness `rootCwdState` reference so successive root
45
- * contexts see TUI/agent `cd`s carry across runs.
46
- */
47
- function resolveContextCwdState(rootCwdState, parent, cwdInit) {
48
- if (cwdInit !== undefined) {
49
- return {
50
- cwd: cwdInit,
51
- };
52
- }
53
- if (parent) {
54
- return snapshotCwdState(parent);
55
- }
56
- return rootCwdState;
57
- }
58
- function createClient(config) {
59
- const processValue = 'process' in globalThis ? globalThis.process : undefined;
60
- const envValue = typeof processValue === 'object' && processValue !== null && 'env' in processValue
61
- ? processValue.env
62
- : undefined;
63
- const openRouterApiKey = typeof envValue === 'object' && envValue !== null && 'OPENROUTER_API_KEY' in envValue
64
- ? envValue.OPENROUTER_API_KEY
65
- : undefined;
66
- const apiKey = config?.apiKey ?? (typeof openRouterApiKey === 'string' ? openRouterApiKey : undefined);
67
- if (!apiKey) {
68
- return undefined;
69
- }
70
- return new OpenRouter({
71
- apiKey,
72
- });
73
- }
74
- function mergeItemSchemaExtensions(extensions) {
75
- let result = {
76
- items: [],
77
- developerMessages: [],
78
- toolCalls: [],
79
- toolResults: [],
80
- };
81
- for (const ext of extensions) {
82
- if (ext) {
83
- result = mergeExtensions(result, ext);
84
- }
85
- }
86
- return result;
87
- }
88
- function collectLayerItemSchemaExtensions(layers) {
89
- return mergeItemSchemaExtensions(layers?.map((layer) => layer.itemSchemas) ?? []);
90
- }
91
- function collectToolItemSchemaExtensions(tools) {
92
- return mergeItemSchemaExtensions(tools?.map((tool) => tool.itemSchemas) ?? []);
93
- }
94
- function buildItemSchemaRegistry({ base, layers, tools, }) {
95
- return base
96
- .extend(collectLayerItemSchemaExtensions(layers))
97
- .extend(collectToolItemSchemaExtensions(tools));
98
- }
99
- function createToolResultItem({ output, callId, roundItemSchemas, tool, callItem, args, result, error, }) {
100
- const baseItem = {
101
- id: crypto.randomUUID(),
102
- status: 'completed',
103
- type: 'function_call_output',
104
- callId,
105
- output,
106
- };
107
- const decorated = tool?.decorateResultItem && callItem?.type === 'function_call'
108
- ? tool.decorateResultItem({
109
- baseItem,
110
- callItem,
111
- args,
112
- result,
113
- output,
114
- error,
115
- })
116
- : baseItem;
117
- return roundItemSchemas.parseWithCategory(decorated, 'toolResults');
118
- }
119
- function buildTextFormat(schema) {
120
- const jsonSchema = z.toJSONSchema(schema);
121
- return {
122
- format: {
123
- type: 'json_schema',
124
- name: 'output',
125
- schema: jsonSchema,
126
- },
127
- };
128
- }
129
- function isStreamRecord(value) {
130
- return typeof value === 'object' && value !== null;
131
- }
132
- function providerTerminalError(response) {
133
- if (!isStreamRecord(response) || response.status === undefined) {
134
- return null;
135
- }
136
- const status = String(response.status);
137
- if (status === 'completed') {
138
- return null;
139
- }
140
- if (status === 'incomplete') {
141
- const details = response.incompleteDetails;
142
- const reason = isStreamRecord(details) && typeof details.reason === 'string' ? `: ${details.reason}` : '';
143
- return {
144
- status,
145
- message: `LLM response incomplete${reason}`,
146
- };
147
- }
148
- if (status === 'failed') {
149
- const error = response.error;
150
- const message = isStreamRecord(error) && typeof error.message === 'string' ? `: ${error.message}` : '';
151
- return {
152
- status,
153
- message: `LLM response failed${message}`,
154
- };
155
- }
156
- return {
157
- status,
158
- message: `LLM response ended with status '${status}'`,
159
- };
160
- }
161
- function hasUsableResponseOutput(response, items) {
162
- if (items.length > 0) {
163
- return true;
164
- }
165
- return (isStreamRecord(response) &&
166
- typeof response.outputText === 'string' &&
167
- response.outputText.length > 0);
168
- }
169
- function withEphemeralContinueInput(input) {
170
- return [
171
- ...frameworkCast(input),
172
- frameworkCast({
173
- type: 'message',
174
- role: 'user',
175
- content: EPHEMERAL_CONTINUE_INPUT,
176
- }),
177
- ];
178
- }
179
- /** Race a promise against an AbortSignal so callers (e.g. `SessionRunner.abort`)
180
- * can break out of a long `await` without waiting for the underlying call to
181
- * settle. When the signal fires, the returned promise rejects with
182
- * `signal.reason` (an `Error`) or a generic `Error('aborted')` fallback. */
183
- function awaitWithAbort(p, signal) {
184
- if (!signal) {
185
- return p;
186
- }
187
- if (signal.aborted) {
188
- return Promise.reject(signal.reason instanceof Error ? signal.reason : new Error('aborted'));
189
- }
190
- return new Promise((resolve, reject) => {
191
- const onAbort = () => {
192
- reject(signal.reason instanceof Error ? signal.reason : new Error('aborted'));
193
- };
194
- signal.addEventListener('abort', onAbort, {
195
- once: true,
196
- });
197
- p.then((v) => {
198
- signal.removeEventListener('abort', onAbort);
199
- resolve(v);
200
- }, (e) => {
201
- signal.removeEventListener('abort', onAbort);
202
- reject(e);
203
- });
204
- });
205
- }
206
- async function pipeStreamEventsToBroadcaster(opts) {
207
- const { stream, broadcaster, agentName, signal, onEvent } = opts;
208
- try {
209
- for await (const event of stream) {
210
- if (signal?.aborted) {
211
- return;
212
- }
213
- onEvent?.();
214
- if (!isStreamRecord(event)) {
215
- continue;
216
- }
217
- broadcaster?.emit({
218
- source: 'sdk',
219
- type: typeof event.type === 'string' ? event.type : 'unknown',
220
- data: event,
221
- outputIndex: typeof event.outputIndex === 'number' ? event.outputIndex : undefined,
222
- contentIndex: typeof event.contentIndex === 'number' ? event.contentIndex : undefined,
223
- });
224
- }
225
- }
226
- catch (err) {
227
- if (broadcaster) {
228
- emitFrameworkEvent({
229
- broadcaster,
230
- agentName,
231
- eventType: 'stream_pipe_error',
232
- data: {
233
- error: err instanceof Error ? err.message : String(err),
234
- },
235
- });
236
- }
237
- throw err;
238
- }
239
- }
240
- /** @internal Create a watchdog that aborts `controller` when no
241
- * {@link StreamIdleWatchdog.reset reset} is called within `timeoutMs`. When
242
- * `timeoutMs <= 0`, returns an inert no-op so callers can always call `.reset()`
243
- * / `.stop()` without a branch. Starts armed: the caller is responsible for
244
- * `.stop()` in a `finally`. `onTimeout` runs before the abort so observers can
245
- * emit a framework event with the original cause. Exported only for unit tests. */
246
- export function createStreamIdleWatchdog(timeoutMs, controller, onTimeout) {
247
- if (timeoutMs <= 0) {
248
- return {
249
- reset: () => { },
250
- stop: () => { },
251
- };
252
- }
253
- let timer;
254
- let stopped = false;
255
- const arm = () => {
256
- timer = setTimeout(() => {
257
- if (stopped) {
258
- return;
259
- }
260
- const reason = new Error(`llm stream idle timeout after ${timeoutMs}ms`);
261
- onTimeout?.();
262
- controller.abort(reason);
263
- }, timeoutMs);
264
- };
265
- arm();
266
- return {
267
- reset: () => {
268
- if (stopped || !timer) {
269
- return;
270
- }
271
- clearTimeout(timer);
272
- arm();
273
- },
274
- stop: () => {
275
- stopped = true;
276
- if (timer) {
277
- clearTimeout(timer);
278
- timer = undefined;
279
- }
280
- },
281
- };
282
- }
283
- function hasSessionQueue(ctx) {
284
- const maybe = frameworkCast(ctx);
285
- return (maybe._sessionBetweenRounds === true &&
286
- maybe._sessionQueue !== undefined &&
287
- typeof maybe._sessionRunnerAgentName === 'string');
288
- }
289
- //#endregion
290
- //#region AgentHarness
291
- /**
292
- * Default agent harness for executing agent steps with built-in channel, memory, and trace support.
293
- * Provides channel store, memory layer lifecycle, and trace export with no external dependencies.
294
- *
295
- * Messages submitted via `execute()` are enqueued on a per-thread session and
296
- * processed by a `SessionRunner`. Consumers observe responses via the session-
297
- * scoped accessors: `getAgentResponse`, `getItemStream`, etc.
298
- *
299
- * @public
300
- */
301
- export class AgentHarness {
302
- config;
303
- fs;
304
- shell;
305
- subprocess;
306
- initialStep;
307
- _memory;
308
- client;
309
- channelStore;
310
- callModelOverride;
311
- defaultDeliveryMode;
312
- streamIdleTimeoutMs;
313
- sessions = new Map();
314
- layerStateStore;
315
- traceExporter;
316
- /**
317
- * Long-lived shared cwd state. The same reference is seeded into every
318
- * root Context this harness creates, so successive `run()` calls — and the
319
- * TUI — observe each other's `cd`s.
320
- */
321
- rootCwdState;
322
- itemSchemas;
323
- constructor(opts) {
324
- const validatedParams = opts.paramsSchema ? opts.paramsSchema.parse(opts.params) : opts.params;
325
- this.config = {
326
- name: opts.name,
327
- storage: opts.storage ?? createInMemoryStorage(),
328
- hooks: opts.hooks,
329
- params: validatedParams,
330
- itemSchemas: opts.itemSchemas,
331
- strictItemSchemas: opts.strictItemSchemas ?? true,
332
- };
333
- this.fs = opts.fs ?? createInMemoryFsAdapter();
334
- this.shell = opts.shell ?? createInMemoryShellAdapter();
335
- this.subprocess = opts.subprocess ?? createInMemorySubprocessAdapter();
336
- this.initialStep = opts.initialStep;
337
- this._memory = opts.memory;
338
- this.callModelOverride = opts._testCallModel;
339
- this.client = opts._testCallModel ? undefined : createClient(opts.llm);
340
- this.channelStore = new ChannelStore();
341
- this.traceExporter = opts.traceExporter ?? new NoopExporter();
342
- this.layerStateStore = opts.layerStateStore ?? createLayerStateStore();
343
- this.defaultDeliveryMode = opts.defaultDeliveryMode ?? 'next-turn';
344
- this.streamIdleTimeoutMs = opts.streamIdleTimeoutMs ?? DEFAULT_STREAM_IDLE_TIMEOUT_MS;
345
- this.itemSchemas = new ItemSchemaRegistry(opts.itemSchemas, {
346
- strictUnknownExtensions: opts.strictItemSchemas ?? true,
347
- });
348
- this.rootCwdState = {
349
- cwd: opts.initialCwd ?? '/',
350
- };
351
- }
352
- /**
353
- * Update the harness-wide root cwd. The TUI calls this in response to a
354
- * user-issued `! cd`, so the next root Context (and any tool inspecting
355
- * `harness.rootCwdState.cwd`) observes the new value. Caller is responsible
356
- * for passing an absolute, validated path.
357
- */
358
- setRootCwd(nextCwd) {
359
- if (nextCwd === this.rootCwdState.cwd) {
360
- return;
361
- }
362
- this.rootCwdState.previousCwd = this.rootCwdState.cwd;
363
- this.rootCwdState.cwd = nextCwd;
364
- }
365
- //#region Session Accessors
366
- execute(input, options) {
367
- if (!this.initialStep) {
368
- return Promise.reject(new NoeticConfigError({
369
- code: 'NO_STEP_CONFIGURED',
370
- message: 'No initialStep configured on this harness.',
371
- hint: 'Pass `initialStep` in constructor options, or use run() directly.',
372
- }));
373
- }
374
- const threadId = options?.threadId ?? DEFAULT_THREAD_ID;
375
- const deliveryMode = options?.deliveryMode ?? this.defaultDeliveryMode;
376
- const session = this.getOrCreateSession(threadId);
377
- const message = {
378
- id: options?.messageId ?? `msg-${crypto.randomUUID()}`,
379
- input,
380
- deliveryMode,
381
- options: options ?? {},
382
- enqueuedAt: Date.now(),
383
- };
384
- if (deliveryMode === 'interrupt' && session.runner.getStatus().kind === 'generating') {
385
- session.runner.queue.prepend(message);
386
- // Abort kicks the runner via queue subscription after the in-flight turn settles.
387
- void session.runner.abort('interrupt');
388
- return Promise.resolve();
389
- }
390
- session.runner.queue.enqueue(message);
391
- return Promise.resolve();
392
- }
393
- getAgentResponse(scope) {
394
- const session = this.requireSession(scope);
395
- return session.runner.getAgentResponse();
396
- }
397
- getItemStream(scope) {
398
- const session = this.requireSession(scope);
399
- return buildItemStream(session.runner.broadcaster, this.itemSchemas);
400
- }
401
- getTextStream(scope) {
402
- const session = this.requireSession(scope);
403
- return filterTextStream(session.runner.broadcaster);
404
- }
405
- getReasoningStream(scope) {
406
- const session = this.requireSession(scope);
407
- return filterReasoningStream(session.runner.broadcaster);
408
- }
409
- getFullStream(scope) {
410
- const session = this.requireSession(scope);
411
- return session.runner.broadcaster;
412
- }
413
- async abort(scope) {
414
- const threadId = scope?.threadId ?? DEFAULT_THREAD_ID;
415
- const session = this.sessions.get(threadId);
416
- if (!session) {
417
- return;
418
- }
419
- await session.runner.abort(scope?.reason);
420
- }
421
- getStatus(scope) {
422
- const threadId = scope?.threadId ?? DEFAULT_THREAD_ID;
423
- const session = this.sessions.get(threadId);
424
- if (!session) {
425
- return {
426
- kind: 'idle',
427
- };
428
- }
429
- return session.runner.getStatus();
430
- }
431
- getQueueSize(scope) {
432
- const threadId = scope?.threadId ?? DEFAULT_THREAD_ID;
433
- const session = this.sessions.get(threadId);
434
- return session ? session.runner.queue.size : 0;
435
- }
436
- seedSessionHistory(threadId, items) {
437
- const session = this.getOrCreateSession(threadId);
438
- session.accumulatedItems = [
439
- ...items,
440
- ];
441
- }
442
- getOrCreateSession(threadId) {
443
- const existing = this.sessions.get(threadId);
444
- if (existing) {
445
- return existing;
446
- }
447
- const session = {
448
- accumulatedItems: [],
449
- runner: new SessionRunner({
450
- threadId,
451
- agentName: this.config.name,
452
- // The first queued message in a batch establishes `resourceId` /
453
- // `state` / `memory` for the turn. If multiple messages are drained
454
- // together (queue flush), later messages' values for these fields
455
- // are ignored. `deliveryMode` is resolved per-message at enqueue
456
- // time and doesn't apply here.
457
- createContext: (items, _turnId, messages) => {
458
- const perTurnOptions = messages[0]?.options ?? {};
459
- const allItems = [
460
- ...session.accumulatedItems,
461
- ...items,
462
- ];
463
- const ctx = this.createContext({
464
- items: allItems,
465
- threadId,
466
- resourceId: perTurnOptions.resourceId,
467
- state: perTurnOptions.state,
468
- memory: perTurnOptions.memory,
469
- _broadcaster: session.runner.broadcaster,
470
- });
471
- const ext = frameworkCast(ctx);
472
- ext._sessionQueue = session.runner.queue;
473
- ext._sessionBetweenRounds = true;
474
- ext._sessionRunnerAgentName = this.config.name;
475
- if (this.initialStep) {
476
- this.setUnifiedTools(ctx, collectAllTools(this.initialStep));
477
- }
478
- return ctx;
479
- },
480
- runTurn: async (ctx, _turn, signal) => {
481
- if (!this.initialStep) {
482
- throw new NoeticConfigError({
483
- code: 'NO_STEP_CONFIGURED',
484
- message: 'No initialStep configured on this harness.',
485
- hint: 'Pass `initialStep` in constructor options.',
486
- });
487
- }
488
- // Wire signal-abort to context-abort so the interpreter bails cleanly.
489
- if (signal.aborted) {
490
- ctx.abort(signal.reason ? String(signal.reason) : 'aborted');
491
- }
492
- else {
493
- signal.addEventListener('abort', () => {
494
- ctx.abort(signal.reason ? String(signal.reason) : 'aborted');
495
- }, {
496
- once: true,
497
- });
498
- }
499
- const result = await this.initAndRun(this.initialStep, '', ctx);
500
- // Snapshot final items into session history for the next turn.
501
- session.accumulatedItems = [
502
- ...ctx.itemLog.items,
503
- ];
504
- return result;
505
- },
506
- }),
507
- };
508
- this.sessions.set(threadId, session);
509
- return session;
510
- }
511
- requireSession(scope) {
512
- const threadId = scope?.threadId ?? DEFAULT_THREAD_ID;
513
- let session = this.sessions.get(threadId);
514
- if (session) {
515
- return session;
516
- }
517
- // Lazily create so consumers can attach stream listeners before the first execute.
518
- session = this.getOrCreateSession(threadId);
519
- return session;
520
- }
521
- //#endregion
522
- //#region callModel
523
- async callModel(request) {
524
- if (this.callModelOverride) {
525
- const response = await this.callModelOverride(request);
526
- const itemSchemas = buildItemSchemaRegistry({
527
- base: this.itemSchemas,
528
- layers: request.layers,
529
- tools: request.tools,
530
- });
531
- return {
532
- ...response,
533
- items: itemSchemas.parseMany(response.items),
534
- };
535
- }
536
- if (!this.client) {
537
- throw new NoeticConfigError({
538
- code: 'NO_LLM_PROVIDER',
539
- message: 'No LLM provider configured on this harness.',
540
- hint: 'Pass `llm: { provider: "openrouter", apiKey: "..." }` in constructor options or set OPENROUTER_API_KEY.',
541
- });
542
- }
543
- const { instructions: extractedInstructions, remaining } = extractSystemInstruction(request.items);
544
- const instructions = [
545
- request.instructions,
546
- extractedInstructions,
547
- ]
548
- .filter(Boolean)
549
- .join('\n\n') || undefined;
550
- const broadcaster = getBroadcaster(request.ctx);
551
- const agentName = this.config.name;
552
- const signal = request.signal;
553
- const emitIfAllowed = (eventType, data) => {
554
- if (shouldEmit(request.emit, eventType, data)) {
555
- emitFrameworkEvent({
556
- broadcaster,
557
- agentName,
558
- eventType,
559
- data,
560
- });
561
- }
562
- };
563
- const allowedNamesSet = request.tools && 'allowedToolNames' in request && request.allowedToolNames
564
- ? new Set(request.allowedToolNames)
565
- : undefined;
566
- const filteredTools = allowedNamesSet
567
- ? request.tools?.filter((t) => allowedNamesSet.has(t.name))
568
- : request.tools;
569
- let sdkTools;
570
- if (filteredTools && filteredTools.length > 0) {
571
- sdkTools = convertTools({
572
- tools: filteredTools,
573
- });
574
- }
575
- const allItems = [];
576
- const totalUsage = {
577
- inputTokens: 0,
578
- outputTokens: 0,
579
- cachedTokens: 0,
580
- };
581
- let totalCost = 0;
582
- const conversationInput = itemsToInput(remaining);
583
- const textFormat = request.outputSchema ? buildTextFormat(request.outputSchema) : undefined;
584
- let round = 0;
585
- let invalidRecoveryContinuations = 0;
586
- let toolLimitRecoveryContinuations = 0;
587
- let useEphemeralContinue = false;
588
- while (!signal?.aborted) {
589
- const recoveryContinuation = useEphemeralContinue;
590
- useEphemeralContinue = false;
591
- // Inject between-rounds inbox messages (mode: 'between-rounds') before
592
- // the next LLM call. Mirrors Claude Code's teammate-attachment path.
593
- // Also append to ctx.itemLog so the session's post-turn snapshot
594
- // carries the injected messages into next turn's history.
595
- if (round > 0 && request.ctx && hasSessionQueue(request.ctx)) {
596
- const injected = this.drainBetweenRoundsMessages(request.ctx._sessionQueue);
597
- if (injected.length > 0) {
598
- for (const msg of injected) {
599
- const text = itemToText(msg);
600
- const userItem = {
601
- id: `user-${crypto.randomUUID()}`,
602
- type: 'message',
603
- role: 'user',
604
- status: 'completed',
605
- content: [
606
- {
607
- type: 'input_text',
608
- text,
609
- },
610
- ],
611
- };
612
- conversationInput.push({
613
- type: 'message',
614
- role: 'user',
615
- content: [
616
- {
617
- type: 'input_text',
618
- text,
619
- },
620
- ],
621
- });
622
- request.ctx.itemLog.append(userItem);
623
- }
624
- emitIfAllowed('inbox_injected', {
625
- round,
626
- count: injected.length,
627
- messageIds: injected.map((i) => i.id),
628
- });
629
- }
630
- }
631
- // Build a round-scoped controller that also aborts when the caller's
632
- // signal fires — `AbortSignal.any` handles listener lifecycle for us
633
- // (no manual cleanup required on normal completion).
634
- const roundController = new AbortController();
635
- const roundSignal = signal
636
- ? AbortSignal.any([
637
- signal,
638
- roundController.signal,
639
- ])
640
- : roundController.signal;
641
- let firstEventSeen = false;
642
- let idleStalled = false;
643
- const watchdog = createStreamIdleWatchdog(this.streamIdleTimeoutMs, roundController, () => {
644
- idleStalled = true;
645
- emitIfAllowed('llm_call_stalled', {
646
- round,
647
- idleTimeoutMs: this.streamIdleTimeoutMs,
648
- });
649
- });
650
- const modelInput = recoveryContinuation
651
- ? withEphemeralContinueInput(conversationInput)
652
- : frameworkCast(conversationInput);
653
- emitIfAllowed('llm_call_started', {
654
- round,
655
- messageCount: modelInput.length,
656
- toolCount: sdkTools?.length ?? 0,
657
- recoveryContinuation,
658
- });
659
- const callResult = this.client.callModel({
660
- model: request.model,
661
- input: modelInput,
662
- instructions,
663
- tools: sdkTools,
664
- temperature: request.params?.temperature,
665
- maxOutputTokens: request.params?.maxTokens,
666
- topP: request.params?.topP,
667
- ...(textFormat
668
- ? {
669
- text: textFormat,
670
- }
671
- : {}),
672
- }, {
673
- signal: roundSignal,
674
- });
675
- const onStreamEvent = () => {
676
- watchdog.reset();
677
- if (!firstEventSeen) {
678
- firstEventSeen = true;
679
- emitIfAllowed('llm_call_first_event', {
680
- round,
681
- });
682
- }
683
- };
684
- // Always consume the SDK stream so the watchdog gets reset per event
685
- // even when no broadcaster is attached (headless / test harness runs).
686
- // Emission into the broadcaster is optional inside the pipe function.
687
- const pipePromise = pipeStreamEventsToBroadcaster({
688
- stream: callResult.getFullResponsesStream(),
689
- broadcaster,
690
- agentName,
691
- signal: roundSignal,
692
- onEvent: onStreamEvent,
693
- });
694
- let sdkResponse;
695
- try {
696
- sdkResponse = await awaitWithAbort(callResult.getResponse(), roundSignal);
697
- // Stop the watchdog synchronously the instant the response resolves,
698
- // closing the race where a pending setTimeout could fire in the
699
- // microtask gap between this await and the `finally` below.
700
- watchdog.stop();
701
- await awaitWithAbort(pipePromise, roundSignal);
702
- }
703
- catch (err) {
704
- if (idleStalled) {
705
- // roundSignal.reason is the Error the watchdog passed to controller.abort().
706
- throw roundSignal.reason instanceof Error
707
- ? roundSignal.reason
708
- : new Error(`llm stream idle timeout after ${this.streamIdleTimeoutMs}ms`);
709
- }
710
- // Parent signal cancelled the round — exit gracefully rather than
711
- // propagating the abort error as an exception.
712
- if (signal?.aborted) {
713
- break;
714
- }
715
- throw err;
716
- }
717
- finally {
718
- watchdog.stop();
719
- }
720
- if (signal?.aborted) {
721
- break;
722
- }
723
- const terminalError = providerTerminalError(sdkResponse);
724
- if (terminalError) {
725
- emitIfAllowed('llm_call_failed', {
726
- round,
727
- status: terminalError.status,
728
- error: terminalError.message,
729
- recoverable: invalidRecoveryContinuations < MAX_RECOVERY_CONTINUATIONS,
730
- });
731
- if (invalidRecoveryContinuations >= MAX_RECOVERY_CONTINUATIONS) {
732
- throw new Error(terminalError.message);
733
- }
734
- invalidRecoveryContinuations += 1;
735
- useEphemeralContinue = true;
736
- emitIfAllowed('llm_call_recovery_continue', {
737
- round,
738
- status: terminalError.status,
739
- attempt: invalidRecoveryContinuations,
740
- maxAttempts: MAX_RECOVERY_CONTINUATIONS,
741
- });
742
- continue;
743
- }
744
- const roundItemSchemas = buildItemSchemaRegistry({
745
- base: this.itemSchemas,
746
- layers: request.layers,
747
- tools: request.tools,
748
- });
749
- const roundItems = roundItemSchemas.parseMany(extractOutputItems(sdkResponse));
750
- if (!hasUsableResponseOutput(sdkResponse, roundItems)) {
751
- const message = 'LLM response completed with no output items';
752
- emitIfAllowed('llm_call_failed', {
753
- round,
754
- status: 'completed',
755
- error: message,
756
- recoverable: invalidRecoveryContinuations < MAX_RECOVERY_CONTINUATIONS,
757
- });
758
- if (invalidRecoveryContinuations >= MAX_RECOVERY_CONTINUATIONS) {
759
- throw new Error(message);
760
- }
761
- invalidRecoveryContinuations += 1;
762
- useEphemeralContinue = true;
763
- emitIfAllowed('llm_call_recovery_continue', {
764
- round,
765
- status: 'completed',
766
- attempt: invalidRecoveryContinuations,
767
- maxAttempts: MAX_RECOVERY_CONTINUATIONS,
768
- });
769
- continue;
770
- }
771
- invalidRecoveryContinuations = 0;
772
- emitIfAllowed('llm_call_completed', {
773
- round,
774
- itemCount: sdkResponse.output?.length ?? 0,
775
- });
776
- const roundUsage = extractUsage(sdkResponse.usage);
777
- totalUsage.inputTokens += roundUsage.inputTokens;
778
- totalUsage.outputTokens += roundUsage.outputTokens;
779
- totalUsage.cachedTokens += roundUsage.cachedTokens ?? 0;
780
- totalCost += sdkResponse.usage?.cost ?? 0;
781
- allItems.push(...roundItems);
782
- const functionCalls = roundItems.filter(isFunctionCall);
783
- if (functionCalls.length === 0 || !request.tools) {
784
- break;
785
- }
786
- emitIfAllowed('tool_round_started', {
787
- round,
788
- toolCount: functionCalls.length,
789
- });
790
- for (const fc of functionCalls) {
791
- conversationInput.push({
792
- type: 'function_call',
793
- callId: fc.callId,
794
- id: fc.id ?? crypto.randomUUID(),
795
- name: fc.name,
796
- arguments: fc.arguments,
797
- });
798
- }
799
- for (const fc of functionCalls) {
800
- if (signal?.aborted) {
801
- break;
802
- }
803
- emitIfAllowed('tool_call_started', {
804
- name: fc.name,
805
- callId: fc.callId,
806
- });
807
- let parsedArgs;
808
- try {
809
- parsedArgs = JSON.parse(fc.arguments);
810
- }
811
- catch {
812
- const errorOutput = `Error: malformed JSON in tool arguments: ${fc.arguments}`;
813
- const toolForCall = request.tools.find((t) => t.name === fc.name);
814
- const outputItem = createToolResultItem({
815
- output: errorOutput,
816
- callId: fc.callId,
817
- roundItemSchemas,
818
- tool: toolForCall,
819
- callItem: fc,
820
- error: true,
821
- });
822
- allItems.push(outputItem);
823
- conversationInput.push({
824
- type: 'function_call_output',
825
- callId: fc.callId,
826
- output: errorOutput,
827
- });
828
- emitIfAllowed('tool_call_completed', {
829
- name: fc.name,
830
- callId: fc.callId,
831
- error: true,
832
- });
833
- continue;
834
- }
835
- const toolForCall = request.tools.find((t) => t.name === fc.name);
836
- const toolResult = await executeToolCall({
837
- toolName: fc.name,
838
- args: parsedArgs,
839
- tools: request.tools,
840
- context: request.ctx,
841
- harness: this,
842
- layers: request.layers,
843
- });
844
- const { output } = toolResult;
845
- const outputItem = createToolResultItem({
846
- output,
847
- callId: fc.callId,
848
- roundItemSchemas,
849
- tool: toolForCall,
850
- callItem: fc,
851
- args: parsedArgs,
852
- result: toolResult.result,
853
- error: toolResult.error,
854
- });
855
- allItems.push(outputItem);
856
- conversationInput.push({
857
- type: 'function_call_output',
858
- callId: fc.callId,
859
- output,
860
- });
861
- emitIfAllowed('tool_call_completed', {
862
- name: fc.name,
863
- callId: fc.callId,
864
- error: false,
865
- });
866
- }
867
- emitIfAllowed('tool_round_completed', {
868
- round,
869
- toolCount: functionCalls.length,
870
- });
871
- round += 1;
872
- if (round >= MAX_TOOL_ROUNDS) {
873
- emitIfAllowed('tool_round_limit_exceeded', {
874
- maxToolRounds: MAX_TOOL_ROUNDS,
875
- attempt: toolLimitRecoveryContinuations + 1,
876
- maxAttempts: MAX_RECOVERY_CONTINUATIONS,
877
- });
878
- if (toolLimitRecoveryContinuations >= MAX_RECOVERY_CONTINUATIONS) {
879
- throw new Error(`LLM exceeded maximum tool rounds (${MAX_TOOL_ROUNDS})`);
880
- }
881
- toolLimitRecoveryContinuations += 1;
882
- useEphemeralContinue = true;
883
- }
884
- }
885
- return {
886
- items: allItems,
887
- usage: totalUsage,
888
- cost: totalCost > 0 ? totalCost : undefined,
889
- };
890
- }
891
- /** @internal Drain only messages tagged `between-rounds` from the queue. */
892
- drainBetweenRoundsMessages(queue) {
893
- const pending = queue.peekAll();
894
- const toInject = [];
895
- const keep = [];
896
- for (const msg of pending) {
897
- if (msg.deliveryMode === 'between-rounds') {
898
- toInject.push(msg);
899
- continue;
900
- }
901
- keep.push(msg);
902
- }
903
- if (toInject.length === 0) {
904
- return [];
905
- }
906
- // Replace queue contents: re-prepend kept messages in order after draining.
907
- queue.drainAll();
908
- for (const msg of keep) {
909
- queue.enqueue(msg);
910
- }
911
- return toInject;
912
- }
913
- //#endregion
914
- async run(s, input, ctx) {
915
- return execute(s, input, ctx);
916
- }
917
- /** Run all layer `init` hooks before the first step executes. Per spec 11,
918
- * init MUST complete before any recall fires so layer state is populated. */
919
- async initAndRun(s, input, ctx) {
920
- const layers = ctx.layers;
921
- const storage = this.config.storage;
922
- if (layers && layers.length > 0 && storage) {
923
- await this.initLayers(layers, ctx, storage);
924
- }
925
- return execute(s, input, ctx);
926
- }
927
- detachedSpawn(s, input, parentCtx, overrides) {
928
- const childCtx = this.createContext({
929
- parent: parentCtx,
930
- threadId: overrides?.threadId ?? parentCtx.threadId,
931
- resourceId: overrides?.resourceId ?? parentCtx.resourceId,
932
- cwdInit: overrides?.cwdInit,
933
- });
934
- const promise = this.run(s, input, childCtx);
935
- return new DetachedHandleImpl(childCtx.id, promise);
936
- }
937
- createContext(opts) {
938
- const { memory: memoryLayers, cwdInit, ...rest } = opts ?? {};
939
- const effectiveMemory = memoryLayers ?? this._memory;
940
- const itemSchemas = buildItemSchemaRegistry({
941
- base: this.itemSchemas,
942
- layers: effectiveMemory,
943
- });
944
- return new ContextImpl({
945
- ...rest,
946
- harness: this,
947
- channelStore: this.channelStore,
948
- layers: effectiveMemory,
949
- itemSchemas,
950
- cwdState: resolveContextCwdState(this.rootCwdState, opts?.parent, cwdInit),
951
- });
952
- }
953
- send(channel, value, _ctx) {
954
- this.channelStore.send(channel, value);
955
- }
956
- recv(channel, _ctx, opts) {
957
- return this.channelStore.recv(channel, opts?.timeout);
958
- }
959
- tryRecv(channel, _ctx) {
960
- return this.channelStore.tryRecv(channel);
961
- }
962
- getChannelHandle(channel, executionId) {
963
- return this.channelStore.getHandle(channel, executionId);
964
- }
965
- /** Resolves layer-provided tools and merges with step tools into ctx.unifiedTools. */
966
- setUnifiedTools(ctx, stepTools) {
967
- const layers = ctx.layers;
968
- const layerTools = layers && layers.length > 0 ? resolveLayerTools(layers, this, ctx) : [];
969
- const allTools = deduplicateTools([
970
- ...stepTools,
971
- ...layerTools,
972
- ]);
973
- if (allTools.length > 0) {
974
- const impl = frameworkCast(ctx);
975
- impl.unifiedTools = allTools;
976
- }
977
- }
978
- toExecCtx(ctx) {
979
- return contextToExecCtx(ctx, {
980
- callModel: (request) => this.callModel(request),
981
- });
982
- }
983
- async initLayers(layers, ctx, storage) {
984
- await initLayers({
985
- layers,
986
- ctx: this.toExecCtx(ctx),
987
- storage,
988
- store: this.layerStateStore,
989
- });
990
- }
991
- async recallLayers(layers, input, ctx) {
992
- return recallLayers({
993
- layers,
994
- query: input,
995
- ctx: this.toExecCtx(ctx),
996
- log: ctx.itemLog,
997
- budgets: resolveLayerBudgets(layers),
998
- store: this.layerStateStore,
999
- itemSchemas: this.itemSchemas,
1000
- });
1001
- }
1002
- /**
1003
- * Compute the items array that would be sent to the model on the next turn —
1004
- * the same arrangement `executeLLM` builds: harness-level memory layers'
1005
- * recall outputs concatenated with the session's accumulated history.
1006
- *
1007
- * Read-mostly: `recallLayers` writes layer-state snapshots to
1008
- * `layerStateStore` exactly as a real turn would, so successive previews
1009
- * remain consistent with what the next real turn produces.
1010
- */
1011
- async previewRequestItems(scope) {
1012
- const threadId = scope?.threadId ?? DEFAULT_THREAD_ID;
1013
- // Read-only: if the session doesn't exist, treat history as empty rather
1014
- // than allocating a SessionRunner for a debug/preview call.
1015
- const existingSession = this.sessions.get(threadId);
1016
- const historyItems = existingSession
1017
- ? [
1018
- ...existingSession.accumulatedItems,
1019
- ]
1020
- : [];
1021
- const ctx = this.createContext({
1022
- items: historyItems,
1023
- threadId,
1024
- memory: this._memory,
1025
- });
1026
- const layers = ctx.layers ?? [];
1027
- if (layers.length === 0) {
1028
- return historyItems;
1029
- }
1030
- const recallResults = await this.recallLayers(layers, '', ctx);
1031
- const layerOutputItems = recallResults.flatMap((r) => r.items);
1032
- if (layerOutputItems.length === 0) {
1033
- return historyItems;
1034
- }
1035
- return assembleView({
1036
- systemPromptItems: [],
1037
- layerOutputItems,
1038
- historyItems,
1039
- });
1040
- }
1041
- async storeLayers(layers, response, ctx) {
1042
- const storage = this.config.storage;
1043
- if (!storage) {
1044
- return;
1045
- }
1046
- await storeLayers({
1047
- layers,
1048
- response,
1049
- ctx: this.toExecCtx(ctx),
1050
- log: ctx.itemLog,
1051
- store: this.layerStateStore,
1052
- storage,
1053
- });
1054
- }
1055
- async disposeLayers(layers, ctx) {
1056
- await disposeLayers({
1057
- layers,
1058
- ctx: this.toExecCtx(ctx),
1059
- store: this.layerStateStore,
1060
- });
1061
- }
1062
- async checkpoint(_ctx) { }
1063
- async restore(_executionId) {
1064
- return null;
1065
- }
1066
- async cancel(_ctx, _reason) { }
1067
- createSpan(name, parent) {
1068
- return new SpanImpl(name, parent);
1069
- }
1070
- getLayerState(executionId, layerId) {
1071
- return this.layerStateStore.get(executionId, layerId);
1072
- }
1073
- setLayerState(executionId, layerId, state) {
1074
- this.layerStateStore.set(executionId, layerId, state);
1075
- }
1076
- async beforeToolCall(layers, toolName, toolArgs, ctx) {
1077
- const hasHook = layers.some((l) => l.hooks.beforeToolCall);
1078
- if (!hasHook) {
1079
- return {
1080
- action: SteeringAction.Allow,
1081
- };
1082
- }
1083
- return beforeToolCallLayers({
1084
- layers,
1085
- toolName,
1086
- toolArgs,
1087
- ctx: this.toExecCtx(ctx),
1088
- store: this.layerStateStore,
1089
- });
1090
- }
1091
- async afterModelCall(layers, response, ctx) {
1092
- const hasHook = layers.some((l) => l.hooks.afterModelCall);
1093
- if (!hasHook) {
1094
- return {
1095
- action: SteeringAction.Allow,
1096
- };
1097
- }
1098
- return afterModelCallLayers({
1099
- layers,
1100
- response,
1101
- ctx: this.toExecCtx(ctx),
1102
- store: this.layerStateStore,
1103
- });
1104
- }
1105
- async projectHistory(layers, items, ctx) {
1106
- const hasHook = layers.some((l) => l.hooks.projectHistory);
1107
- if (!hasHook) {
1108
- return items;
1109
- }
1110
- return projectHistoryLayers({
1111
- layers,
1112
- items,
1113
- ctx: this.toExecCtx(ctx),
1114
- store: this.layerStateStore,
1115
- });
1116
- }
1117
- async runAppendPipeline(layers, items, ctx) {
1118
- const hasHook = layers.some((l) => l.hooks.onItemAppend);
1119
- if (!hasHook) {
1120
- return {
1121
- items,
1122
- rerenderRequests: [],
1123
- };
1124
- }
1125
- return runAppendPipeline({
1126
- layers,
1127
- items,
1128
- ctx: this.toExecCtx(ctx),
1129
- log: ctx.itemLog,
1130
- store: this.layerStateStore,
1131
- });
1132
- }
1133
- async executeRerender(requests, layers, ctx, budgets, query) {
1134
- return executeRerender({
1135
- requests,
1136
- layers,
1137
- ctx: this.toExecCtx(ctx),
1138
- log: ctx.itemLog,
1139
- budgets,
1140
- store: this.layerStateStore,
1141
- query,
1142
- itemSchemas: this.itemSchemas,
1143
- });
1144
- }
1145
- }
1146
- //#endregion
1147
- //#region Text extraction helper
1148
- function itemToText(msg) {
1149
- if (typeof msg.input === 'string') {
1150
- return msg.input;
1151
- }
1152
- const items = Array.isArray(msg.input)
1153
- ? msg.input
1154
- : [
1155
- msg.input,
1156
- ];
1157
- const texts = [];
1158
- for (const item of items) {
1159
- if (item.type !== 'message') {
1160
- continue;
1161
- }
1162
- for (const part of item.content) {
1163
- if (part.type === 'input_text' || part.type === 'output_text') {
1164
- texts.push(part.text);
1165
- }
1166
- }
1167
- }
1168
- return texts.join('\n');
1169
- }
1170
- //#endregion