@noetic-tools/core 5.3.0 → 5.4.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 (94) hide show
  1. package/README.md +8 -6
  2. package/dist/adapters/in-memory-subprocess/step-completion.js +1 -1
  3. package/dist/adapters/openrouter.d.ts +12 -1
  4. package/dist/adapters/openrouter.js +79 -5
  5. package/dist/builders/acp-agent-tool.d.ts +67 -0
  6. package/dist/builders/acp-agent-tool.js +87 -0
  7. package/dist/builders/control-flow-builders.d.ts +13 -13
  8. package/dist/builders/control-flow-builders.js +11 -11
  9. package/dist/{patterns → builders}/dynamic-workflow.d.ts +1 -1
  10. package/dist/{patterns → builders}/dynamic-workflow.js +20 -14
  11. package/dist/builders/every.d.ts +14 -14
  12. package/dist/builders/every.js +16 -16
  13. package/dist/builders/layer-provides-builders.d.ts +1 -1
  14. package/dist/builders/layer-provides-builders.js +1 -1
  15. package/dist/builders/provide-builder.d.ts +5 -12
  16. package/dist/builders/provide-builder.js +9 -10
  17. package/dist/builders/spawn-builder.d.ts +0 -2
  18. package/dist/builders/spawn-builder.js +2 -3
  19. package/dist/builders/step-builders.d.ts +103 -92
  20. package/dist/builders/step-builders.js +146 -163
  21. package/dist/builders/tool-builder.d.ts +18 -22
  22. package/dist/builders/tool-builder.js +6 -4
  23. package/dist/builders/workflow-hydrator.d.ts +11 -7
  24. package/dist/builders/workflow-hydrator.js +75 -87
  25. package/dist/builders/workflow-step.d.ts +15 -27
  26. package/dist/builders/workflow-step.js +14 -19
  27. package/dist/conditions/conditions.d.ts +2 -2
  28. package/dist/conditions/conditions.js +1 -1
  29. package/dist/harness/agent-harness.d.ts +89 -46
  30. package/dist/harness/agent-harness.js +93 -39
  31. package/dist/harness/deps/types.d.ts +1 -1
  32. package/dist/harness/model-call.d.ts +1 -1
  33. package/dist/harness/model-call.js +23 -18
  34. package/dist/index.d.ts +27 -35
  35. package/dist/index.js +13 -29
  36. package/dist/interpreter/acp-events.d.ts +67 -0
  37. package/dist/interpreter/acp-events.js +256 -0
  38. package/dist/interpreter/acp-history.d.ts +22 -0
  39. package/dist/interpreter/acp-history.js +58 -0
  40. package/dist/interpreter/action-types.d.ts +1 -1
  41. package/dist/interpreter/collect-tools.js +10 -13
  42. package/dist/interpreter/execute-acp-agent.d.ts +10 -0
  43. package/dist/interpreter/execute-acp-agent.js +505 -0
  44. package/dist/interpreter/execute-action.d.ts +5 -5
  45. package/dist/interpreter/execute-action.js +284 -218
  46. package/dist/interpreter/execute-control.d.ts +9 -9
  47. package/dist/interpreter/execute-control.js +27 -27
  48. package/dist/interpreter/execute.d.ts +1 -1
  49. package/dist/interpreter/execute.js +29 -35
  50. package/dist/interpreter/step-dispatcher.d.ts +1 -1
  51. package/dist/interpreter/step-dispatcher.js +3 -3
  52. package/dist/interpreter/structured-output.d.ts +14 -0
  53. package/dist/interpreter/structured-output.js +40 -0
  54. package/dist/interpreter/typeguards.d.ts +1 -1
  55. package/dist/interpreter/typeguards.js +1 -1
  56. package/dist/portable/builders.d.ts +5 -3
  57. package/dist/portable/builders.js +8 -5
  58. package/dist/portable/context-layers.d.ts +1 -1
  59. package/dist/portable/context-layers.js +1 -1
  60. package/dist/runtime/acp-session-store.d.ts +75 -0
  61. package/dist/runtime/acp-session-store.js +0 -0
  62. package/dist/runtime/broadcaster-utils.d.ts +1 -1
  63. package/dist/runtime/broadcaster-utils.js +1 -1
  64. package/dist/runtime/context-impl.d.ts +8 -10
  65. package/dist/runtime/context-impl.js +8 -12
  66. package/dist/runtime/durable/step-ledger.d.ts +2 -2
  67. package/dist/runtime/durable/step-ledger.js +3 -3
  68. package/dist/runtime/session-runner.d.ts +12 -1
  69. package/dist/runtime/session-runner.js +33 -1
  70. package/dist/runtime/tool-context.js +0 -2
  71. package/dist/schemas/workflow.d.ts +3 -3
  72. package/dist/schemas/workflow.js +2 -2
  73. package/dist/types/checkpoint.d.ts +11 -2
  74. package/dist/types/checkpoint.js +10 -1
  75. package/dist/{runtime → util}/detached-handle.js +1 -1
  76. package/package.json +6 -4
  77. package/schema/noetic-workflow.schema.json +239 -260
  78. package/dist/builders/context-option.d.ts +0 -23
  79. package/dist/builders/context-option.js +0 -22
  80. package/dist/deprecated.d.ts +0 -30
  81. package/dist/deprecated.js +0 -28
  82. package/dist/interpreter/execute-sub-harness.d.ts +0 -8
  83. package/dist/interpreter/execute-sub-harness.js +0 -241
  84. package/dist/interpreter/sub-harness-events.d.ts +0 -48
  85. package/dist/interpreter/sub-harness-events.js +0 -208
  86. package/dist/patterns/interview.d.ts +0 -53
  87. package/dist/patterns/interview.js +0 -113
  88. package/dist/patterns/plans.d.ts +0 -44
  89. package/dist/patterns/plans.js +0 -125
  90. package/dist/patterns/ralph-wiggum.d.ts +0 -19
  91. package/dist/patterns/ralph-wiggum.js +0 -37
  92. package/dist/patterns/react.d.ts +0 -19
  93. package/dist/patterns/react.js +0 -41
  94. /package/dist/{runtime → util}/detached-handle.d.ts +0 -0
