@noetic-tools/core 0.0.1 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (254) hide show
  1. package/README.md +31 -11
  2. package/dist/adapters/in-memory-fs-adapter.d.ts +1 -1
  3. package/dist/adapters/in-memory-fs-adapter.js +1 -1
  4. package/dist/adapters/in-memory-shell-adapter.d.ts +1 -1
  5. package/dist/adapters/in-memory-subprocess/manifest-persistence.d.ts +8 -0
  6. package/dist/adapters/in-memory-subprocess/manifest-persistence.js +56 -0
  7. package/dist/adapters/in-memory-subprocess/metadata.d.ts +12 -0
  8. package/dist/adapters/in-memory-subprocess/metadata.js +58 -0
  9. package/dist/adapters/in-memory-subprocess/step-completion.d.ts +3 -0
  10. package/dist/adapters/in-memory-subprocess/step-completion.js +86 -0
  11. package/dist/adapters/in-memory-subprocess/types.d.ts +88 -0
  12. package/dist/adapters/in-memory-subprocess/types.js +2 -0
  13. package/dist/adapters/in-memory-subprocess-adapter.d.ts +12 -4
  14. package/dist/adapters/in-memory-subprocess-adapter.js +164 -55
  15. package/dist/adapters/openrouter.d.ts +16 -7
  16. package/dist/adapters/openrouter.js +68 -7
  17. package/dist/builders/channel-builder.d.ts +1 -1
  18. package/dist/builders/control-flow-builders.d.ts +3 -3
  19. package/dist/builders/control-flow-builders.js +9 -4
  20. package/dist/builders/every.d.ts +3 -3
  21. package/dist/builders/every.js +5 -2
  22. package/dist/builders/layer-provides-builders.d.ts +1 -32
  23. package/dist/builders/layer-provides-builders.js +1 -33
  24. package/dist/builders/loop-builder.d.ts +2 -2
  25. package/dist/builders/loop-builder.js +5 -2
  26. package/dist/builders/memory-builder.d.ts +1 -1
  27. package/dist/builders/memory-builder.js +1 -1
  28. package/dist/builders/provide-builder.d.ts +2 -2
  29. package/dist/builders/provide-builder.js +5 -2
  30. package/dist/builders/spawn-builder.d.ts +22 -9
  31. package/dist/builders/spawn-builder.js +17 -3
  32. package/dist/builders/step-builders.d.ts +51 -30
  33. package/dist/builders/step-builders.js +31 -12
  34. package/dist/builders/tool-builder.d.ts +7 -3
  35. package/dist/builders/tool-builder.js +3 -1
  36. package/dist/builders/workflow-hydrator.d.ts +38 -0
  37. package/dist/builders/workflow-hydrator.js +336 -0
  38. package/dist/conditions/conditions.d.ts +4 -4
  39. package/dist/conditions/conditions.js +2 -2
  40. package/dist/errors/noetic-error.d.ts +1 -1
  41. package/dist/errors/noetic-error.js +2 -0
  42. package/dist/{runtime → harness}/agent-harness.d.ts +65 -45
  43. package/dist/harness/agent-harness.js +576 -0
  44. package/dist/harness/deps/adapters.d.ts +4 -0
  45. package/dist/harness/deps/adapters.js +4 -0
  46. package/dist/harness/deps/interpreter.d.ts +5 -0
  47. package/dist/harness/deps/interpreter.js +4 -0
  48. package/dist/harness/deps/memory.d.ts +5 -0
  49. package/dist/harness/deps/memory.js +4 -0
  50. package/dist/harness/deps/runtime.d.ts +10 -0
  51. package/dist/harness/deps/runtime.js +7 -0
  52. package/dist/harness/deps/types.d.ts +17 -0
  53. package/dist/harness/deps/types.js +1 -0
  54. package/dist/harness/model-call.d.ts +50 -0
  55. package/dist/harness/model-call.js +694 -0
  56. package/dist/harness/model-schema.d.ts +19 -0
  57. package/dist/harness/model-schema.js +46 -0
  58. package/dist/harness/step-bootstrap.d.ts +25 -0
  59. package/dist/harness/step-bootstrap.js +170 -0
  60. package/dist/index.d.ts +114 -76
  61. package/dist/index.js +74 -50
  62. package/dist/internal-test.d.ts +4 -4
  63. package/dist/internal-test.js +4 -4
  64. package/dist/interpreter/action-deps.d.ts +12 -0
  65. package/dist/interpreter/action-deps.js +10 -0
  66. package/dist/interpreter/action-types.d.ts +8 -0
  67. package/dist/interpreter/action-types.js +1 -0
  68. package/dist/interpreter/collect-tools.d.ts +2 -2
  69. package/dist/interpreter/collect-tools.js +6 -2
  70. package/dist/interpreter/execute-action.d.ts +22 -0
  71. package/dist/interpreter/execute-action.js +527 -0
  72. package/dist/interpreter/{execute-every.d.ts → execute-control.d.ts} +8 -2
  73. package/dist/interpreter/execute-control.js +530 -0
  74. package/dist/interpreter/execute.d.ts +16 -3
  75. package/dist/interpreter/execute.js +154 -51
  76. package/dist/interpreter/message-helpers.d.ts +1 -9
  77. package/dist/interpreter/message-helpers.js +1 -42
  78. package/dist/interpreter/step-dispatcher.d.ts +61 -0
  79. package/dist/interpreter/step-dispatcher.js +67 -0
  80. package/dist/interpreter/typeguards.d.ts +8 -15
  81. package/dist/interpreter/typeguards.js +4 -21
  82. package/dist/memory/budget.d.ts +2 -2
  83. package/dist/memory/budget.js +1 -1
  84. package/dist/{runtime → memory}/exec-context-factory.d.ts +2 -2
  85. package/dist/{runtime → memory}/exec-context-factory.js +1 -1
  86. package/dist/memory/flow-schema.d.ts +52 -0
  87. package/dist/memory/flow-schema.js +102 -0
  88. package/dist/memory/function-call-utils.d.ts +1 -1
  89. package/dist/memory/function-call-utils.js +4 -2
  90. package/dist/memory/layer-api.d.ts +4 -4
  91. package/dist/memory/layer-api.js +2 -2
  92. package/dist/memory/layer-lifecycle.d.ts +6 -6
  93. package/dist/memory/layer-lifecycle.js +6 -6
  94. package/dist/memory/layer-provides.d.ts +32 -0
  95. package/dist/memory/layer-provides.js +33 -0
  96. package/dist/memory/layer-usage.d.ts +4 -3
  97. package/dist/memory/layer-usage.js +9 -3
  98. package/dist/memory/layers/durable-task-state.d.ts +8 -8
  99. package/dist/memory/layers/durable-task-state.js +2 -2
  100. package/dist/memory/layers/file-reference.d.ts +1 -1
  101. package/dist/memory/layers/file-reference.js +303 -247
  102. package/dist/memory/layers/history-window.d.ts +1 -1
  103. package/dist/memory/layers/history-window.js +4 -4
  104. package/dist/memory/layers/observational-memory.d.ts +5 -5
  105. package/dist/memory/layers/observational-memory.js +2 -3
  106. package/dist/memory/layers/plan.d.ts +2 -2
  107. package/dist/memory/layers/plan.js +15 -10
  108. package/dist/memory/layers/static-content.d.ts +2 -2
  109. package/dist/memory/layers/static-content.js +1 -1
  110. package/dist/memory/layers/steering.d.ts +7 -7
  111. package/dist/memory/layers/steering.js +5 -5
  112. package/dist/memory/layers/tool-memory-layer.d.ts +2 -2
  113. package/dist/memory/layers/tool-memory-layer.js +1 -1
  114. package/dist/memory/layers/working-memory.d.ts +7 -7
  115. package/dist/memory/layers/working-memory.js +4 -4
  116. package/dist/memory/projector.d.ts +2 -2
  117. package/dist/memory/scope.d.ts +1 -1
  118. package/dist/memory/strip-unresolved.d.ts +1 -1
  119. package/dist/observability/span-impl.d.ts +1 -1
  120. package/dist/observability/trace-exporter.d.ts +2 -2
  121. package/dist/observability/trace-exporter.js +1 -1
  122. package/dist/patterns/dynamic-workflow.d.ts +55 -0
  123. package/dist/patterns/dynamic-workflow.js +194 -0
  124. package/dist/patterns/flow.d.ts +2 -52
  125. package/dist/patterns/flow.js +1 -103
  126. package/dist/patterns/interview.d.ts +2 -2
  127. package/dist/patterns/interview.js +5 -5
  128. package/dist/patterns/plans.d.ts +6 -6
  129. package/dist/patterns/plans.js +4 -4
  130. package/dist/patterns/ralph-wiggum.d.ts +4 -4
  131. package/dist/patterns/ralph-wiggum.js +5 -5
  132. package/dist/patterns/react.d.ts +3 -3
  133. package/dist/patterns/react.js +5 -5
  134. package/dist/portable/adapters.d.ts +6 -0
  135. package/dist/portable/adapters.js +6 -0
  136. package/dist/portable/ask-user.d.ts +4 -0
  137. package/dist/portable/ask-user.js +2 -0
  138. package/dist/portable/builders.d.ts +12 -0
  139. package/dist/portable/builders.js +12 -0
  140. package/dist/portable/memory-layers.d.ts +12 -0
  141. package/dist/portable/memory-layers.js +12 -0
  142. package/dist/portable/runtime.d.ts +4 -0
  143. package/dist/portable/runtime.js +4 -0
  144. package/dist/portable/until.d.ts +4 -0
  145. package/dist/portable/until.js +4 -0
  146. package/dist/portable.d.ts +8 -26
  147. package/dist/portable.js +8 -34
  148. package/dist/runtime/broadcaster-utils.d.ts +7 -3
  149. package/dist/runtime/broadcaster-utils.js +11 -6
  150. package/dist/runtime/channel-store.d.ts +1 -1
  151. package/dist/runtime/channel-store.js +2 -2
  152. package/dist/runtime/context-deps.d.ts +4 -0
  153. package/dist/runtime/context-deps.js +2 -0
  154. package/dist/runtime/context-impl.d.ts +42 -12
  155. package/dist/runtime/context-impl.js +52 -5
  156. package/dist/runtime/context-type-deps.d.ts +14 -0
  157. package/dist/runtime/context-type-deps.js +1 -0
  158. package/dist/runtime/cwd-helpers.d.ts +1 -1
  159. package/dist/runtime/detached-handle.d.ts +52 -4
  160. package/dist/runtime/detached-handle.js +218 -14
  161. package/dist/runtime/durable/checkpoint-store.d.ts +38 -0
  162. package/dist/runtime/durable/checkpoint-store.js +101 -0
  163. package/dist/runtime/durable/detached-signal.d.ts +37 -0
  164. package/dist/runtime/durable/detached-signal.js +52 -0
  165. package/dist/runtime/durable/harness-checkpoints.d.ts +51 -0
  166. package/dist/runtime/durable/harness-checkpoints.js +104 -0
  167. package/dist/runtime/durable/index.d.ts +10 -0
  168. package/dist/runtime/durable/index.js +5 -0
  169. package/dist/runtime/durable/runnable-loop.d.ts +100 -0
  170. package/dist/runtime/durable/runnable-loop.js +90 -0
  171. package/dist/runtime/durable/session-seed.d.ts +28 -0
  172. package/dist/runtime/durable/session-seed.js +27 -0
  173. package/dist/runtime/durable/stall-nudge.d.ts +82 -0
  174. package/dist/runtime/durable/stall-nudge.js +87 -0
  175. package/dist/runtime/event-broadcaster.d.ts +1 -1
  176. package/dist/runtime/in-memory-storage.d.ts +1 -1
  177. package/dist/runtime/in-memory-storage.js +1 -1
  178. package/dist/runtime/item-log-impl.d.ts +3 -3
  179. package/dist/runtime/item-log-impl.js +1 -1
  180. package/dist/runtime/message-queue.d.ts +2 -2
  181. package/dist/runtime/session-runner.d.ts +7 -7
  182. package/dist/runtime/session-runner.js +3 -3
  183. package/dist/runtime/session-streams.d.ts +3 -3
  184. package/dist/runtime/session-streams.js +1 -1
  185. package/dist/runtime/step-registry.d.ts +42 -0
  186. package/dist/runtime/step-registry.js +77 -0
  187. package/dist/runtime/tool-memory.d.ts +3 -3
  188. package/dist/schemas/item.d.ts +1 -1
  189. package/dist/schemas/workflow.d.ts +139 -0
  190. package/dist/schemas/workflow.js +232 -0
  191. package/dist/types/ask-user-types.d.ts +57 -0
  192. package/dist/types/ask-user-types.js +74 -0
  193. package/dist/types/checkpoint.d.ts +98 -0
  194. package/dist/types/checkpoint.js +81 -0
  195. package/dist/types/common.d.ts +1 -50
  196. package/dist/types/context-parts/item-log.d.ts +6 -0
  197. package/dist/types/context-parts/item-log.js +1 -0
  198. package/dist/types/context-parts/layer-usage.d.ts +18 -0
  199. package/dist/types/context-parts/layer-usage.js +1 -0
  200. package/dist/types/context.d.ts +140 -31
  201. package/dist/types/error.d.ts +15 -0
  202. package/dist/types/harness-result.d.ts +2 -2
  203. package/dist/types/items.d.ts +18 -2
  204. package/dist/types/memory-context.d.ts +39 -0
  205. package/dist/types/memory-context.js +1 -0
  206. package/dist/types/memory.d.ts +38 -68
  207. package/dist/types/mutable-context.d.ts +3 -3
  208. package/dist/types/runtime.d.ts +14 -13
  209. package/dist/types/steering.d.ts +2 -2
  210. package/dist/types/step-registrar.d.ts +46 -0
  211. package/dist/types/step-registrar.js +56 -0
  212. package/dist/types/step.d.ts +34 -9
  213. package/dist/types/subprocess-adapter.d.ts +119 -3
  214. package/dist/types/subprocess-adapter.js +2 -0
  215. package/dist/types/tool-context.d.ts +6 -6
  216. package/dist/types/tool.d.ts +64 -0
  217. package/dist/types/tool.js +1 -0
  218. package/dist/unstable.d.ts +9 -9
  219. package/dist/unstable.js +7 -7
  220. package/dist/until/combinators.d.ts +1 -1
  221. package/dist/until/never.d.ts +1 -1
  222. package/dist/until/predicates.d.ts +13 -4
  223. package/dist/until/predicates.js +16 -2
  224. package/dist/util/message-helpers.d.ts +17 -0
  225. package/dist/util/message-helpers.js +64 -0
  226. package/package.json +6 -4
  227. package/dist/adapters/local-fs-adapter.d.ts +0 -3
  228. package/dist/adapters/local-fs-adapter.js +0 -43
  229. package/dist/adapters/local-shell-adapter.d.ts +0 -51
  230. package/dist/adapters/local-shell-adapter.js +0 -167
  231. package/dist/adapters/local-subprocess-adapter.d.ts +0 -16
  232. package/dist/adapters/local-subprocess-adapter.js +0 -187
  233. package/dist/conditions/index.d.ts +0 -3
  234. package/dist/conditions/index.js +0 -2
  235. package/dist/interpreter/execute-branch.d.ts +0 -3
  236. package/dist/interpreter/execute-branch.js +0 -10
  237. package/dist/interpreter/execute-every.js +0 -143
  238. package/dist/interpreter/execute-fork.d.ts +0 -3
  239. package/dist/interpreter/execute-fork.js +0 -215
  240. package/dist/interpreter/execute-llm.d.ts +0 -4
  241. package/dist/interpreter/execute-llm.js +0 -217
  242. package/dist/interpreter/execute-loop.d.ts +0 -3
  243. package/dist/interpreter/execute-loop.js +0 -171
  244. package/dist/interpreter/execute-provide.d.ts +0 -10
  245. package/dist/interpreter/execute-provide.js +0 -64
  246. package/dist/interpreter/execute-run.d.ts +0 -3
  247. package/dist/interpreter/execute-run.js +0 -39
  248. package/dist/interpreter/execute-spawn.d.ts +0 -11
  249. package/dist/interpreter/execute-spawn.js +0 -113
  250. package/dist/interpreter/execute-tool.d.ts +0 -5
  251. package/dist/interpreter/execute-tool.js +0 -81
  252. package/dist/runtime/agent-harness.js +0 -1170
  253. /package/dist/{interpreter → util}/framework-cast.d.ts +0 -0
  254. /package/dist/{interpreter → util}/framework-cast.js +0 -0
