@noetic-tools/core 0.0.1 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (202) hide show
  1. package/dist/adapters/in-memory-fs-adapter.js +1 -1
  2. package/dist/adapters/in-memory-subprocess/manifest-persistence.d.ts +8 -0
  3. package/dist/adapters/in-memory-subprocess/manifest-persistence.js +56 -0
  4. package/dist/adapters/in-memory-subprocess/metadata.d.ts +12 -0
  5. package/dist/adapters/in-memory-subprocess/metadata.js +58 -0
  6. package/dist/adapters/in-memory-subprocess/step-completion.d.ts +3 -0
  7. package/dist/adapters/in-memory-subprocess/step-completion.js +86 -0
  8. package/dist/adapters/in-memory-subprocess/types.d.ts +88 -0
  9. package/dist/adapters/in-memory-subprocess/types.js +2 -0
  10. package/dist/adapters/in-memory-subprocess-adapter.d.ts +12 -4
  11. package/dist/adapters/in-memory-subprocess-adapter.js +164 -55
  12. package/dist/adapters/openrouter.d.ts +11 -2
  13. package/dist/adapters/openrouter.js +66 -5
  14. package/dist/builders/control-flow-builders.js +8 -3
  15. package/dist/builders/every.js +4 -1
  16. package/dist/builders/layer-provides-builders.d.ts +1 -32
  17. package/dist/builders/layer-provides-builders.js +1 -33
  18. package/dist/builders/loop-builder.js +4 -1
  19. package/dist/builders/memory-builder.js +1 -1
  20. package/dist/builders/provide-builder.js +4 -1
  21. package/dist/builders/spawn-builder.d.ts +20 -7
  22. package/dist/builders/spawn-builder.js +16 -2
  23. package/dist/builders/step-builders.d.ts +49 -28
  24. package/dist/builders/step-builders.js +30 -11
  25. package/dist/builders/tool-builder.d.ts +5 -1
  26. package/dist/builders/tool-builder.js +2 -0
  27. package/dist/builders/workflow-hydrator.d.ts +38 -0
  28. package/dist/builders/workflow-hydrator.js +336 -0
  29. package/dist/conditions/conditions.js +1 -1
  30. package/dist/errors/noetic-error.js +2 -0
  31. package/dist/{runtime → harness}/agent-harness.d.ts +65 -45
  32. package/dist/harness/agent-harness.js +576 -0
  33. package/dist/harness/deps/adapters.d.ts +4 -0
  34. package/dist/harness/deps/adapters.js +4 -0
  35. package/dist/harness/deps/interpreter.d.ts +5 -0
  36. package/dist/harness/deps/interpreter.js +4 -0
  37. package/dist/harness/deps/memory.d.ts +5 -0
  38. package/dist/harness/deps/memory.js +4 -0
  39. package/dist/harness/deps/runtime.d.ts +10 -0
  40. package/dist/harness/deps/runtime.js +7 -0
  41. package/dist/harness/deps/types.d.ts +17 -0
  42. package/dist/harness/deps/types.js +1 -0
  43. package/dist/harness/model-call.d.ts +50 -0
  44. package/dist/harness/model-call.js +694 -0
  45. package/dist/harness/model-schema.d.ts +19 -0
  46. package/dist/harness/model-schema.js +46 -0
  47. package/dist/harness/step-bootstrap.d.ts +25 -0
  48. package/dist/harness/step-bootstrap.js +170 -0
  49. package/dist/index.d.ts +48 -12
  50. package/dist/index.js +29 -7
  51. package/dist/interpreter/action-deps.d.ts +12 -0
  52. package/dist/interpreter/action-deps.js +10 -0
  53. package/dist/interpreter/action-types.d.ts +8 -0
  54. package/dist/interpreter/action-types.js +1 -0
  55. package/dist/interpreter/collect-tools.d.ts +1 -1
  56. package/dist/interpreter/collect-tools.js +6 -2
  57. package/dist/interpreter/execute-action.d.ts +22 -0
  58. package/dist/interpreter/execute-action.js +527 -0
  59. package/dist/interpreter/{execute-every.d.ts → execute-control.d.ts} +7 -1
  60. package/dist/interpreter/execute-control.js +530 -0
  61. package/dist/interpreter/execute.d.ts +13 -0
  62. package/dist/interpreter/execute.js +151 -48
  63. package/dist/interpreter/message-helpers.d.ts +1 -9
  64. package/dist/interpreter/message-helpers.js +1 -42
  65. package/dist/interpreter/step-dispatcher.d.ts +61 -0
  66. package/dist/interpreter/step-dispatcher.js +67 -0
  67. package/dist/interpreter/typeguards.d.ts +3 -10
  68. package/dist/interpreter/typeguards.js +3 -20
  69. package/dist/{runtime → memory}/exec-context-factory.js +1 -1
  70. package/dist/memory/flow-schema.d.ts +52 -0
  71. package/dist/memory/flow-schema.js +102 -0
  72. package/dist/memory/function-call-utils.js +4 -2
  73. package/dist/memory/layer-api.d.ts +1 -1
  74. package/dist/memory/layer-api.js +2 -2
  75. package/dist/memory/layer-lifecycle.d.ts +1 -1
  76. package/dist/memory/layer-lifecycle.js +2 -2
  77. package/dist/memory/layer-provides.d.ts +32 -0
  78. package/dist/memory/layer-provides.js +33 -0
  79. package/dist/memory/layer-usage.d.ts +3 -2
  80. package/dist/memory/layer-usage.js +9 -3
  81. package/dist/memory/layers/durable-task-state.js +1 -1
  82. package/dist/memory/layers/file-reference.js +302 -246
  83. package/dist/memory/layers/history-window.js +1 -1
  84. package/dist/memory/layers/observational-memory.js +1 -2
  85. package/dist/memory/layers/plan.d.ts +1 -1
  86. package/dist/memory/layers/plan.js +13 -8
  87. package/dist/memory/layers/steering.js +1 -1
  88. package/dist/memory/layers/tool-memory-layer.d.ts +1 -1
  89. package/dist/memory/layers/working-memory.js +2 -2
  90. package/dist/patterns/dynamic-workflow.d.ts +55 -0
  91. package/dist/patterns/dynamic-workflow.js +194 -0
  92. package/dist/patterns/flow.d.ts +2 -52
  93. package/dist/patterns/flow.js +1 -103
  94. package/dist/patterns/interview.js +1 -1
  95. package/dist/patterns/plans.d.ts +3 -3
  96. package/dist/patterns/plans.js +3 -3
  97. package/dist/patterns/ralph-wiggum.d.ts +1 -1
  98. package/dist/patterns/react.d.ts +1 -1
  99. package/dist/portable/adapters.d.ts +6 -0
  100. package/dist/portable/adapters.js +6 -0
  101. package/dist/portable/ask-user.d.ts +4 -0
  102. package/dist/portable/ask-user.js +2 -0
  103. package/dist/portable/builders.d.ts +12 -0
  104. package/dist/portable/builders.js +12 -0
  105. package/dist/portable/memory-layers.d.ts +12 -0
  106. package/dist/portable/memory-layers.js +12 -0
  107. package/dist/portable/runtime.d.ts +4 -0
  108. package/dist/portable/runtime.js +4 -0
  109. package/dist/portable/until.d.ts +4 -0
  110. package/dist/portable/until.js +4 -0
  111. package/dist/portable.d.ts +8 -26
  112. package/dist/portable.js +8 -34
  113. package/dist/runtime/broadcaster-utils.d.ts +5 -1
  114. package/dist/runtime/broadcaster-utils.js +11 -6
  115. package/dist/runtime/channel-store.js +1 -1
  116. package/dist/runtime/context-deps.d.ts +4 -0
  117. package/dist/runtime/context-deps.js +2 -0
  118. package/dist/runtime/context-impl.d.ts +42 -12
  119. package/dist/runtime/context-impl.js +51 -4
  120. package/dist/runtime/context-type-deps.d.ts +14 -0
  121. package/dist/runtime/context-type-deps.js +1 -0
  122. package/dist/runtime/detached-handle.d.ts +50 -2
  123. package/dist/runtime/detached-handle.js +216 -12
  124. package/dist/runtime/durable/checkpoint-store.d.ts +38 -0
  125. package/dist/runtime/durable/checkpoint-store.js +101 -0
  126. package/dist/runtime/durable/detached-signal.d.ts +37 -0
  127. package/dist/runtime/durable/detached-signal.js +52 -0
  128. package/dist/runtime/durable/harness-checkpoints.d.ts +51 -0
  129. package/dist/runtime/durable/harness-checkpoints.js +104 -0
  130. package/dist/runtime/durable/index.d.ts +10 -0
  131. package/dist/runtime/durable/index.js +5 -0
  132. package/dist/runtime/durable/runnable-loop.d.ts +100 -0
  133. package/dist/runtime/durable/runnable-loop.js +90 -0
  134. package/dist/runtime/durable/session-seed.d.ts +28 -0
  135. package/dist/runtime/durable/session-seed.js +27 -0
  136. package/dist/runtime/durable/stall-nudge.d.ts +82 -0
  137. package/dist/runtime/durable/stall-nudge.js +87 -0
  138. package/dist/runtime/in-memory-storage.js +1 -1
  139. package/dist/runtime/item-log-impl.d.ts +1 -1
  140. package/dist/runtime/step-registry.d.ts +42 -0
  141. package/dist/runtime/step-registry.js +77 -0
  142. package/dist/schemas/workflow.d.ts +139 -0
  143. package/dist/schemas/workflow.js +232 -0
  144. package/dist/types/ask-user-types.d.ts +57 -0
  145. package/dist/types/ask-user-types.js +74 -0
  146. package/dist/types/checkpoint.d.ts +98 -0
  147. package/dist/types/checkpoint.js +81 -0
  148. package/dist/types/common.d.ts +1 -50
  149. package/dist/types/context-parts/item-log.d.ts +6 -0
  150. package/dist/types/context-parts/item-log.js +1 -0
  151. package/dist/types/context-parts/layer-usage.d.ts +18 -0
  152. package/dist/types/context-parts/layer-usage.js +1 -0
  153. package/dist/types/context.d.ts +135 -26
  154. package/dist/types/error.d.ts +15 -0
  155. package/dist/types/harness-result.d.ts +1 -1
  156. package/dist/types/items.d.ts +18 -2
  157. package/dist/types/memory-context.d.ts +39 -0
  158. package/dist/types/memory-context.js +1 -0
  159. package/dist/types/memory.d.ts +35 -65
  160. package/dist/types/runtime.d.ts +2 -1
  161. package/dist/types/steering.d.ts +1 -1
  162. package/dist/types/step-registrar.d.ts +46 -0
  163. package/dist/types/step-registrar.js +56 -0
  164. package/dist/types/step.d.ts +30 -5
  165. package/dist/types/subprocess-adapter.d.ts +119 -3
  166. package/dist/types/subprocess-adapter.js +2 -0
  167. package/dist/types/tool.d.ts +64 -0
  168. package/dist/types/tool.js +1 -0
  169. package/dist/unstable.d.ts +2 -2
  170. package/dist/unstable.js +2 -2
  171. package/dist/until/predicates.d.ts +9 -0
  172. package/dist/until/predicates.js +14 -0
  173. package/dist/util/message-helpers.d.ts +17 -0
  174. package/dist/util/message-helpers.js +64 -0
  175. package/package.json +1 -1
  176. package/dist/adapters/local-fs-adapter.d.ts +0 -3
  177. package/dist/adapters/local-fs-adapter.js +0 -43
  178. package/dist/adapters/local-shell-adapter.d.ts +0 -51
  179. package/dist/adapters/local-shell-adapter.js +0 -167
  180. package/dist/adapters/local-subprocess-adapter.d.ts +0 -16
  181. package/dist/adapters/local-subprocess-adapter.js +0 -187
  182. package/dist/interpreter/execute-branch.d.ts +0 -3
  183. package/dist/interpreter/execute-branch.js +0 -10
  184. package/dist/interpreter/execute-every.js +0 -143
  185. package/dist/interpreter/execute-fork.d.ts +0 -3
  186. package/dist/interpreter/execute-fork.js +0 -215
  187. package/dist/interpreter/execute-llm.d.ts +0 -4
  188. package/dist/interpreter/execute-llm.js +0 -217
  189. package/dist/interpreter/execute-loop.d.ts +0 -3
  190. package/dist/interpreter/execute-loop.js +0 -171
  191. package/dist/interpreter/execute-provide.d.ts +0 -10
  192. package/dist/interpreter/execute-provide.js +0 -64
  193. package/dist/interpreter/execute-run.d.ts +0 -3
  194. package/dist/interpreter/execute-run.js +0 -39
  195. package/dist/interpreter/execute-spawn.d.ts +0 -11
  196. package/dist/interpreter/execute-spawn.js +0 -113
  197. package/dist/interpreter/execute-tool.d.ts +0 -5
  198. package/dist/interpreter/execute-tool.js +0 -81
  199. package/dist/runtime/agent-harness.js +0 -1170
  200. /package/dist/{runtime → memory}/exec-context-factory.d.ts +0 -0
  201. /package/dist/{interpreter → util}/framework-cast.d.ts +0 -0
  202. /package/dist/{interpreter → util}/framework-cast.js +0 -0