@@ -1,33 +1,21 @@
1
1
  import { ItemSchemaRegistry } from '@noetic-tools/types';
2
+ import { AcpSessionStore } from '../runtime/acp-session-store.js';
2
3
  import type { LayerStateStore, RecallCache } from './deps/context.js';
3
4
  import type { DetachedSpawnOverrides } from './deps/interpreter.js';
4
5
  import type { CheckpointStore, EventBroadcaster, RestoreCheckpointOptions, StepLedgerRetention, StepLedgerStore } from './deps/runtime.js';
5
- import type { AgentConfig, AgentHarnessContract, AgentHooks, CallModelRequest, Channel, ChannelHandle, Context, ContextCacheConfig, ContextCacheStore, ContextData, ContextLayer, CwdState, DeliveryMode, DetachedHandle, ExecuteInput, ExecuteOptions, ExternalChannel, FsAdapter, HarnessResponse, HarnessStatus, Item, ItemSchemaExtensions, LLMResponse, LlmProviderConfig, ProjectionPolicy, RecallLayerOutput, SessionScope, ShellAdapter, Span, SteeringDecision, Step, StorageAdapter, StreamEvent, StreamingItem, SubHarnessSession, SubprocessAdapter, Tool, TraceExporter, ZodType } from './deps/types.js';
6
+ import type { AcpLiveSession, AcpSessionInfo, AgentConfig, AgentHarnessContract, AgentHooks, CallModelRequest, Channel, ChannelHandle, Context, ContextCacheConfig, ContextCacheStore, ContextData, ContextLayer, CwdState, DeliveryMode, DetachedHandle, ExecuteInput, ExecuteOptions, ExternalChannel, FsAdapter, HarnessResponse, HarnessStatus, Item, ItemSchemaConfig, LLMResponse, LlmProviderConfig, ProjectionPolicy, RecallLayerOutput, SessionScope, SessionUsage, ShellAdapter, Span, SteeringDecision, Step, StorageAdapter, StreamEvent, StreamingItem, SubprocessAdapter, Tool, TraceExporter, ZodType } from './deps/types.js';
6
7
  export { createStreamIdleWatchdog } from './model-call.js';