@@ -1,14 +1,62 @@
1
- import type { DetachedHandle } from '../types/detached';
2
- import { DetachedStatus } from '../types/detached';
1
+ import type { DetachedHandle } from '../types/detached.js';
2
+ import { DetachedStatus } from '../types/detached.js';
3
+ import type { SubprocessAdapter, SubprocessHandle } from '../types/subprocess-adapter.js';
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
- import { NoeticErrorImpl } from '../errors/noetic-error';
2
- import { DetachedStatus } from '../types/detached';
1
+ import { NoeticErrorImpl } from '../errors/noetic-error.js';
2
+ import { DetachedStatus } from '../types/detached.js';
3
+ import { frameworkCast } from '../util/framework-cast.js';
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.js';
2
+ import type { StorageAdapter } from '../../types/memory.js';
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;
@@ -0,0 +1,101 @@
1
+ import { NoeticConfigError } from '../../errors/noetic-config-error.js';
2
+ import { CheckpointSnapshotSchema } from '../../types/checkpoint.js';
3
+ //#region Keys
4
+ /**
5
+ * Typed key constants for the checkpoint namespace. Every execution snapshot
6
+ * lives under a single key so `save`/`load` are cheap single `get`/`set`
7
+ * calls, while a `list` prefix enumeration exposes every known execution id
8
+ * to the restart/reattach path. Auxiliary suffixes exist so future sharded
9
+ * storage schemes can still co-locate snapshot fragments under a shared
10
+ * execution prefix without bumping schemaVersion.
11
+ *
12
+ * @internal
13
+ */
14
+ const EXEC_KEY_PREFIX = 'execution:';
15
+ const SNAPSHOT_SUFFIX = ':snapshot';
16
+ const FRONTIER_SUFFIX = ':frontier';
17
+ const LAYERS_SUFFIX = ':layers:';
18
+ const CWD_SUFFIX = ':cwd';
19
+ const ASK_USER_SUFFIX = ':askUser';
20
+ const ITEM_LOG_SUFFIX = ':itemLog';
21
+ export const CheckpointKeys = {
22
+ ExecPrefix: EXEC_KEY_PREFIX,
23
+ SnapshotSuffix: SNAPSHOT_SUFFIX,
24
+ FrontierSuffix: FRONTIER_SUFFIX,
25
+ LayersSuffix: LAYERS_SUFFIX,
26
+ CwdSuffix: CWD_SUFFIX,
27
+ AskUserSuffix: ASK_USER_SUFFIX,
28
+ ItemLogSuffix: ITEM_LOG_SUFFIX,
29
+ };
30
+ //#endregion
31
+ //#region Helpers
32
+ function snapshotKey(executionId) {
33
+ return `${EXEC_KEY_PREFIX}${executionId}${SNAPSHOT_SUFFIX}`;
34
+ }
35
+ function executionIdFromSnapshotKey(key) {
36
+ if (!key.startsWith(EXEC_KEY_PREFIX) || !key.endsWith(SNAPSHOT_SUFFIX)) {
37
+ return null;
38
+ }
39
+ const start = EXEC_KEY_PREFIX.length;
40
+ const end = key.length - SNAPSHOT_SUFFIX.length;
41
+ if (end <= start) {
42
+ return null;
43
+ }
44
+ return key.slice(start, end);
45
+ }
46
+ function isJsonRecord(value) {
47
+ return typeof value === 'object' && value !== null;
48
+ }
49
+ function parseSnapshot(raw, executionId) {
50
+ const parsed = CheckpointSnapshotSchema.safeParse(raw);
51
+ if (parsed.success) {
52
+ return parsed.data;
53
+ }
54
+ const observedVersion = isJsonRecord(raw) && 'schemaVersion' in raw ? raw.schemaVersion : 'unknown';
55
+ throw new NoeticConfigError({
56
+ code: 'CHECKPOINT_SCHEMA_MISMATCH',
57
+ message: `Checkpoint snapshot for execution "${executionId}" failed schema validation (observed schemaVersion=${String(observedVersion)}).`,
58
+ hint: 'The snapshot was produced by a different runtime version. Clear the snapshot via CheckpointStore.clear() and restart the execution.',
59
+ });
60
+ }
61
+ //#endregion
62
+ //#region Factory
63
+ export function createCheckpointStore(options) {
64
+ const { storage } = options;
65
+ async function save(snapshot) {
66
+ // Persisted shape is the validated snapshot. The adapter is free to
67
+ // JSON-encode it internally; `InMemoryStorage` stores by reference which
68
+ // is fine because `CheckpointSnapshotSchema.parse` is deep-immutable.
69
+ await storage.set(snapshotKey(snapshot.executionId), snapshot);
70
+ }
71
+ async function load(executionId) {
72
+ const raw = await storage.get(snapshotKey(executionId));
73
+ if (raw === null) {
74
+ return null;
75
+ }
76
+ return parseSnapshot(raw, executionId);
77
+ }
78
+ async function list() {
79
+ const keys = await storage.list(EXEC_KEY_PREFIX);
80
+ const out = [];
81
+ for (const key of keys) {
82
+ const id = executionIdFromSnapshotKey(key);
83
+ if (id !== null) {
84
+ out.push({
85
+ executionId: id,
86
+ });
87
+ }
88
+ }
89
+ return out;
90
+ }
91
+ async function clear(executionId) {
92
+ await storage.delete(snapshotKey(executionId));
93
+ }
94
+ return {
95
+ save,
96
+ load,
97
+ list,
98
+ clear,
99
+ };
100
+ }
101
+ //#endregion
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Deferred-style, single-shot signal used to wire a detached child — a
3
+ * terminal tool, an IPC handler, an external watchdog — back to the
4
+ * parent that awaits its outcome.
5
+ *
6
+ * The pattern is intentionally minimal: the parent awaits `signal.done`
7
+ * while the child calls `signal.resolve(outcome)` (or `signal.reject(err)`)
8
+ * when it has produced the final value. Any subsequent `resolve` /
9
+ * `reject` calls are silently dropped so a late-arriving duplicate
10
+ * settlement never overwrites the first.
11
+ *
12
+ * This is the canonical primitive the `runnableLoop` (and the task
13
+ * runner scripts that use it) build on. Keeping it in core means every
14
+ * out-of-process worker / detached step shares one signal shape rather
15
+ * than each reinventing the single-shot-deferred.
16
+ */
17
+ /** Single-shot deferred signal the caller awaits via `.done`. */
18
+ export interface DetachedSignal<TOutcome> {
19
+ readonly done: Promise<TOutcome>;
20
+ /**
21
+ * Settle the signal with a success outcome. Only the first call has
22
+ * effect; subsequent calls (from retry logic, duplicate terminal
23
+ * tool invocations, external signal handlers) are dropped silently.
24
+ */
25
+ resolve(outcome: TOutcome): void;
26
+ /**
27
+ * Settle the signal with a failure. Only the first call has effect;
28
+ * a `resolve` that races with a `reject` wins by whichever runs
29
+ * first — both further attempts are dropped.
30
+ */
31
+ reject(err: unknown): void;
32
+ }
33
+ /**
34
+ * Construct a fresh single-shot `DetachedSignal<TOutcome>`. The returned
35
+ * signal's `done` Promise resolves/rejects at most once.
36
+ */
37
+ export declare function createDetachedSignal<TOutcome>(): DetachedSignal<TOutcome>;
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Deferred-style, single-shot signal used to wire a detached child — a
3
+ * terminal tool, an IPC handler, an external watchdog — back to the
4
+ * parent that awaits its outcome.
5
+ *
6
+ * The pattern is intentionally minimal: the parent awaits `signal.done`
7
+ * while the child calls `signal.resolve(outcome)` (or `signal.reject(err)`)
8
+ * when it has produced the final value. Any subsequent `resolve` /
9
+ * `reject` calls are silently dropped so a late-arriving duplicate
10
+ * settlement never overwrites the first.
11
+ *
12
+ * This is the canonical primitive the `runnableLoop` (and the task
13
+ * runner scripts that use it) build on. Keeping it in core means every
14
+ * out-of-process worker / detached step shares one signal shape rather
15
+ * than each reinventing the single-shot-deferred.
16
+ */
17
+ //#endregion
18
+ //#region Public API
19
+ /**
20
+ * Construct a fresh single-shot `DetachedSignal<TOutcome>`. The returned
21
+ * signal's `done` Promise resolves/rejects at most once.
22
+ */
23
+ export function createDetachedSignal() {
24
+ let resolveFn = null;
25
+ let rejectFn = null;
26
+ const done = new Promise((resolve, reject) => {
27
+ resolveFn = resolve;
28
+ rejectFn = reject;
29
+ });
30
+ return {
31
+ done,
32
+ resolve(outcome) {
33
+ if (resolveFn === null) {
34
+ return;
35
+ }
36
+ const fn = resolveFn;
37
+ resolveFn = null;
38
+ rejectFn = null;
39
+ fn(outcome);
40
+ },
41
+ reject(err) {
42
+ if (rejectFn === null) {
43
+ return;
44
+ }
45
+ const fn = rejectFn;
46
+ resolveFn = null;
47
+ rejectFn = null;
48
+ fn(err);
49
+ },
50
+ };
51
+ }
52
+ //#endregion
@@ -0,0 +1,51 @@
1
+ import type { LayerStateStore } from '../../memory/layer-lifecycle.js';
2
+ import type { ItemSchemaRegistry } from '../../schemas/item.js';
3
+ import type { Context } from '../../types/context.js';
4
+ import type { Item } from '../../types/items.js';
5
+ import type { MemoryLayer } from '../../types/memory.js';
6
+ import type { CheckpointStore } from './checkpoint-store.js';
7
+ /**
8
+ * Minimum harness surface `captureCheckpoint` / `restoreFromCheckpoint`
9
+ * require. `AgentHarness` already satisfies this shape via its
10
+ * `@internal` readonly fields — defining it structurally keeps the free
11
+ * functions loosely coupled to the harness implementation.
12
+ *
13
+ * @internal
14
+ */
15
+ export interface CheckpointHarnessHandle {
16
+ readonly checkpointStore?: CheckpointStore;
17
+ readonly layerStateStore: LayerStateStore;
18
+ readonly itemSchemas: ItemSchemaRegistry;
19
+ readonly _memory?: MemoryLayer[];
20
+ createContext(opts?: {
21
+ items?: Item[];
22
+ threadId?: string;
23
+ resourceId?: string;
24
+ cwdInit?: string;
25
+ memory?: MemoryLayer[];
26
+ }): Context;
27
+ }
28
+ /**
29
+ * Snapshot the execution state at a checkpoint boundary. No-op when no
30
+ * `CheckpointStore` is configured — zero-config harnesses preserve
31
+ * ephemeral semantics. Save failures are logged rather than thrown,
32
+ * because a checkpoint failing must never abort an otherwise-successful
33
+ * step.
34
+ *
35
+ * @internal
36
+ */
37
+ export declare function captureCheckpoint(h: CheckpointHarnessHandle, ctx: Context): Promise<void>;
38
+ /**
39
+ * Rebuild a `Context` from a previously-persisted snapshot. Returns
40
+ * `null` if no snapshot is recorded for `executionId`. Layer state is
41
+ * replayed into `layerStateStore` keyed by the original executionId so
42
+ * the restored context observes the pre-crash state through
43
+ * `readLayerState` and the memory projectors.
44
+ *
45
+ * Preserves the original executionId on the returned context via
46
+ * `Object.defineProperty` — adapter correlation across crash/restart
47
+ * requires a stable id.
48
+ *
49
+ * @internal
50
+ */
51
+ export declare function restoreFromCheckpoint(h: CheckpointHarnessHandle, executionId: string): Promise<Context | null>;