package/dist/portable.js CHANGED
@@ -1,34 +1,8 @@
1
- //#region Portable Adapters
2
- /** @public */
3
- export { createInMemoryFsAdapter } from './adapters/in-memory-fs-adapter';
4
- /** @public */
5
- export { createInMemoryShellAdapter } from './adapters/in-memory-shell-adapter';
6
- /** @public */
7
- export { createInMemorySubprocessAdapter } from './adapters/in-memory-subprocess-adapter';
8
- //#endregion
9
- //#region Builders
10
- /** @public */
11
- export { step } from './builders/step-builders';
12
- /** @public */
13
- export { tool, toolWithGenerator } from './builders/tool-builder';
14
- //#endregion
15
- //#region Memory Layers
16
- /** @public */
17
- export { durableTaskState } from './memory/layers/durable-task-state';
18
- /** @public */
19
- export { historyWindow } from './memory/layers/history-window';
20
- /** @public */
21
- export { observationalMemory } from './memory/layers/observational-memory';
22
- /** @public */
23
- export { planMemory } from './memory/layers/plan';
24
- /** @public */
25
- export { toolMemoryLayer } from './memory/layers/tool-memory-layer';
26
- /** @public */
27
- export { workingMemory } from './memory/layers/working-memory';
28
- //#endregion
29
- //#region Runtime
30
- /** @public */
31
- export { AgentHarness } from './runtime/agent-harness';
32
- /** @public */
33
- export { createInMemoryStorage } from './runtime/in-memory-storage';
34
- //#endregion
1
+ export * from './portable/adapters';
2
+ export * from './portable/ask-user';
3
+ export * from './portable/builders';
4
+ export * from './portable/memory-layers';
5
+ export * from './portable/runtime';
6
+ export * from './portable/until';
7
+ /** @public */
8
+ export { Slot } from './types/memory';
@@ -13,7 +13,11 @@ export type EmitOption = boolean | ((eventType: string, data: Record<string, unk
13
13
  */
14
14
  export declare function shouldEmit(emit: EmitOption | undefined, eventType: string, data: Record<string, unknown>): boolean;
15
15
  /**
16
- * Get the EventBroadcaster from a context if available.
16
+ * Get the EventBroadcaster from a context if available. Walks up the parent
17
+ * chain so spawned child contexts inherit the session-level broadcaster — the
18
+ * interpreter never re-installs `_broadcaster` on spawn children, so without
19
+ * this traversal every LLM call inside a `spawn()` step would be invisible to
20
+ * the UI stream.
17
21
  * Uses property check rather than instanceof to support mock contexts in tests.
18
22
  * @internal
19
23
  */
@@ -26,16 +26,21 @@ function hasBroadcaster(ctx) {
26
26
  return typeof ctx._broadcaster === 'object' && ctx._broadcaster !== null;
27
27
  }
28
28
  /**
29
- * Get the EventBroadcaster from a context if available.
29
+ * Get the EventBroadcaster from a context if available. Walks up the parent
30
+ * chain so spawned child contexts inherit the session-level broadcaster — the
31
+ * interpreter never re-installs `_broadcaster` on spawn children, so without
32
+ * this traversal every LLM call inside a `spawn()` step would be invisible to
33
+ * the UI stream.
30
34
  * Uses property check rather than instanceof to support mock contexts in tests.
31
35
  * @internal
32
36
  */
33
37
  export function getBroadcaster(ctx) {
34
- if (!ctx) {
35
- return undefined;
36
- }
37
- if (hasBroadcaster(ctx)) {
38
- return ctx._broadcaster;
38
+ let current = ctx;
39
+ while (current) {
40
+ if (hasBroadcaster(current)) {
41
+ return current._broadcaster;
42
+ }
43
+ current = current.parent ?? null;
39
44
  }
40
45
  return undefined;
41
46
  }
@@ -1,5 +1,5 @@
1
1
  import { NoeticErrorImpl } from '../errors/noetic-error';
2
- import { frameworkCast } from '../interpreter/framework-cast';
2
+ import { frameworkCast } from '../util/framework-cast';
3
3
  function isExternalChannel(ch) {
4
4
  return 'external' in ch && ch.external === true;
5
5
  }
@@ -0,0 +1,4 @@
1
+ export { buildContextMemory } from '../memory/layer-api';
2
+ export type { ItemSchemaRegistry } from '../schemas/item';
3
+ export { defaultItemSchemaRegistry } from '../schemas/item';
4
+ export type { AgentHarnessContract, Channel, ChannelStore, Context, ContextMemory, CwdState, EventBroadcaster, FrontierFrame, FsAdapter, Item, ItemLog, LastLayerUsage, MemoryLayer, Span, StepMeta, TokenUsage, Tool, } from './context-type-deps';
@@ -0,0 +1,2 @@
1
+ export { buildContextMemory } from '../memory/layer-api';
2
+ export { defaultItemSchemaRegistry } from '../schemas/item';
@@ -1,14 +1,4 @@
1
- import type { ItemSchemaRegistry } from '../schemas/item';
2
- import type { Channel } from '../types/channel';
3
- import type { StepMeta, TokenUsage, Tool } from '../types/common';
4
- import type { Context, CwdState, ItemLog, LastLayerUsage } from '../types/context';
5
- import type { FsAdapter } from '../types/fs-adapter';
6
- import type { Item } from '../types/items';
7
- import type { ContextMemory, MemoryLayer } from '../types/memory';
8
- import type { Span } from '../types/observability';
9
- import type { AgentHarnessContract } from '../types/runtime';
10
- import type { ChannelStore } from './channel-store';
11
- import type { EventBroadcaster } from './event-broadcaster';
1
+ import type { AgentHarnessContract, Channel, ChannelStore, Context, ContextMemory, CwdState, EventBroadcaster, FrontierFrame, FsAdapter, Item, ItemLog, ItemSchemaRegistry, LastLayerUsage, MemoryLayer, Span, StepMeta, TokenUsage, Tool } from './context-deps';
12
2
  export declare class ContextImpl implements Context<ContextMemory> {
13
3
  readonly id: string;
14
4
  stepCount: number;
@@ -34,7 +24,8 @@ export declare class ContextImpl implements Context<ContextMemory> {
34
24
  /**
35
25
  * Shared channel store for cross-context communication. Inherited by
36
26
  * `fork` / `spawn` children so channels published by a sibling are visible
37
- * to peers — see `interpreter/execute-fork.ts` and `interpreter/execute-spawn.ts`.
27
+ * to peers — see `executeFork` and `executeSpawn` in `interpreter/execute-action.ts`
28
+ * and `interpreter/execute-control.ts`.
38
29
  * @internal
39
30
  */
40
31
  readonly channelStore?: ChannelStore;
@@ -44,6 +35,15 @@ export declare class ContextImpl implements Context<ContextMemory> {
44
35
  private _aborted;
45
36
  private _abortReason?;
46
37
  private _memory?;
38
+ /**
39
+ * Stack of steps currently in flight on this context, most-recent last.
40
+ * Pushed by `enterStep` at the top of `execute()` and popped by
41
+ * `leaveStep` when the step resolves (success or failure). The harness'
42
+ * checkpoint writer serialises this stack as the execution frontier so
43
+ * a restart can identify which step the context was paused inside.
44
+ * @internal
45
+ */
46
+ private readonly _frontier;
47
47
  constructor(opts: {
48
48
  harness: AgentHarnessContract;
49
49
  parent?: Context;
@@ -59,6 +59,14 @@ export declare class ContextImpl implements Context<ContextMemory> {
59
59
  itemSchemas?: ItemSchemaRegistry;
60
60
  cwdState?: CwdState;
61
61
  _broadcaster?: EventBroadcaster;
62
+ /**
63
+ * Pre-chosen context id. When set, the ContextImpl adopts this id instead
64
+ * of generating a fresh UUID. Used by `executeSpawn` so the child's
65
+ * `ctx.id` matches the `executionId` keyed into the layer-state store —
66
+ * otherwise writes via `ctx.memory[layerId].state` land on one id while
67
+ * spawn's `onReturn` reads from another, silently losing the update.
68
+ */
69
+ id?: string;
62
70
  });
63
71
  get elapsed(): number;
64
72
  get aborted(): boolean;
@@ -77,4 +85,26 @@ export declare class ContextImpl implements Context<ContextMemory> {
77
85
  get completionValue(): unknown;
78
86
  get abortReason(): string | undefined;
79
87
  abort(reason?: string): void;
88
+ /**
89
+ * @internal
90
+ * Push a frame onto the execution frontier. Called by `execute()` at the
91
+ * top of every step dispatch so that the frontier reflects exactly the
92
+ * stack of steps currently in-flight on this context.
93
+ */
94
+ enterStep(frame: FrontierFrame): void;
95
+ /**
96
+ * @internal
97
+ * Pop the top frame. Called when a step resolves (success or failure)
98
+ * so that the frontier unwinds cleanly. The value of `expectedStepId`
99
+ * is used as a consistency check — if it does not match the top frame
100
+ * the pop is still performed, but a best-effort warning is surfaced to
101
+ * stderr rather than swallowed silently.
102
+ */
103
+ leaveStep(expectedStepId: string): void;
104
+ /**
105
+ * @internal
106
+ * Return a defensive copy of the current frontier. Consumed by the
107
+ * checkpoint writer — see `AgentHarness.checkpoint`.
108
+ */
109
+ serialiseFrontier(): FrontierFrame[];
80
110
  }
@@ -1,5 +1,4 @@
1
- import { buildContextMemory } from '../memory/layer-api';
2
- import { defaultItemSchemaRegistry } from '../schemas/item';
1
+ import { buildContextMemory, defaultItemSchemaRegistry } from './context-deps';
3
2
  import { ItemLogImpl } from './item-log-impl';
4
3
  const EMPTY_MEMORY = Object.freeze({});
5
4
  class NoopSpan {
@@ -39,7 +38,8 @@ export class ContextImpl {
39
38
  /**
40
39
  * Shared channel store for cross-context communication. Inherited by
41
40
  * `fork` / `spawn` children so channels published by a sibling are visible
42
- * to peers — see `interpreter/execute-fork.ts` and `interpreter/execute-spawn.ts`.
41
+ * to peers — see `executeFork` and `executeSpawn` in `interpreter/execute-action.ts`
42
+ * and `interpreter/execute-control.ts`.
43
43
  * @internal
44
44
  */
45
45
  channelStore;
@@ -49,8 +49,17 @@ export class ContextImpl {
49
49
  _aborted = false;
50
50
  _abortReason;
51
51
  _memory;
52
+ /**
53
+ * Stack of steps currently in flight on this context, most-recent last.
54
+ * Pushed by `enterStep` at the top of `execute()` and popped by
55
+ * `leaveStep` when the step resolves (success or failure). The harness'
56
+ * checkpoint writer serialises this stack as the execution frontier so
57
+ * a restart can identify which step the context was paused inside.
58
+ * @internal
59
+ */
60
+ _frontier = [];
52
61
  constructor(opts) {
53
- this.id = crypto.randomUUID();
62
+ this.id = opts.id ?? crypto.randomUUID();
54
63
  this._createdAt = Date.now();
55
64
  this.harness = opts.harness;
56
65
  this.state = opts.state ?? {};
@@ -137,4 +146,42 @@ export class ContextImpl {
137
146
  this._aborted = true;
138
147
  this._abortReason = reason;
139
148
  }
149
+ /**
150
+ * @internal
151
+ * Push a frame onto the execution frontier. Called by `execute()` at the
152
+ * top of every step dispatch so that the frontier reflects exactly the
153
+ * stack of steps currently in-flight on this context.
154
+ */
155
+ enterStep(frame) {
156
+ this._frontier.push(frame);
157
+ }
158
+ /**
159
+ * @internal
160
+ * Pop the top frame. Called when a step resolves (success or failure)
161
+ * so that the frontier unwinds cleanly. The value of `expectedStepId`
162
+ * is used as a consistency check — if it does not match the top frame
163
+ * the pop is still performed, but a best-effort warning is surfaced to
164
+ * stderr rather than swallowed silently.
165
+ */
166
+ leaveStep(expectedStepId) {
167
+ const top = this._frontier[this._frontier.length - 1];
168
+ if (top && top.stepId !== expectedStepId) {
169
+ // A mismatch indicates bookkeeping drift. We unwind best-effort and
170
+ // let the caller observe via `serialiseFrontier()` if needed.
171
+ console.warn(`ContextImpl.leaveStep: expected "${expectedStepId}" on top of frontier but saw "${top.stepId}".`);
172
+ }
173
+ this._frontier.pop();
174
+ }
175
+ /**
176
+ * @internal
177
+ * Return a defensive copy of the current frontier. Consumed by the
178
+ * checkpoint writer — see `AgentHarness.checkpoint`.
179
+ */
180
+ serialiseFrontier() {
181
+ return this._frontier.map((frame) => ({
182
+ stepId: frame.stepId,
183
+ input: frame.input,
184
+ state: frame.state,
185
+ }));
186
+ }
140
187
  }
@@ -0,0 +1,14 @@
1
+ export type { Channel } from '../types/channel';
2
+ export type { FrontierFrame } from '../types/checkpoint';
3
+ export type { StepMeta, TokenUsage } from '../types/common';
4
+ export type { Context, CwdState } from '../types/context';
5
+ export type { ItemLog } from '../types/context-parts/item-log';
6
+ export type { LastLayerUsage } from '../types/context-parts/layer-usage';
7
+ export type { FsAdapter } from '../types/fs-adapter';
8
+ export type { Item } from '../types/items';
9
+ export type { ContextMemory, MemoryLayer } from '../types/memory';
10
+ export type { Span } from '../types/observability';
11
+ export type { AgentHarnessContract } from '../types/runtime';
12
+ export type { Tool } from '../types/tool';
13
+ export type { ChannelStore } from './channel-store';
14
+ export type { EventBroadcaster } from './event-broadcaster';
@@ -0,0 +1 @@
1
+ export {};
@@ -1,14 +1,62 @@
1
1
  import type { DetachedHandle } from '../types/detached';
2
2
  import { DetachedStatus } from '../types/detached';
3
+ import type { SubprocessAdapter, SubprocessHandle } from '../types/subprocess-adapter';
4
+ interface DetachedHandleOpts {
5
+ /** Id surfaced to callers — usually the execution id (childCtx.id in
6
+ * `detachedSpawn`) so consumers can correlate it with their own state. */
7
+ id: string;
8
+ /** Step id — used when the adapter surfaces a generic failure and we need
9
+ * to produce a `NoeticError` with a meaningful `stepId`. */
10
+ stepId: string;
11
+ /** Adapter that produced the handle. */
12
+ adapter: SubprocessAdapter;
13
+ /**
14
+ * Promise that resolves to the adapter-assigned handle. `detachedSpawn`
15
+ * passes the in-flight `adapter.spawn(request)` promise here so `.await()`
16
+ * is reachable even before the spawn itself has settled.
17
+ */
18
+ spawnPromise: Promise<SubprocessHandle>;
19
+ }
20
+ /**
21
+ * @public
22
+ * Thin wrapper over a `SubprocessHandle` that implements the `DetachedHandle`
23
+ * contract. Polls the adapter until the handle reaches a terminal status,
24
+ * then reads the result (or error) off `handle.metadata`.
25
+ *
26
+ * The polling cadence starts at 10ms and ramps to 100ms after the first
27
+ * second — responsive for short-lived in-memory steps without burning cycles
28
+ * on long-running ones.
29
+ */
3
30
  export declare class DetachedHandleImpl<O> implements DetachedHandle<O> {
4
31
  readonly id: string;
32
+ private readonly _stepId;
33
+ private readonly _adapter;
34
+ private readonly _spawnPromise;
35
+ private readonly _startTime;
5
36
  private _status;
6
37
  private _result;
7
38
  private _error;
8
- private readonly promise;
9
- constructor(id: string, promise: Promise<O>);
39
+ private _settlementPromise;
40
+ constructor(opts: DetachedHandleOpts);
10
41
  get status(): DetachedStatus;
11
42
  get result(): O | undefined;
12
43
  get error(): string | undefined;
44
+ /**
45
+ * Wait for the underlying handle to settle, up to an optional timeout.
46
+ * On settlement, the cached status/result/error are updated before
47
+ * returning — so a caller that inspects `status` after `await()` resolves
48
+ * observes the final value.
49
+ */
13
50
  await(timeout?: number): Promise<O>;
51
+ /**
52
+ * Start (or return the cached) polling promise that resolves on the first
53
+ * terminal status the adapter reports for this handle.
54
+ *
55
+ * A single settlement promise is shared across all `.await()` callers so
56
+ * repeated calls don't spawn parallel polling loops.
57
+ */
58
+ private getOrStartSettlement;
59
+ private pollUntilSettled;
60
+ private finalise;
14
61
  }
62
+ export {};
@@ -1,19 +1,125 @@
1
1
  import { NoeticErrorImpl } from '../errors/noetic-error';
2
2
  import { DetachedStatus } from '../types/detached';
3
+ import { frameworkCast } from '../util/framework-cast';
4
+ //#endregion
5
+ //#region Helpers
6
+ const INITIAL_POLL_MS = 10;
7
+ const MAX_POLL_MS = 100;
8
+ const POLL_RAMP_AFTER_MS = 1_000;
9
+ /** Upper bound on microtask yields before falling back to timer-based
10
+ * polling. Chosen empirically: in-memory step bodies settle within a
11
+ * handful of microtasks, and 64 gives plenty of headroom for chained
12
+ * awaits inside complex step trees without busy-spinning. */
13
+ const MAX_MICROTASK_YIELDS = 64;
14
+ /** Grace period before treating persistent null `adapter.get(handleId)`
15
+ * results as an eviction. Tolerates transient nulls from adapters that
16
+ * register the handle asynchronously relative to the spawn promise
17
+ * settling, but bounds the worst case so a corrupt adapter doesn't
18
+ * produce an infinite poll. */
19
+ const HANDLE_EVICTED_GRACE_MS = 500;
20
+ /**
21
+ * Yield control to the timer queue so the adapter has a chance to
22
+ * transition the handle before we issue the next `adapter.get()` call.
23
+ * Using a timer (even 0ms) ensures we don't spin-lock when the adapter
24
+ * schedules its completion on a later microtask.
25
+ */
26
+ function sleep(ms) {
27
+ if (ms <= 0) {
28
+ return Promise.resolve();
29
+ }
30
+ return new Promise((resolve) => setTimeout(resolve, ms));
31
+ }
32
+ function isTerminalStatus(status) {
33
+ return (status === 'completed' || status === 'failed' || status === 'stopped' || status === 'stale');
34
+ }
35
+ function isRecord(value) {
36
+ return typeof value === 'object' && value !== null;
37
+ }
38
+ function isNoeticErrorPayload(value) {
39
+ return isRecord(value) && typeof value.kind === 'string';
40
+ }
41
+ /** Rehydrate a thrown error from the handle metadata. Preserves the
42
+ * structured NoeticError shape when present so type-guards like
43
+ * `isNoeticError` keep working on the consumer side. */
44
+ function rehydrateError(payload, stepId) {
45
+ if (!payload) {
46
+ return new NoeticErrorImpl({
47
+ kind: 'step_failed',
48
+ stepId,
49
+ cause: new Error('Detached spawn failed without an error payload'),
50
+ retriesExhausted: false,
51
+ });
52
+ }
53
+ if (isNoeticErrorPayload(payload.noeticError)) {
54
+ return new NoeticErrorImpl(payload.noeticError);
55
+ }
56
+ const err = new Error(payload.message);
57
+ if (payload.name) {
58
+ err.name = payload.name;
59
+ }
60
+ if (payload.stack) {
61
+ err.stack = payload.stack;
62
+ }
63
+ return err;
64
+ }
65
+ function extractResult(metadata) {
66
+ if (!metadata || metadata.result === undefined) {
67
+ return undefined;
68
+ }
69
+ return frameworkCast(metadata.result);
70
+ }
71
+ function extractError(metadata) {
72
+ if (!metadata || metadata.error === undefined) {
73
+ return undefined;
74
+ }
75
+ const raw = metadata.error;
76
+ if (!isRecord(raw)) {
77
+ return {
78
+ message: String(raw),
79
+ };
80
+ }
81
+ const message = typeof raw.message === 'string' ? raw.message : 'unknown error';
82
+ const name = typeof raw.name === 'string' ? raw.name : undefined;
83
+ const stack = typeof raw.stack === 'string' ? raw.stack : undefined;
84
+ return {
85
+ message,
86
+ name,
87
+ stack,
88
+ noeticError: raw.noeticError,
89
+ };
90
+ }
91
+ //#endregion
3
92
  //#region DetachedHandleImpl
93
+ /**
94
+ * @public
95
+ * Thin wrapper over a `SubprocessHandle` that implements the `DetachedHandle`
96
+ * contract. Polls the adapter until the handle reaches a terminal status,
97
+ * then reads the result (or error) off `handle.metadata`.
98
+ *
99
+ * The polling cadence starts at 10ms and ramps to 100ms after the first
100
+ * second — responsive for short-lived in-memory steps without burning cycles
101
+ * on long-running ones.
102
+ */
4
103
  export class DetachedHandleImpl {
5
104
  id;
105
+ _stepId;
106
+ _adapter;
107
+ _spawnPromise;
108
+ _startTime;
6
109
  _status = DetachedStatus.Running;
7
110
  _result;
8
111
  _error;
9
- promise;
10
- constructor(id, promise) {
11
- this.id = id;
12
- this.promise = promise;
13
- promise.then((value) => {
14
- this._status = DetachedStatus.Completed;
15
- this._result = value;
16
- }, (err) => {
112
+ _settlementPromise;
113
+ constructor(opts) {
114
+ this.id = opts.id;
115
+ this._stepId = opts.stepId;
116
+ this._adapter = opts.adapter;
117
+ this._spawnPromise = opts.spawnPromise;
118
+ this._startTime = Date.now();
119
+ // Observe spawn failure eagerly so `.error` is populated even if the
120
+ // caller never invokes `.await()`. We intentionally swallow the
121
+ // rejection here — `.await()` surfaces it to the caller.
122
+ this._spawnPromise.catch((err) => {
17
123
  this._status = DetachedStatus.Failed;
18
124
  this._error = err instanceof Error ? err.message : String(err);
19
125
  });
@@ -27,16 +133,23 @@ export class DetachedHandleImpl {
27
133
  get error() {
28
134
  return this._error;
29
135
  }
136
+ /**
137
+ * Wait for the underlying handle to settle, up to an optional timeout.
138
+ * On settlement, the cached status/result/error are updated before
139
+ * returning — so a caller that inspects `status` after `await()` resolves
140
+ * observes the final value.
141
+ */
30
142
  async await(timeout) {
143
+ const settlement = this.getOrStartSettlement();
31
144
  if (timeout === undefined || timeout <= 0) {
32
- return this.promise;
145
+ return settlement;
33
146
  }
34
147
  let timerId;
35
148
  const timeoutPromise = new Promise((_resolve, reject) => {
36
149
  timerId = setTimeout(() => {
37
150
  reject(new NoeticErrorImpl({
38
151
  kind: 'step_failed',
39
- stepId: this.id,
152
+ stepId: this._stepId,
40
153
  cause: new Error(`Detached spawn timed out after ${timeout}ms`),
41
154
  retriesExhausted: false,
42
155
  }));
@@ -44,13 +157,104 @@ export class DetachedHandleImpl {
44
157
  });
45
158
  try {
46
159
  return await Promise.race([
47
- this.promise,
160
+ settlement,
48
161
  timeoutPromise,
49
162
  ]);
50
163
  }
51
164
  finally {
52
- clearTimeout(timerId);
165
+ if (timerId !== undefined) {
166
+ clearTimeout(timerId);
167
+ }
168
+ }
169
+ }
170
+ /**
171
+ * Start (or return the cached) polling promise that resolves on the first
172
+ * terminal status the adapter reports for this handle.
173
+ *
174
+ * A single settlement promise is shared across all `.await()` callers so
175
+ * repeated calls don't spawn parallel polling loops.
176
+ */
177
+ getOrStartSettlement() {
178
+ if (this._settlementPromise) {
179
+ return this._settlementPromise;
180
+ }
181
+ this._settlementPromise = this.pollUntilSettled();
182
+ return this._settlementPromise;
183
+ }
184
+ async pollUntilSettled() {
185
+ // Wait for the spawn itself to succeed before we start polling —
186
+ // otherwise we'd be polling with an undefined handle id.
187
+ const initialHandle = await this._spawnPromise;
188
+ if (isTerminalStatus(initialHandle.status)) {
189
+ return this.finalise(initialHandle);
190
+ }
191
+ // Track when the adapter persistently returns null for this handle.
192
+ // Transient nulls are tolerated for `HANDLE_EVICTED_GRACE_MS`; after
193
+ // that we raise `handle_evicted` instead of looping forever. A
194
+ // non-null observation resets the grace window.
195
+ let firstNullAt = null;
196
+ const observeNull = () => {
197
+ if (firstNullAt === null) {
198
+ firstNullAt = Date.now();
199
+ return;
200
+ }
201
+ if (Date.now() - firstNullAt >= HANDLE_EVICTED_GRACE_MS) {
202
+ throw new NoeticErrorImpl({
203
+ kind: 'handle_evicted',
204
+ handleId: initialHandle.id,
205
+ stepId: this._stepId,
206
+ gracePeriodMs: HANDLE_EVICTED_GRACE_MS,
207
+ });
208
+ }
209
+ };
210
+ const observeHandle = (h) => {
211
+ if (h === null) {
212
+ observeNull();
213
+ return null;
214
+ }
215
+ firstNullAt = null;
216
+ return h;
217
+ };
218
+ // Fast path: yield through the microtask queue first. The in-memory
219
+ // adapter schedules completion on a microtask, so a handful of yields
220
+ // typically resolves the handle in sub-millisecond time. This keeps the
221
+ // interpreter's "route through adapter" overhead negligible.
222
+ for (let i = 0; i < MAX_MICROTASK_YIELDS; i++) {
223
+ const handle = observeHandle(await this._adapter.get(initialHandle.id));
224
+ if (handle && isTerminalStatus(handle.status)) {
225
+ return this.finalise(handle);
226
+ }
227
+ await Promise.resolve();
228
+ }
229
+ // Slow path: fall back to timer-based polling for genuinely long-running
230
+ // handles (out-of-process adapters, async work that doesn't resolve on
231
+ // the microtask queue).
232
+ let delay = INITIAL_POLL_MS;
233
+ for (;;) {
234
+ const handle = observeHandle(await this._adapter.get(initialHandle.id));
235
+ if (handle && isTerminalStatus(handle.status)) {
236
+ return this.finalise(handle);
237
+ }
238
+ await sleep(delay);
239
+ if (Date.now() - this._startTime >= POLL_RAMP_AFTER_MS) {
240
+ delay = MAX_POLL_MS;
241
+ }
242
+ }
243
+ }
244
+ finalise(handle) {
245
+ const metadata = handle.metadata;
246
+ if (handle.status === 'completed') {
247
+ const value = extractResult(metadata);
248
+ this._status = DetachedStatus.Completed;
249
+ this._result = value;
250
+ return frameworkCast(value);
53
251
  }
252
+ // Failed / stopped / stale — all map to Failed from the caller's view.
253
+ const errorPayload = extractError(metadata);
254
+ const err = rehydrateError(errorPayload, this._stepId);
255
+ this._status = DetachedStatus.Failed;
256
+ this._error = err.message;
257
+ throw err;
54
258
  }
55
259
  }
56
260
  //#endregion
@@ -0,0 +1,38 @@
1
+ import type { CheckpointSnapshot } from '../../types/checkpoint';
2
+ import type { StorageAdapter } from '../../types/memory';
3
+ export declare const CheckpointKeys: {
4
+ readonly ExecPrefix: "execution:";
5
+ readonly SnapshotSuffix: ":snapshot";
6
+ readonly FrontierSuffix: ":frontier";
7
+ readonly LayersSuffix: ":layers:";
8
+ readonly CwdSuffix: ":cwd";
9
+ readonly AskUserSuffix: ":askUser";
10
+ readonly ItemLogSuffix: ":itemLog";
11
+ };
12
+ /**
13
+ * @public
14
+ * Typed wrapper around a `StorageAdapter` that owns the key layout for
15
+ * checkpoint snapshots and validates every read with the canonical Zod
16
+ * schema.
17
+ *
18
+ * Corruption or schema-version drift is surfaced as `NoeticConfigError`
19
+ * (`code === 'CHECKPOINT_SCHEMA_MISMATCH'`) so callers can discard the
20
+ * offending snapshot without crashing the host.
21
+ */
22
+ export interface CheckpointStore {
23
+ /** Persist a snapshot. Later calls for the same `executionId` overwrite. */
24
+ save(snapshot: CheckpointSnapshot): Promise<void>;
25
+ /** Load the snapshot for an execution, or `null` if none is recorded. */
26
+ load(executionId: string): Promise<CheckpointSnapshot | null>;
27
+ /** List every `executionId` that has a persisted snapshot. */
28
+ list(): Promise<ReadonlyArray<{
29
+ executionId: string;
30
+ }>>;
31
+ /** Remove the snapshot for `executionId`. No-op when the key is absent. */
32
+ clear(executionId: string): Promise<void>;
33
+ }
34
+ /** @public Options for `createCheckpointStore`. */
35
+ export interface CreateCheckpointStoreOptions {
36
+ storage: StorageAdapter;
37
+ }
38
+ export declare function createCheckpointStore(options: CreateCheckpointStoreOptions): CheckpointStore;