7
- interface AgentHarnessOpts<TParams extends Record<string, unknown> = Record<string, unknown>> {
8
- name: string;
9
- initialStep?: Step<ContextData, string, string>;
10
- /** Default context layers applied to every context created via `createContext()` / `execute()`. */
11
- context?: ContextLayer[];
12
- /** @deprecated Renamed to `context`. */
13
- memory?: ContextLayer[];
14
- storage?: StorageAdapter;
15
- hooks?: AgentHooks;
8
+ /**
9
+ * Storage-scoped environment configuration for `AgentHarness`.
10
+ *
11
+ * @public
12
+ */
13
+ export interface StorageEnvironmentConfig {
16
14
  /**
17
- * Harness-wide tool pool. Merged (identity-deduplicated) with tools
18
- * collected from `initialStep` to form every context's `unifiedTools`.
19
- * Use this when the workflow step tree is static and tools are supplied
20
- * per harness instance rather than baked into individual `step.llm` calls.
15
+ * Key-value storage adapter backing context-layer persistence and the
16
+ * step-completion ledger. Defaults to an in-memory adapter.
21
17
  */
22
- tools?: Tool[];
23
- params: TParams;
24
- paramsSchema?: ZodType<TParams>;
25
- /** Filesystem adapter. Defaults to local node:fs when not provided. */
26
- fs?: FsAdapter;
27
- /** Shell adapter. Defaults to local sh when not provided. */
28
- shell?: ShellAdapter;
29
- /** Subprocess adapter. Defaults to an in-memory, same-process adapter. */
30
- subprocess?: SubprocessAdapter;
18
+ adapter?: StorageAdapter;
31
19
  /**
32
20
  * Checkpoint store used by `harness.checkpoint(ctx)` / `harness.restore()`.
33
21
  * When absent, checkpoint/restore are no-ops — a zero-config harness keeps
@@ -35,6 +23,8 @@ interface AgentHarnessOpts<TParams extends Record<string, unknown> = Record<stri
35
23
  * to enable durable execution.
36
24
  */
37
25
  checkpointStore?: CheckpointStore;
26
+ /** Layer-state store override. Defaults to an in-memory store. */
27
+ layerStateStore?: LayerStateStore;
38
28
  /**
39
29
  * Bounds on the step-completion ledger that backs step-level resume. Only meaningful
40
30
  * alongside a `checkpointStore`. Defaults to `DEFAULT_STEP_LEDGER_RETENTION` — 128 KiB
@@ -43,11 +33,45 @@ interface AgentHarnessOpts<TParams extends Record<string, unknown> = Record<stri
43
33
  * never correctness.
44
34
  */
45
35
  stepLedgerRetention?: StepLedgerRetention;
46
- llm?: LlmProviderConfig;
47
- /** Harness-wide item schema extensions. */
48
- itemSchemas?: ItemSchemaExtensions;
49
- /** Whether unknown extension item types must match a registered schema. Defaults to true. */
50
- strictItemSchemas?: boolean;
36
+ }
37
+ /**
38
+ * Execution-environment configuration for `AgentHarness`: the storage,
39
+ * filesystem, shell, and subprocess surfaces the agent runs against.
40
+ *
41
+ * @public
42
+ */
43
+ export interface AgentEnvironmentConfig {
44
+ /** Storage configuration: adapter, durability stores, ledger retention. */
45
+ storage?: StorageEnvironmentConfig;
46
+ /** Filesystem adapter. Defaults to an in-memory adapter. */
47
+ fs?: FsAdapter;
48
+ /** Shell adapter. Defaults to an in-memory adapter. */
49
+ shell?: ShellAdapter;
50
+ /** Subprocess adapter. Defaults to an in-memory, same-process adapter. */
51
+ subprocess?: SubprocessAdapter;
52
+ }
53
+ interface AgentHarnessOpts<TParams extends Record<string, unknown> = Record<string, unknown>> {
54
+ name: string;
55
+ /** Root step tree executed for every turn submitted via `execute()`. */
56
+ agentGraph?: Step<ContextData, string, string>;
57
+ /** Default context layers applied to every context created via `createContext()` / `execute()`. */
58
+ contextLayers?: ContextLayer[];
59
+ hooks?: AgentHooks;
60
+ /**
61
+ * Harness-wide tool pool. Merged (identity-deduplicated) with tools
62
+ * collected from `agentGraph` to form every context's `unifiedTools`.
63
+ * Use this when the workflow step tree is static and tools are supplied
64
+ * per harness instance rather than baked into individual `step.callModel` calls.
65
+ */
66
+ tools?: Tool[];
67
+ params: TParams;
68
+ paramsSchema?: ZodType<TParams>;
69
+ /** Execution environment: storage, filesystem, shell, subprocess. */
70
+ environment?: AgentEnvironmentConfig;
71
+ /** Default provider configuration for model calls made through this harness. */
72
+ callModelDefaults?: LlmProviderConfig;
73
+ /** Harness-wide item schema extensions and validation strictness. */
74
+ itemSchemas?: ItemSchemaConfig;
51
75
  /** Default projection policy for all LLM steps. Individual steps override via `step.projection`. */
52
76
  projection?: ProjectionPolicy;
53
77
  /** When true, every layer is recalled atomically regardless of its `recallMode`. */
@@ -55,15 +79,8 @@ interface AgentHarnessOpts<TParams extends Record<string, unknown> = Record<stri
55
79
  /** Tuning for prompt-cache anchoring. See `ContextCacheConfig` for the defaults. */
56
80
  contextCache?: ContextCacheConfig;
57
81
  traceExporter?: TraceExporter;
58
- layerStateStore?: LayerStateStore;
59
82
  /** Default delivery mode for messages that don't specify one. Defaults to `next-turn`. */
60
83
  defaultDeliveryMode?: DeliveryMode;
61
- /**
62
- * Abort the in-flight model call if the provider stream emits no events for this
63
- * many milliseconds. Defaults to `DEFAULT_STREAM_IDLE_TIMEOUT_MS` (120s).
64
- * Pass `0` or a negative number to disable the watchdog.
65
- */
66
- streamIdleTimeoutMs?: number;
67
84
  /**
68
85
  * Initial working directory for the harness. Used as the seed value for the
69
86
  * shared `cwdState.cwd` on every Context this harness creates, including
@@ -129,7 +146,7 @@ export declare class AgentHarness<TParams extends Record<string, unknown> = Reco
129
146
  * @internal
130
147
  */
131
148
  readonly stepLedgerRetention: Required<StepLedgerRetention>;
132
- private readonly initialStep?;
149
+ private readonly agentGraph?;
133
150
  /** Harness-wide tool pool merged into every context's `unifiedTools`. */
134
151
  private readonly harnessTools;
135
152
  /** @internal Context layers configured for this harness. Exposed non-private
@@ -142,15 +159,42 @@ export declare class AgentHarness<TParams extends Record<string, unknown> = Reco
142
159
  private readonly rootRunDepth;
143
160
  private readonly callModelOverride?;
144
161
  private readonly defaultDeliveryMode;
145
- private readonly streamIdleTimeoutMs;
146
162
  private readonly sessions;
147
163
  /**
148
- * @internal Cross-step harness sessions keyed by `step.session.reuse`, kept
149
- * alive for the life of this harness so reused coding-agent sessions span
150
- * multiple steps. Reached by the interpreter's harness handler via
151
- * `frameworkCast`; do not access from outside core.
164
+ * @internal Live ACP connections. Reached by the interpreter via
165
+ * `frameworkCast`; use {@link listAcpSessions}, {@link getAcpSession},
166
+ * {@link cancelAcpSession}, and {@link closeAcpSessions} from outside core.
167
+ */
168
+ readonly acpSessions: AcpSessionStore;
169
+ /**
170
+ * Snapshot of every live ACP sub-agent, for a UI that wants to show what is
171
+ * running and let a user act on it. Read-only: a session's turns are driven
172
+ * by steps, so nothing here can start work behind the runtime's back.
173
+ * @public
174
+ */
175
+ listAcpSessions(): AcpSessionInfo[];
176
+ /**
177
+ * The live connection + session behind a handle from {@link listAcpSessions}
178
+ * (or a step's `session.reuse` key), or `undefined` when nothing matches.
179
+ * @public
180
+ */
181
+ getAcpSession(key: string): AcpLiveSession | undefined;
182
+ /**
183
+ * Interrupt whatever the sub-agent under `key` is doing, via `session/cancel`.
184
+ * The connection stays open and the turn still returns — with the `cancelled`
185
+ * stop reason, which the running step surfaces as a `cancelled` error.
186
+ * Returns false when nothing matches the key.
187
+ * @public
188
+ */
189
+ cancelAcpSession(key: string): Promise<boolean>;
190
+ /**
191
+ * Close every ACP connection this harness still holds, whatever its
192
+ * keep-alive scope. A connection owns a live agent — usually a child process
193
+ * whose stdio keeps the event loop alive — so a `keepAlive: 'harness'`
194
+ * session must be closed by its owner or the host will not exit. Idempotent.
195
+ * @public
152
196
  */
153
- readonly subHarnessSessions: Map<string, SubHarnessSession>;
197
+ closeAcpSessions(): Promise<void>;
154
198
  readonly layerStateStore: LayerStateStore;
155
199
  /** Per-harness memoization cache for `recallMode: 'eventual'` layers. */
156
200
  readonly recallCache: RecallCache;
@@ -193,6 +237,7 @@ export declare class AgentHarness<TParams extends Record<string, unknown> = Reco
193
237
  }): Promise<void>;
