@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
@@ -0,0 +1,694 @@
1
+ import { z } from 'zod';
2
+ import { convertTools, executeToolCall, extractOutputItems, extractSystemInstruction, extractUsage, itemsToInput, sanitizeToolNameForWire, } from '../adapters/openrouter.js';
3
+ import { NoeticConfigError } from '../errors/noetic-config-error.js';
4
+ import { isFunctionCall } from '../interpreter/typeguards.js';
5
+ import { emitFrameworkEvent, getBroadcaster, shouldEmit } from '../runtime/broadcaster-utils.js';
6
+ import { frameworkCast } from '../util/framework-cast.js';
7
+ import { buildItemSchemaRegistry, createToolResultItem } from './model-schema.js';
8
+ const MAX_TOOL_ROUNDS = 32;
9
+ const MAX_RECOVERY_CONTINUATIONS = 3;
10
+ const EPHEMERAL_CONTINUE_INPUT = 'continue';
11
+ function buildTextFormat(schema) {
12
+ const jsonSchema = z.toJSONSchema(schema);
13
+ return {
14
+ format: {
15
+ type: 'json_schema',
16
+ name: 'output',
17
+ schema: jsonSchema,
18
+ },
19
+ };
20
+ }
21
+ function isStreamRecord(value) {
22
+ return typeof value === 'object' && value !== null;
23
+ }
24
+ function providerTerminalError(response) {
25
+ if (!isStreamRecord(response) || response.status === undefined) {
26
+ return null;
27
+ }
28
+ const status = String(response.status);
29
+ if (status === 'completed') {
30
+ return null;
31
+ }
32
+ if (status === 'incomplete') {
33
+ const details = response.incompleteDetails;
34
+ const reason = isStreamRecord(details) && typeof details.reason === 'string' ? `: ${details.reason}` : '';
35
+ return {
36
+ status,
37
+ message: `LLM response incomplete${reason}`,
38
+ };
39
+ }
40
+ if (status === 'failed') {
41
+ const error = response.error;
42
+ const message = isStreamRecord(error) && typeof error.message === 'string' ? `: ${error.message}` : '';
43
+ return {
44
+ status,
45
+ message: `LLM response failed${message}`,
46
+ };
47
+ }
48
+ return {
49
+ status,
50
+ message: `LLM response ended with status '${status}'`,
51
+ };
52
+ }
53
+ function hasUsableResponseOutput(response, items) {
54
+ if (items.length > 0) {
55
+ return true;
56
+ }
57
+ return (isStreamRecord(response) &&
58
+ typeof response.outputText === 'string' &&
59
+ response.outputText.length > 0);
60
+ }
61
+ function withEphemeralContinueInput(input) {
62
+ return [
63
+ ...frameworkCast(input),
64
+ frameworkCast({
65
+ type: 'message',
66
+ role: 'user',
67
+ content: EPHEMERAL_CONTINUE_INPUT,
68
+ }),
69
+ ];
70
+ }
71
+ /** Race a promise against an AbortSignal so callers (e.g. `SessionRunner.abort`)
72
+ * can break out of a long `await` without waiting for the underlying call to
73
+ * settle. When the signal fires, the returned promise rejects with
74
+ * `signal.reason` (an `Error`) or a generic `Error('aborted')` fallback. */
75
+ function awaitWithAbort(p, signal) {
76
+ if (!signal) {
77
+ return p;
78
+ }
79
+ if (signal.aborted) {
80
+ return Promise.reject(signal.reason instanceof Error ? signal.reason : new Error('aborted'));
81
+ }
82
+ return new Promise((resolve, reject) => {
83
+ const onAbort = () => {
84
+ reject(signal.reason instanceof Error ? signal.reason : new Error('aborted'));
85
+ };
86
+ signal.addEventListener('abort', onAbort, {
87
+ once: true,
88
+ });
89
+ p.then((v) => {
90
+ signal.removeEventListener('abort', onAbort);
91
+ resolve(v);
92
+ }, (e) => {
93
+ signal.removeEventListener('abort', onAbort);
94
+ reject(e);
95
+ });
96
+ });
97
+ }
98
+ async function pipeStreamEventsToBroadcaster(opts) {
99
+ const { stream, broadcaster, agentName, signal, onEvent } = opts;
100
+ try {
101
+ for await (const event of stream) {
102
+ if (signal?.aborted) {
103
+ return;
104
+ }
105
+ onEvent?.();
106
+ if (!isStreamRecord(event)) {
107
+ continue;
108
+ }
109
+ broadcaster?.emit({
110
+ source: 'sdk',
111
+ type: typeof event.type === 'string' ? event.type : 'unknown',
112
+ data: event,
113
+ outputIndex: typeof event.outputIndex === 'number' ? event.outputIndex : undefined,
114
+ contentIndex: typeof event.contentIndex === 'number' ? event.contentIndex : undefined,
115
+ });
116
+ }
117
+ }
118
+ catch (err) {
119
+ if (broadcaster) {
120
+ emitFrameworkEvent({
121
+ broadcaster,
122
+ agentName,
123
+ eventType: 'stream_pipe_error',
124
+ data: {
125
+ error: err instanceof Error ? err.message : String(err),
126
+ },
127
+ });
128
+ }
129
+ throw err;
130
+ }
131
+ }
132
+ /** @internal Create a watchdog that aborts `controller` when no
133
+ * {@link StreamIdleWatchdog.reset reset} is called within `timeoutMs`. When
134
+ * `timeoutMs <= 0`, returns an inert no-op so callers can always call `.reset()`
135
+ * / `.stop()` without a branch. Starts armed: the caller is responsible for
136
+ * `.stop()` in a `finally`. `onTimeout` runs before the abort so observers can
137
+ * emit a framework event with the original cause. Exported only for unit tests. */
138
+ export function createStreamIdleWatchdog(timeoutMs, controller, onTimeout) {
139
+ if (timeoutMs <= 0) {
140
+ return {
141
+ reset: () => { },
142
+ stop: () => { },
143
+ };
144
+ }
145
+ let timer;
146
+ let stopped = false;
147
+ const arm = () => {
148
+ timer = setTimeout(() => {
149
+ if (stopped) {
150
+ return;
151
+ }
152
+ const reason = new Error(`llm stream idle timeout after ${timeoutMs}ms`);
153
+ onTimeout?.();
154
+ controller.abort(reason);
155
+ }, timeoutMs);
156
+ };
157
+ arm();
158
+ return {
159
+ reset: () => {
160
+ if (stopped || !timer) {
161
+ return;
162
+ }
163
+ clearTimeout(timer);
164
+ arm();
165
+ },
166
+ stop: () => {
167
+ stopped = true;
168
+ if (timer) {
169
+ clearTimeout(timer);
170
+ timer = undefined;
171
+ }
172
+ },
173
+ };
174
+ }
175
+ function hasSessionQueue(ctx) {
176
+ const maybe = frameworkCast(ctx);
177
+ return (maybe._sessionBetweenRounds === true &&
178
+ maybe._sessionQueue !== undefined &&
179
+ typeof maybe._sessionRunnerAgentName === 'string');
180
+ }
181
+ function prepareModelRequest(request, agentName) {
182
+ const { instructions: extractedInstructions, remaining } = extractSystemInstruction(request.items);
183
+ const instructions = [
184
+ request.instructions,
185
+ extractedInstructions,
186
+ ]
187
+ .filter(Boolean)
188
+ .join('\n\n') || undefined;
189
+ const broadcaster = getBroadcaster(request.ctx);
190
+ const filteredTools = filterAllowedTools(request);
191
+ return {
192
+ instructions,
193
+ remaining,
194
+ broadcaster,
195
+ sdkTools: filteredTools && filteredTools.length > 0
196
+ ? convertTools({
197
+ tools: filteredTools,
198
+ })
199
+ : undefined,
200
+ emitIfAllowed(eventType, data) {
201
+ if (!shouldEmit(request.emit, eventType, data)) {
202
+ return;
203
+ }
204
+ emitFrameworkEvent({
205
+ broadcaster,
206
+ agentName,
207
+ eventType,
208
+ data,
209
+ });
210
+ },
211
+ };
212
+ }
213
+ function filterAllowedTools(request) {
214
+ if (!request.tools) {
215
+ return undefined;
216
+ }
217
+ if (!('allowedToolNames' in request) || !request.allowedToolNames) {
218
+ return request.tools;
219
+ }
220
+ const allowedNames = new Set(request.allowedToolNames);
221
+ return request.tools.filter((tool) => allowedNames.has(tool.name));
222
+ }
223
+ export class AgentHarnessModelCaller {
224
+ opts;
225
+ constructor(opts) {
226
+ this.opts = opts;
227
+ }
228
+ async callOverriddenModel(request) {
229
+ if (!this.opts.callModelOverride) {
230
+ throw new Error('No callModel override configured.');
231
+ }
232
+ const response = await this.opts.callModelOverride(request);
233
+ const itemSchemas = buildItemSchemaRegistry({
234
+ base: this.opts.itemSchemas,
235
+ layers: request.layers,
236
+ tools: request.tools,
237
+ });
238
+ return {
239
+ ...response,
240
+ items: itemSchemas.parseMany(response.items),
241
+ };
242
+ }
243
+ requireModelClient() {
244
+ if (this.opts.client) {
245
+ return this.opts.client;
246
+ }
247
+ throw new NoeticConfigError({
248
+ code: 'NO_LLM_PROVIDER',
249
+ message: 'No LLM provider configured on this harness.',
250
+ hint: 'Pass `llm: { provider: "openrouter", apiKey: "..." }` in constructor options or set OPENROUTER_API_KEY.',
251
+ });
252
+ }
253
+ async callModel(request) {
254
+ if (this.opts.callModelOverride) {
255
+ return this.callOverriddenModel(request);
256
+ }
257
+ const client = this.requireModelClient();
258
+ const prepared = prepareModelRequest(request, this.opts.agentName);
259
+ const allItems = [];
260
+ const totalUsage = {
261
+ inputTokens: 0,
262
+ outputTokens: 0,
263
+ cachedTokens: 0,
264
+ };
265
+ let totalCost = 0;
266
+ const conversationInput = itemsToInput(prepared.remaining);
267
+ const textFormat = request.outputSchema ? buildTextFormat(request.outputSchema) : undefined;
268
+ let round = 0;
269
+ let invalidRecoveryContinuations = 0;
270
+ let toolLimitRecoveryContinuations = 0;
271
+ let useEphemeralContinue = false;
272
+ while (!request.signal?.aborted) {
273
+ const recoveryContinuation = useEphemeralContinue;
274
+ useEphemeralContinue = false;
275
+ if (round > 0 && request.ctx && hasSessionQueue(request.ctx)) {
276
+ this.injectBetweenRoundMessages(request.ctx, conversationInput, round, prepared.emitIfAllowed);
277
+ }
278
+ const roundController = new AbortController();
279
+ const roundSignal = request.signal
280
+ ? AbortSignal.any([
281
+ request.signal,
282
+ roundController.signal,
283
+ ])
284
+ : roundController.signal;
285
+ let firstEventSeen = false;
286
+ let idleStalled = false;
287
+ const watchdog = createStreamIdleWatchdog(this.opts.streamIdleTimeoutMs, roundController, () => {
288
+ idleStalled = true;
289
+ prepared.emitIfAllowed('llm_call_stalled', {
290
+ round,
291
+ idleTimeoutMs: this.opts.streamIdleTimeoutMs,
292
+ });
293
+ });
294
+ const modelInput = recoveryContinuation
295
+ ? withEphemeralContinueInput(conversationInput)
296
+ : frameworkCast(conversationInput);
297
+ prepared.emitIfAllowed('llm_call_started', {
298
+ round,
299
+ messageCount: modelInput.length,
300
+ toolCount: prepared.sdkTools?.length ?? 0,
301
+ recoveryContinuation,
302
+ });
303
+ const callResult = client.callModel({
304
+ model: request.model,
305
+ input: modelInput,
306
+ instructions: prepared.instructions,
307
+ tools: prepared.sdkTools,
308
+ temperature: request.params?.temperature,
309
+ maxOutputTokens: request.params?.maxTokens,
310
+ topP: request.params?.topP,
311
+ ...(textFormat
312
+ ? {
313
+ text: textFormat,
314
+ }
315
+ : {}),
316
+ }, {
317
+ signal: roundSignal,
318
+ });
319
+ const pipePromise = pipeStreamEventsToBroadcaster({
320
+ stream: callResult.getFullResponsesStream(),
321
+ broadcaster: prepared.broadcaster,
322
+ agentName: this.opts.agentName,
323
+ signal: roundSignal,
324
+ onEvent: () => {
325
+ watchdog.reset();
326
+ if (!firstEventSeen) {
327
+ firstEventSeen = true;
328
+ prepared.emitIfAllowed('llm_call_first_event', {
329
+ round,
330
+ });
331
+ }
332
+ },
333
+ });
334
+ let sdkResponse;
335
+ try {
336
+ sdkResponse = await awaitWithAbort(callResult.getResponse(), roundSignal);
337
+ watchdog.stop();
338
+ await awaitWithAbort(pipePromise, roundSignal);
339
+ }
340
+ catch (err) {
341
+ if (idleStalled) {
342
+ throw roundSignal.reason instanceof Error
343
+ ? roundSignal.reason
344
+ : new Error(`llm stream idle timeout after ${this.opts.streamIdleTimeoutMs}ms`);
345
+ }
346
+ if (request.signal?.aborted) {
347
+ break;
348
+ }
349
+ throw err;
350
+ }
351
+ finally {
352
+ watchdog.stop();
353
+ }
354
+ if (request.signal?.aborted) {
355
+ break;
356
+ }
357
+ const recovery = this.handleModelRecovery({
358
+ sdkResponse,
359
+ round,
360
+ invalidRecoveryContinuations,
361
+ emitIfAllowed: prepared.emitIfAllowed,
362
+ });
363
+ if (recovery.kind === 'continue') {
364
+ invalidRecoveryContinuations = recovery.invalidRecoveryContinuations;
365
+ useEphemeralContinue = true;
366
+ continue;
367
+ }
368
+ const roundItemSchemas = buildItemSchemaRegistry({
369
+ base: this.opts.itemSchemas,
370
+ layers: request.layers,
371
+ tools: request.tools,
372
+ });
373
+ const roundItems = roundItemSchemas.parseMany(extractOutputItems(sdkResponse));
374
+ const outputRecovery = this.handleEmptyOutputRecovery({
375
+ sdkResponse,
376
+ roundItems,
377
+ round,
378
+ invalidRecoveryContinuations,
379
+ emitIfAllowed: prepared.emitIfAllowed,
380
+ });
381
+ if (outputRecovery.kind === 'continue') {
382
+ invalidRecoveryContinuations = outputRecovery.invalidRecoveryContinuations;
383
+ useEphemeralContinue = true;
384
+ continue;
385
+ }
386
+ invalidRecoveryContinuations = 0;
387
+ prepared.emitIfAllowed('llm_call_completed', {
388
+ round,
389
+ itemCount: sdkResponse.output?.length ?? 0,
390
+ });
391
+ const roundUsage = extractUsage(sdkResponse.usage);
392
+ totalUsage.inputTokens += roundUsage.inputTokens;
393
+ totalUsage.outputTokens += roundUsage.outputTokens;
394
+ totalUsage.cachedTokens += roundUsage.cachedTokens ?? 0;
395
+ totalCost += sdkResponse.usage?.cost ?? 0;
396
+ allItems.push(...roundItems);
397
+ const functionCalls = roundItems.filter(isFunctionCall);
398
+ if (functionCalls.length === 0 || !request.tools) {
399
+ break;
400
+ }
401
+ await this.executeToolRound({
402
+ functionCalls,
403
+ request,
404
+ roundItemSchemas,
405
+ allItems,
406
+ conversationInput,
407
+ round,
408
+ emitIfAllowed: prepared.emitIfAllowed,
409
+ });
410
+ round += 1;
411
+ const limitRecovery = this.handleToolRoundLimit({
412
+ round,
413
+ toolLimitRecoveryContinuations,
414
+ emitIfAllowed: prepared.emitIfAllowed,
415
+ });
416
+ if (limitRecovery.kind === 'continue') {
417
+ toolLimitRecoveryContinuations = limitRecovery.toolLimitRecoveryContinuations;
418
+ useEphemeralContinue = true;
419
+ }
420
+ }
421
+ return {
422
+ items: allItems,
423
+ usage: totalUsage,
424
+ cost: totalCost > 0 ? totalCost : undefined,
425
+ };
426
+ }
427
+ injectBetweenRoundMessages(ctx, conversationInput, round, emitIfAllowed) {
428
+ const injected = this.drainBetweenRoundsMessages(ctx._sessionQueue);
429
+ if (injected.length === 0) {
430
+ return;
431
+ }
432
+ for (const msg of injected) {
433
+ const text = itemToText(msg);
434
+ const userItem = {
435
+ id: `user-${crypto.randomUUID()}`,
436
+ type: 'message',
437
+ role: 'user',
438
+ status: 'completed',
439
+ content: [
440
+ {
441
+ type: 'input_text',
442
+ text,
443
+ },
444
+ ],
445
+ };
446
+ conversationInput.push({
447
+ type: 'message',
448
+ role: 'user',
449
+ content: [
450
+ {
451
+ type: 'input_text',
452
+ text,
453
+ },
454
+ ],
455
+ });
456
+ ctx.itemLog.append(userItem);
457
+ }
458
+ emitIfAllowed('inbox_injected', {
459
+ round,
460
+ count: injected.length,
461
+ messageIds: injected.map((i) => i.id),
462
+ });
463
+ }
464
+ handleModelRecovery(params) {
465
+ const terminalError = providerTerminalError(params.sdkResponse);
466
+ if (!terminalError) {
467
+ return {
468
+ kind: 'ok',
469
+ };
470
+ }
471
+ params.emitIfAllowed('llm_call_failed', {
472
+ round: params.round,
473
+ status: terminalError.status,
474
+ error: terminalError.message,
475
+ recoverable: params.invalidRecoveryContinuations < MAX_RECOVERY_CONTINUATIONS,
476
+ });
477
+ if (params.invalidRecoveryContinuations >= MAX_RECOVERY_CONTINUATIONS) {
478
+ throw new Error(terminalError.message);
479
+ }
480
+ const next = params.invalidRecoveryContinuations + 1;
481
+ params.emitIfAllowed('llm_call_recovery_continue', {
482
+ round: params.round,
483
+ status: terminalError.status,
484
+ attempt: next,
485
+ maxAttempts: MAX_RECOVERY_CONTINUATIONS,
486
+ });
487
+ return {
488
+ kind: 'continue',
489
+ invalidRecoveryContinuations: next,
490
+ };
491
+ }
492
+ handleEmptyOutputRecovery(params) {
493
+ if (hasUsableResponseOutput(params.sdkResponse, params.roundItems)) {
494
+ return {
495
+ kind: 'ok',
496
+ };
497
+ }
498
+ const message = 'LLM response completed with no output items';
499
+ params.emitIfAllowed('llm_call_failed', {
500
+ round: params.round,
501
+ status: 'completed',
502
+ error: message,
503
+ recoverable: params.invalidRecoveryContinuations < MAX_RECOVERY_CONTINUATIONS,
504
+ });
505
+ if (params.invalidRecoveryContinuations >= MAX_RECOVERY_CONTINUATIONS) {
506
+ throw new Error(message);
507
+ }
508
+ const next = params.invalidRecoveryContinuations + 1;
509
+ params.emitIfAllowed('llm_call_recovery_continue', {
510
+ round: params.round,
511
+ status: 'completed',
512
+ attempt: next,
513
+ maxAttempts: MAX_RECOVERY_CONTINUATIONS,
514
+ });
515
+ return {
516
+ kind: 'continue',
517
+ invalidRecoveryContinuations: next,
518
+ };
519
+ }
520
+ async executeToolRound(params) {
521
+ const { functionCalls, request, roundItemSchemas, allItems, conversationInput } = params;
522
+ params.emitIfAllowed('tool_round_started', {
523
+ round: params.round,
524
+ toolCount: functionCalls.length,
525
+ });
526
+ for (const fc of functionCalls) {
527
+ conversationInput.push({
528
+ type: 'function_call',
529
+ callId: fc.callId,
530
+ id: fc.id ?? crypto.randomUUID(),
531
+ name: fc.name,
532
+ arguments: fc.arguments,
533
+ });
534
+ }
535
+ for (const fc of functionCalls) {
536
+ if (request.signal?.aborted) {
537
+ break;
538
+ }
539
+ await this.executeFunctionCall({
540
+ fc,
541
+ request,
542
+ roundItemSchemas,
543
+ allItems,
544
+ conversationInput,
545
+ emitIfAllowed: params.emitIfAllowed,
546
+ });
547
+ }
548
+ params.emitIfAllowed('tool_round_completed', {
549
+ round: params.round,
550
+ toolCount: functionCalls.length,
551
+ });
552
+ }
553
+ async executeFunctionCall(params) {
554
+ const { fc, request, roundItemSchemas, allItems, conversationInput } = params;
555
+ params.emitIfAllowed('tool_call_started', {
556
+ name: fc.name,
557
+ callId: fc.callId,
558
+ });
559
+ let parsedArgs;
560
+ try {
561
+ parsedArgs = JSON.parse(fc.arguments);
562
+ }
563
+ catch {
564
+ this.recordMalformedToolCall({
565
+ fc,
566
+ request,
567
+ roundItemSchemas,
568
+ allItems,
569
+ conversationInput,
570
+ emitIfAllowed: params.emitIfAllowed,
571
+ });
572
+ return;
573
+ }
574
+ if (!request.tools) {
575
+ throw new Error(`executeFunctionCall invoked without tools on CallModelRequest (tool name: ${fc.name}).`);
576
+ }
577
+ const toolForCall = request.tools.find((tool) => tool.name === fc.name || sanitizeToolNameForWire(tool.name) === fc.name);
578
+ const toolResult = await executeToolCall({
579
+ toolName: fc.name,
580
+ args: parsedArgs,
581
+ tools: request.tools,
582
+ context: request.ctx,
583
+ harness: this.opts.harness,
584
+ layers: request.layers,
585
+ });
586
+ const outputItem = createToolResultItem({
587
+ output: toolResult.output,
588
+ callId: fc.callId,
589
+ roundItemSchemas,
590
+ tool: toolForCall,
591
+ callItem: fc,
592
+ args: parsedArgs,
593
+ result: toolResult.result,
594
+ error: toolResult.error,
595
+ });
596
+ allItems.push(outputItem);
597
+ conversationInput.push({
598
+ type: 'function_call_output',
599
+ callId: fc.callId,
600
+ output: toolResult.output,
601
+ });
602
+ params.emitIfAllowed('tool_call_completed', {
603
+ name: fc.name,
604
+ callId: fc.callId,
605
+ error: false,
606
+ });
607
+ }
608
+ recordMalformedToolCall(params) {
609
+ const { fc, request, roundItemSchemas, allItems, conversationInput } = params;
610
+ const errorOutput = `Error: malformed JSON in tool arguments: ${fc.arguments}`;
611
+ const toolForCall = request.tools?.find((tool) => tool.name === fc.name);
612
+ const outputItem = createToolResultItem({
613
+ output: errorOutput,
614
+ callId: fc.callId,
615
+ roundItemSchemas,
616
+ tool: toolForCall,
617
+ callItem: fc,
618
+ error: true,
619
+ });
620
+ allItems.push(outputItem);
621
+ conversationInput.push({
622
+ type: 'function_call_output',
623
+ callId: fc.callId,
624
+ output: errorOutput,
625
+ });
626
+ params.emitIfAllowed('tool_call_completed', {
627
+ name: fc.name,
628
+ callId: fc.callId,
629
+ error: true,
630
+ });
631
+ }
632
+ handleToolRoundLimit(params) {
633
+ if (params.round < MAX_TOOL_ROUNDS) {
634
+ return {
635
+ kind: 'ok',
636
+ };
637
+ }
638
+ params.emitIfAllowed('tool_round_limit_exceeded', {
639
+ maxToolRounds: MAX_TOOL_ROUNDS,
640
+ attempt: params.toolLimitRecoveryContinuations + 1,
641
+ maxAttempts: MAX_RECOVERY_CONTINUATIONS,
642
+ });
643
+ if (params.toolLimitRecoveryContinuations >= MAX_RECOVERY_CONTINUATIONS) {
644
+ throw new Error(`LLM exceeded maximum tool rounds (${MAX_TOOL_ROUNDS})`);
645
+ }
646
+ return {
647
+ kind: 'continue',
648
+ toolLimitRecoveryContinuations: params.toolLimitRecoveryContinuations + 1,
649
+ };
650
+ }
651
+ /** @internal Drain only messages tagged `between-rounds` from the queue. */
652
+ drainBetweenRoundsMessages(queue) {
653
+ const pending = queue.peekAll();
654
+ const toInject = [];
655
+ const keep = [];
656
+ for (const msg of pending) {
657
+ if (msg.deliveryMode === 'between-rounds') {
658
+ toInject.push(msg);
659
+ continue;
660
+ }
661
+ keep.push(msg);
662
+ }
663
+ if (toInject.length === 0) {
664
+ return [];
665
+ }
666
+ queue.drainAll();
667
+ for (const msg of keep) {
668
+ queue.enqueue(msg);
669
+ }
670
+ return toInject;
671
+ }
672
+ }
673
+ function itemToText(msg) {
674
+ if (typeof msg.input === 'string') {
675
+ return msg.input;
676
+ }
677
+ const items = Array.isArray(msg.input)
678
+ ? msg.input
679
+ : [
680
+ msg.input,
681
+ ];
682
+ const texts = [];
683
+ for (const item of items) {
684
+ if (item.type !== 'message') {
685
+ continue;
686
+ }
687
+ for (const part of item.content) {
688
+ if (part.type === 'input_text' || part.type === 'output_text') {
689
+ texts.push(part.text);
690
+ }
691
+ }
692
+ }
693
+ return texts.join('\n');
694
+ }
@@ -0,0 +1,19 @@
1
+ import type { ItemSchemaRegistry } from '../schemas/item.js';
2
+ import type { Item } from '../types/items.js';
3
+ import type { MemoryLayer } from '../types/memory.js';
4
+ import type { Tool } from '../types/tool.js';
5
+ export declare function buildItemSchemaRegistry({ base, layers, tools, }: {
6
+ base: ItemSchemaRegistry;
7
+ layers?: ReadonlyArray<MemoryLayer>;
8
+ tools?: ReadonlyArray<Tool>;
9
+ }): ItemSchemaRegistry;
10
+ export declare function createToolResultItem({ output, callId, roundItemSchemas, tool, callItem, args, result, error, }: {
11
+ output: string;
12
+ callId: string;
13
+ roundItemSchemas: ItemSchemaRegistry;
14
+ tool?: Tool;
15
+ callItem?: Item;
16
+ args?: unknown;
17
+ result?: unknown;
18
+ error?: boolean;
19
+ }): Item;