194
238
  getStatus(scope?: SessionScope): HarnessStatus;
195
239
  getQueueSize(scope?: SessionScope): number;
240
+ getUsage(scope?: SessionScope): SessionUsage;
196
241
  seedSessionHistory(threadId: string, items: ReadonlyArray<Item>): void;
197
242
  private getOrCreateSession;
198
243
  private requireSession;
@@ -225,9 +270,7 @@ export declare class AgentHarness<TParams extends Record<string, unknown> = Reco
225
270
  state?: unknown;
226
271
  threadId?: string;
227
272
  resourceId?: string;
228
- context?: ContextLayer[];
229
- /** @deprecated Renamed to `context`. */
230
- memory?: ContextLayer[];
273
+ contextLayers?: ContextLayer[];
231
274
  /**
232
275
  * Initial cwd for the new context. When set, takes precedence over both
233
276
  * the parent snapshot and the harness root cwd — used by worktree
@@ -332,7 +375,7 @@ export declare class AgentHarness<TParams extends Record<string, unknown> = Reco
332
375
  *
333
376
  * Aborting a context rejects everything blocked on it (channel `recv`
334
377
  * waiters, parked senders) with `{ kind: 'cancelled' }`, stops its in-flight
335
- * model call and sub-harness turn, and makes the next step boundary throw
378
+ * model call and ACP agent turn, and makes the next step boundary throw
336
379
  * `cancelled`.
337
380
  *
338
381
  * Cancellation is cooperative: a layer hook already in flight is allowed to
@@ -1,6 +1,7 @@
1
1
  import { frameworkCast, ItemSchemaRegistry, NoeticConfigError } from '@noetic-tools/types';
2
2
  import { SpanImpl } from '../observability/span-impl.js';
3
3
  import { NoopExporter } from '../observability/trace-exporter.js';
4
+ import { AcpSessionStore } from '../runtime/acp-session-store.js';
4
5
  import { createInMemoryFsAdapter, createInMemoryShellAdapter, createInMemorySubprocessAdapter, OpenRouter, } from './deps/adapters.js';
5
6
  import { afterModelCallLayers, allocateBudgets, assembleView, beforeToolCallLayers, completeLayers, contextToExecCtx, createContextCacheStore, createLayerStateStore, createRecallCache, DEFAULT_PROJECTION, disposeLayers, executeRerender, initLayers, projectHistoryLayers, recallLayers, recallLayersAtomic, recallLayersEventual, resolveLayerTools, runAppendPipeline, storeLayers, } from './deps/context.js';
6
7
  import { collectAllTools, deduplicateTools, dispatchStepThroughAdapter, execute, prepareBandedView, } from './deps/interpreter.js';
@@ -8,7 +9,6 @@ import { buildItemStream, ChannelStore, ContextImpl, captureCheckpoint, clearChe
8
9
  import { SteeringAction } from './deps/types.js';
9
10
  import { AgentHarnessModelCaller } from './model-call.js';
10
11
  export { createStreamIdleWatchdog } from './model-call.js';
11
- import { resolveContextOption } from '../builders/context-option.js';
12
12
  import { buildItemSchemaRegistry } from './model-schema.js';
13
13
  //#endregion
14
14
  const DEFAULT_THREAD_ID = '__default__';
@@ -178,7 +178,7 @@ export class AgentHarness {
178
178
  * @internal
179
179
  */
180
180
  stepLedgerRetention;
181
- initialStep;
181
+ agentGraph;
182
182
  /** Harness-wide tool pool merged into every context's `unifiedTools`. */
183
183
  harnessTools;
184
184
  /** @internal Context layers configured for this harness. Exposed non-private
@@ -191,15 +191,55 @@ export class AgentHarness {
191
191
  rootRunDepth = new Map();
192
192
  callModelOverride;
193
193
  defaultDeliveryMode;
194
- streamIdleTimeoutMs;
195
194
  sessions = new Map();
196
195
  /**
197
- * @internal Cross-step harness sessions keyed by `step.session.reuse`, kept
198
- * alive for the life of this harness so reused coding-agent sessions span
199
- * multiple steps. Reached by the interpreter's harness handler via
200
- * `frameworkCast`; do not access from outside core.
196
+ * @internal Live ACP connections. Reached by the interpreter via
197
+ * `frameworkCast`; use {@link listAcpSessions}, {@link getAcpSession},
198
+ * {@link cancelAcpSession}, and {@link closeAcpSessions} from outside core.
201
199
  */
202
- subHarnessSessions = new Map();
200
+ acpSessions = new AcpSessionStore();
201
+ /**
202
+ * Snapshot of every live ACP sub-agent, for a UI that wants to show what is
203
+ * running and let a user act on it. Read-only: a session's turns are driven
204
+ * by steps, so nothing here can start work behind the runtime's back.
205
+ * @public
206
+ */
207
+ listAcpSessions() {
208
+ return this.acpSessions.list();
209
+ }
210
+ /**
211
+ * The live connection + session behind a handle from {@link listAcpSessions}
212
+ * (or a step's `session.reuse` key), or `undefined` when nothing matches.
213
+ * @public
214
+ */
215
+ getAcpSession(key) {
216
+ return this.acpSessions.get(key);
217
+ }
218
+ /**
219
+ * Interrupt whatever the sub-agent under `key` is doing, via `session/cancel`.
220
+ * The connection stays open and the turn still returns — with the `cancelled`
221
+ * stop reason, which the running step surfaces as a `cancelled` error.
222
+ * Returns false when nothing matches the key.
223
+ * @public
224
+ */
225
+ async cancelAcpSession(key) {
226
+ const entry = this.acpSessions.get(key);
227
+ if (!entry) {
228
+ return false;
229
+ }
230
+ await entry.session.cancel();
231
+ return true;
232
+ }
233
+ /**
234
+ * Close every ACP connection this harness still holds, whatever its
235
+ * keep-alive scope. A connection owns a live agent — usually a child process
236
+ * whose stdio keeps the event loop alive — so a `keepAlive: 'harness'`
237
+ * session must be closed by its owner or the host will not exit. Idempotent.
238
+ * @public
239
+ */
240
+ async closeAcpSessions() {
241
+ await this.acpSessions.closeAll();
242
+ }
203
243
  layerStateStore;
204
244
  /** Per-harness memoization cache for `recallMode: 'eventual'` layers. */
205
245
  recallCache;
@@ -225,45 +265,47 @@ export class AgentHarness {
225
265
  itemSchemas;
226
266
  constructor(opts) {
227
267
  const validatedParams = opts.paramsSchema ? opts.paramsSchema.parse(opts.params) : opts.params;
268
+ const environment = opts.environment;
269
+ const storageEnv = environment?.storage;
228
270
  this.config = {
229
271
  name: opts.name,
230
- storage: opts.storage ?? createInMemoryStorage(),
272
+ storage: storageEnv?.adapter ?? createInMemoryStorage(),
231
273
  hooks: opts.hooks,
232
274
  params: validatedParams,
233
275
  itemSchemas: opts.itemSchemas,
234
- strictItemSchemas: opts.strictItemSchemas ?? true,
235
276
  projection: opts.projection,
236
277
  forceAtomicRecall: opts.forceAtomicRecall,
237
278
  contextCache: opts.contextCache,
238
279
  };
239
- this.fs = opts.fs ?? createInMemoryFsAdapter();
240
- this.shell = opts.shell ?? createInMemoryShellAdapter();
241
- this.subprocess = opts.subprocess ?? createInMemorySubprocessAdapter();
242
- this.checkpointStore = opts.checkpointStore;
243
- this.stepLedgerStore = opts.checkpointStore
280
+ this.fs = environment?.fs ?? createInMemoryFsAdapter();
281
+ this.shell = environment?.shell ?? createInMemoryShellAdapter();
282
+ this.subprocess = environment?.subprocess ?? createInMemorySubprocessAdapter();
283
+ this.checkpointStore = storageEnv?.checkpointStore;
284
+ this.stepLedgerStore = storageEnv?.checkpointStore
244
285
  ? createStepLedgerStore({
245
286
  storage: this.config.storage ?? createInMemoryStorage(),
246
287
  })
247
288
  : undefined;
248
- this.stepLedgerRetention = resolveStepLedgerRetention(opts.stepLedgerRetention);
249
- this.initialStep = opts.initialStep;
289
+ this.stepLedgerRetention = resolveStepLedgerRetention(storageEnv?.stepLedgerRetention);
290
+ this.agentGraph = opts.agentGraph;
250
291
  this.harnessTools = opts.tools ?? [];
251
- this._contextLayers = resolveContextOption(opts);
292
+ this._contextLayers = opts.contextLayers;
252
293
  this.callModelOverride = opts._testCallModel;
253
- this.client = opts._testCallModel ? undefined : createClient(opts.llm, opts.contextCache);
294
+ this.client = opts._testCallModel
295
+ ? undefined
296
+ : createClient(opts.callModelDefaults, opts.contextCache);
254
297
  this.channelStore = new ChannelStore();
255
298
  this.traceExporter = opts.traceExporter ?? new NoopExporter();
256
299
  this.layerStateStore =
257
- opts.layerStateStore ??
300
+ storageEnv?.layerStateStore ??
258
301
  createLayerStateStore((layerId, hook, error) => {
259
302
  console.warn(`[noetic] context layer '${layerId}' ${hook} error:`, error);
260
303
  });
261
304
  this.recallCache = createRecallCache();
262
305
  this.contextCache = createContextCacheStore();
263
306
  this.defaultDeliveryMode = opts.defaultDeliveryMode ?? 'next-turn';
264
- this.streamIdleTimeoutMs = opts.streamIdleTimeoutMs ?? DEFAULT_STREAM_IDLE_TIMEOUT_MS;
265
- this.itemSchemas = new ItemSchemaRegistry(opts.itemSchemas, {
266
- strictUnknownExtensions: opts.strictItemSchemas ?? true,
307
+ this.itemSchemas = new ItemSchemaRegistry(opts.itemSchemas?.schemas, {
308
+ strictUnknownExtensions: opts.itemSchemas?.strict ?? true,
267
309
  });
268
310
  this.rootCwdState = {
269
311
  cwd: opts.initialCwd ?? '/',
@@ -284,11 +326,11 @@ export class AgentHarness {
284
326
  }
285
327
  //#region Session Accessors
286
328
  execute(input, options) {
287
- if (!this.initialStep) {
329
+ if (!this.agentGraph) {
288
330
  return Promise.reject(new NoeticConfigError({
289
331
  code: 'NO_STEP_CONFIGURED',
290
- message: 'No initialStep configured on this harness.',
291
- hint: 'Pass `initialStep` in constructor options, or use run() directly.',
332
+ message: 'No agentGraph configured on this harness.',
333
+ hint: 'Pass `agentGraph` in constructor options, or use run() directly.',
292
334
  }));
293
335
  }
294
336
  const threadId = options?.threadId ?? DEFAULT_THREAD_ID;
@@ -353,6 +395,17 @@ export class AgentHarness {
353
395
  const session = this.sessions.get(threadId);
354
396
  return session ? session.runner.queue.size : 0;
355
397
  }
398
+ getUsage(scope) {
399
+ const threadId = scope?.threadId ?? DEFAULT_THREAD_ID;
400
+ const session = this.sessions.get(threadId);
401
+ if (!session) {
402
+ return {
403
+ inputTokens: 0,
404
+ outputTokens: 0,
405
+ };
406
+ }
407
+ return session.runner.getUsage();
408
+ }
356
409
  seedSessionHistory(threadId, items) {
357
410
  const session = this.getOrCreateSession(threadId);
358
411
  session.accumulatedItems = [
@@ -385,15 +438,15 @@ export class AgentHarness {
385
438
  threadId,
386
439
  resourceId: perTurnOptions.resourceId,
387
440
  state: perTurnOptions.state,
388
- context: resolveContextOption(perTurnOptions),
441
+ contextLayers: perTurnOptions.contextLayers,
389
442
  _broadcaster: session.runner.broadcaster,
390
443
  });
391
444
  const ext = frameworkCast(ctx);
392
445
  ext._sessionQueue = session.runner.queue;
393
446
  ext._sessionBetweenRounds = true;
394
447
  ext._sessionRunnerAgentName = this.config.name;
395
- if (this.initialStep || this.harnessTools.length > 0) {
396
- const stepTools = this.initialStep ? collectAllTools(this.initialStep) : [];
448
+ if (this.agentGraph || this.harnessTools.length > 0) {
449
+ const stepTools = this.agentGraph ? collectAllTools(this.agentGraph) : [];
397
450
  this.setUnifiedTools(ctx, [
398
451
  ...stepTools,
399
452
  ...this.harnessTools,
@@ -402,11 +455,11 @@ export class AgentHarness {
402
455
  return ctx;
403
456
  },
404
457
  runTurn: async (ctx, _turn, signal) => {
405
- if (!this.initialStep) {
458
+ if (!this.agentGraph) {
406
459
  throw new NoeticConfigError({
407
460
  code: 'NO_STEP_CONFIGURED',
408
- message: 'No initialStep configured on this harness.',
409
- hint: 'Pass `initialStep` in constructor options.',
461
+ message: 'No agentGraph configured on this harness.',
462
+ hint: 'Pass `agentGraph` in constructor options.',
410
463
  });
411
464
  }
412
465
  // Wire signal-abort to context-abort so the interpreter bails cleanly.
@@ -420,7 +473,7 @@ export class AgentHarness {
420
473
  once: true,
421
474
  });
422
475
  }
423
- const result = await this.initAndRun(this.initialStep, '', ctx);
476
+ const result = await this.initAndRun(this.agentGraph, '', ctx);
424
477
  // Snapshot final items into session history for the next turn.
425
478
  session.accumulatedItems = [
426
479
  ...ctx.itemLog.items,
@@ -450,7 +503,7 @@ export class AgentHarness {
450
503
  itemSchemas: this.itemSchemas,
451
504
  client: this.client,
452
505
  callModelOverride: this.callModelOverride,
453
- streamIdleTimeoutMs: this.streamIdleTimeoutMs,
506
+ streamIdleTimeoutMs: DEFAULT_STREAM_IDLE_TIMEOUT_MS,
454
507
  harness: this,
455
508
  traceExporter: this.traceExporter,
456
509
  }).callModel(request);
@@ -509,6 +562,7 @@ export class AgentHarness {
509
562
  else {
510
563
  this.rootRunDepth.delete(ctx.id);
511
564
  this.channelStore.closeExecution(ctx.id);
565
+ await this.acpSessions.closeOwnedBy(ctx.id);
512
566
  }
513
567
  }
514
568
  }
@@ -540,10 +594,10 @@ export class AgentHarness {
540
594
  }
541
595
  createContext(opts) {
542
596
  const resolved = opts ?? {};
543
- // `ContextImpl` takes the layers as `layers`, so both option spellings are
597
+ // `ContextImpl` takes the layers as `layers`, so the option spelling is
544
598
  // dropped from `rest` rather than spread through under the wrong name.
545
- const { context: _context, memory: _memory, cwdInit, ...rest } = resolved;
546
- const effectiveLayers = resolveContextOption(resolved) ?? this._contextLayers;
599
+ const { contextLayers, cwdInit, ...rest } = resolved;
600
+ const effectiveLayers = contextLayers ?? this._contextLayers;
547
601
  const itemSchemas = buildItemSchemaRegistry({
548
602
  base: this.itemSchemas,
549
603
  layers: effectiveLayers,
@@ -696,7 +750,7 @@ export class AgentHarness {
696
750
  const ctx = this.createContext({
697
751
  items: historyItems,
698
752
  threadId,
699
- context: this._contextLayers,
753
+ contextLayers: this._contextLayers,
700
754
  });
701
755
  const layers = ctx.layers ?? [];
702
756
  if (layers.length === 0) {
@@ -837,7 +891,7 @@ export class AgentHarness {
837
891
  *
838
892
  * Aborting a context rejects everything blocked on it (channel `recv`
839
893
  * waiters, parked senders) with `{ kind: 'cancelled' }`, stops its in-flight
840
- * model call and sub-harness turn, and makes the next step boundary throw
894
+ * model call and ACP agent turn, and makes the next step boundary throw
841
895
  * `cancelled`.
842
896
  *
843
897
  * Cancellation is cooperative: a layer hook already in flight is allowed to
@@ -1,4 +1,4 @@
1
1
  export type { ContextCacheConfig, ContextCacheStore, ContextData, ContextLayer, ExecutionContext, ProjectionPolicy, StorageAdapter, } from '@noetic-tools/context';
2
- export type { AgentConfig, AgentHarnessContract, AgentHooks, CallModelRequest, Channel, ChannelHandle, Context, CwdState, DeliveryMode, DetachedHandle, ExecuteInput, ExecuteOptions, ExternalChannel, FsAdapter, HarnessResponse, HarnessStatus, Item, ItemSchemaExtensions, LLMResponse, LlmProviderConfig, RecallLayerOutput, SessionScope, ShellAdapter, Span, SteeringDecision, Step, StreamEvent, StreamingItem, SubHarnessSession, SubprocessAdapter, Tool, TraceExporter, } from '@noetic-tools/types';
2
+ export type { AcpKeepAlive, AcpLiveSession, AcpSessionInfo, AgentConfig, AgentHarnessContract, AgentHooks, CallModelRequest, Channel, ChannelHandle, Context, CwdState, DeliveryMode, DetachedHandle, ExecuteInput, ExecuteOptions, ExternalChannel, FsAdapter, HarnessResponse, HarnessStatus, Item, ItemSchemaConfig, ItemSchemaExtensions, LLMResponse, LlmProviderConfig, RecallLayerOutput, SessionScope, SessionUsage, ShellAdapter, Span, SteeringDecision, Step, StreamEvent, StreamingItem, SubprocessAdapter, Tool, TraceExporter, } from '@noetic-tools/types';
3
3
  export { SteeringAction } from '@noetic-tools/types';
4
4
  export type { ZodType } from 'zod';
@@ -10,7 +10,7 @@ interface StreamIdleWatchdog {
10
10
  /** @internal Create a watchdog that aborts `controller` when no
11
11
  * {@link StreamIdleWatchdog.reset reset} is called within `timeoutMs`. When
12
12
  * `timeoutMs <= 0`, returns an inert no-op so callers can always call `.reset()`
13
- * / `.stop()` without a branch. Starts armed: the caller is responsible for
13
+ * / `.stop()` without branching. Starts armed: the caller is responsible for
14
14
  * `.stop()` in a `finally`. `onTimeout` runs before the abort so observers can
15
15
  * emit a framework event with the original cause. Exported only for unit tests. */
16
16
  export declare function createStreamIdleWatchdog(timeoutMs: number, controller: AbortController, onTimeout?: () => void): StreamIdleWatchdog;
@@ -1,7 +1,6 @@
1
1
  import { frameworkCast, NoeticConfigError } from '@noetic-tools/types';
2
2
  import { serverTool } from '@openrouter/agent';
3
- import { z } from 'zod';
4
- import { convertTools, executeToolCall, extractOutputItems, extractSystemInstruction, extractUsage, itemsToInput, sanitizeToolNameForWire, } from '../adapters/openrouter.js';
3
+ import { convertTools, executeToolCall, extractOutputItems, extractSystemInstruction, extractUsage, itemsToInput, resolveWireJsonSchema, sanitizeToolNameForWire, } from '../adapters/openrouter.js';
5
4
  import { isFunctionCall } from '../interpreter/typeguards.js';
6
5
  import { GenAI, NoeticAttr, ToolAttr } from '../observability/genai-attributes.js';
7
6
  import { emitFrameworkEvent, getBroadcaster, shouldEmit } from '../runtime/broadcaster-utils.js';
@@ -9,8 +8,12 @@ import { buildItemSchemaRegistry, createToolResultItem } from './model-schema.js
9
8
  const MAX_TOOL_ROUNDS = 32;
10
9
  const MAX_RECOVERY_CONTINUATIONS = 3;
11
10
  const EPHEMERAL_CONTINUE_INPUT = 'continue';
12
- function buildTextFormat(schema) {
13
- const jsonSchema = z.toJSONSchema(schema);
11
+ function buildTextFormat(schema, explicitJsonSchema) {
12
+ const jsonSchema = resolveWireJsonSchema({
13
+ schema,
14
+ explicitJsonSchema,
15
+ what: 'structured output schema',
16
+ });
14
17
  return {
15
18
  format: {
16
19
  type: 'json_schema',
@@ -133,7 +136,7 @@ async function pipeStreamEventsToBroadcaster(opts) {
133
136
  /** @internal Create a watchdog that aborts `controller` when no
134
137
  * {@link StreamIdleWatchdog.reset reset} is called within `timeoutMs`. When
135
138
  * `timeoutMs <= 0`, returns an inert no-op so callers can always call `.reset()`
136
- * / `.stop()` without a branch. Starts armed: the caller is responsible for
139
+ * / `.stop()` without branching. Starts armed: the caller is responsible for
137
140
  * `.stop()` in a `finally`. `onTimeout` runs before the abort so observers can
138
141
  * emit a framework event with the original cause. Exported only for unit tests. */
139
142
  export function createStreamIdleWatchdog(timeoutMs, controller, onTimeout) {
@@ -150,7 +153,7 @@ export function createStreamIdleWatchdog(timeoutMs, controller, onTimeout) {
150
153
  if (stopped) {
151
154
  return;
152
155
  }
153
- const reason = new Error(`llm stream idle timeout after ${timeoutMs}ms`);
156
+ const reason = new Error(`model stream idle timeout after ${timeoutMs}ms`);
154
157
  onTimeout?.();
155
158
  controller.abort(reason);
156
159
  }, timeoutMs);
@@ -354,7 +357,7 @@ export class AgentHarnessModelCaller {
354
357
  throw new NoeticConfigError({
355
358
  code: 'NO_LLM_PROVIDER',
356
359
  message: 'No LLM provider configured on this harness.',
357
- hint: 'Pass `llm: { apiKey: "..." }` (defaults to the Noetic platform) or set NOETIC_API_KEY. For direct OpenRouter, use `llm: { provider: "openrouter", apiKey: "..." }` or set OPENROUTER_API_KEY.',
360
+ hint: 'Pass `callModelDefaults: { apiKey: "..." }` (defaults to the Noetic platform) or set NOETIC_API_KEY. For direct OpenRouter, use `callModelDefaults: { provider: "openrouter", apiKey: "..." }` or set OPENROUTER_API_KEY.',
358
361
  });
359
362
  }
360
363
  async callModel(request) {
@@ -384,7 +387,9 @@ export class AgentHarnessModelCaller {
384
387
  const rounds = [];
385
388
  let totalCost = 0;
386
389
  const conversationInput = itemsToInput(prepared.remaining);
387
- const textFormat = request.outputSchema ? buildTextFormat(request.outputSchema) : undefined;
390
+ const textFormat = request.outputSchema
391
+ ? buildTextFormat(request.outputSchema, request.outputJsonSchema)
392
+ : undefined;
388
393
  let round = 0;
389
394
  let invalidRecoveryContinuations = 0;
390
395
  let toolLimitRecoveryContinuations = 0;
@@ -406,7 +411,7 @@ export class AgentHarnessModelCaller {
406
411
  let idleStalled = false;
407
412
  const watchdog = createStreamIdleWatchdog(this.opts.streamIdleTimeoutMs, roundController, () => {
408
413
  idleStalled = true;
409
- prepared.emitIfAllowed('llm_call_stalled', {
414
+ prepared.emitIfAllowed('model_call_stalled', {
410
415
  round,
411
416
  idleTimeoutMs: this.opts.streamIdleTimeoutMs,
412
417
  });
@@ -414,7 +419,7 @@ export class AgentHarnessModelCaller {
414
419
  const modelInput = recoveryContinuation
415
420
  ? withEphemeralContinueInput(conversationInput)
416
421
  : frameworkCast(conversationInput);
417
- prepared.emitIfAllowed('llm_call_started', {
422
+ prepared.emitIfAllowed('model_call_started', {
418
423
  round,
419
424
  messageCount: modelInput.length,
420
425
  toolCount: prepared.sdkTools?.length ?? 0,
@@ -446,7 +451,7 @@ export class AgentHarnessModelCaller {
446
451
  watchdog.reset();
447
452
  if (!firstEventSeen) {
448
453
  firstEventSeen = true;
449
- prepared.emitIfAllowed('llm_call_first_event', {
454
+ prepared.emitIfAllowed('model_call_first_event', {
450
455
  round,
451
456
  });
452
457
  }
@@ -462,7 +467,7 @@ export class AgentHarnessModelCaller {
462
467
  if (idleStalled) {
463
468
  throw roundSignal.reason instanceof Error
464
469
  ? roundSignal.reason
465
- : new Error(`llm stream idle timeout after ${this.opts.streamIdleTimeoutMs}ms`);
470
+ : new Error(`model stream idle timeout after ${this.opts.streamIdleTimeoutMs}ms`);
466
471
  }
467
472
  if (request.signal?.aborted) {
468
473
  break;
@@ -507,7 +512,7 @@ export class AgentHarnessModelCaller {
507
512
  continue;
508
513
  }
509
514
  invalidRecoveryContinuations = 0;
510
- prepared.emitIfAllowed('llm_call_completed', {
515
+ prepared.emitIfAllowed('model_call_completed', {
511
516
  round,
512
517
  itemCount: sdkResponse.output?.length ?? 0,
513
518
  });
@@ -605,7 +610,7 @@ export class AgentHarnessModelCaller {
605
610
  kind: 'ok',
606
611
  };
607
612
  }
608
- params.emitIfAllowed('llm_call_failed', {
613
+ params.emitIfAllowed('model_call_failed', {
609
614
  round: params.round,
610
615
  status: terminalError.status,
611
616
  error: terminalError.message,
@@ -615,7 +620,7 @@ export class AgentHarnessModelCaller {
615
620
  throw new Error(terminalError.message);
616
621
  }
617
622
  const next = params.invalidRecoveryContinuations + 1;
618
- params.emitIfAllowed('llm_call_recovery_continue', {
623
+ params.emitIfAllowed('model_call_recovery_continue', {
619
624
  round: params.round,
620
625
  status: terminalError.status,
621
626
  attempt: next,
@@ -633,7 +638,7 @@ export class AgentHarnessModelCaller {
633
638
  };
634
639
  }
635
640
  const message = 'LLM response completed with no output items';
636
- params.emitIfAllowed('llm_call_failed', {
641
+ params.emitIfAllowed('model_call_failed', {
637
642
  round: params.round,
638
643
  status: 'completed',
639
644
  error: message,
@@ -643,7 +648,7 @@ export class AgentHarnessModelCaller {
643
648
  throw new Error(message);
644
649
  }
645
650
  const next = params.invalidRecoveryContinuations + 1;
646
- params.emitIfAllowed('llm_call_recovery_continue', {
651
+ params.emitIfAllowed('model_call_recovery_continue', {
647
652
  round: params.round,
648
653
  status: 'completed',
649
654
  attempt: next,
@@ -750,7 +755,7 @@ export class AgentHarnessModelCaller {
750
755
  recordMalformedToolCall(params) {
751
756
  const { fc, request, allItems, conversationInput } = params;
752
757
  const errorOutput = `Error: malformed JSON in tool arguments: ${fc.arguments}`;
753
- const toolForCall = request.tools?.find((tool) => tool.name === fc.name);
758
+ const toolForCall = request.tools?.find((tool) => tool.name === fc.name || sanitizeToolNameForWire(tool.name) === fc.name);
754
759
  // Owner-scoped result validation (see executeFunctionCall).
755
760
  const outputItem = createToolResultItem({
756
761
  output: errorOutput,