@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
@@ -0,0 +1,101 @@
1
+ import { NoeticConfigError } from '../../errors/noetic-config-error';
2
+ import { CheckpointSnapshotSchema } from '../../types/checkpoint';
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';
2
+ import type { ItemSchemaRegistry } from '../../schemas/item';
3
+ import type { Context } from '../../types/context';
4
+ import type { Item } from '../../types/items';
5
+ import type { MemoryLayer } from '../../types/memory';
6
+ import type { CheckpointStore } from './checkpoint-store';
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>;
@@ -0,0 +1,104 @@
1
+ import { CheckpointSchemaVersion } from '../../types/checkpoint';
2
+ import { ContextImpl } from '../context-impl';
3
+ //#endregion
4
+ //#region captureCheckpoint
5
+ /**
6
+ * Snapshot the execution state at a checkpoint boundary. No-op when no
7
+ * `CheckpointStore` is configured — zero-config harnesses preserve
8
+ * ephemeral semantics. Save failures are logged rather than thrown,
9
+ * because a checkpoint failing must never abort an otherwise-successful
10
+ * step.
11
+ *
12
+ * @internal
13
+ */
14
+ export async function captureCheckpoint(h, ctx) {
15
+ const store = h.checkpointStore;
16
+ if (!store) {
17
+ return;
18
+ }
19
+ const impl = ctx instanceof ContextImpl ? ctx : null;
20
+ const frontier = impl ? impl.serialiseFrontier() : [];
21
+ const layers = {};
22
+ for (const layer of ctx.layers ?? []) {
23
+ const state = h.layerStateStore.get(ctx.id, layer.id);
24
+ if (state !== undefined) {
25
+ layers[layer.id] = state;
26
+ }
27
+ }
28
+ const snapshot = {
29
+ schemaVersion: CheckpointSchemaVersion,
30
+ executionId: ctx.id,
31
+ threadId: ctx.threadId,
32
+ resourceId: ctx.resourceId,
33
+ frontier,
34
+ layers,
35
+ cwd: {
36
+ current: ctx.cwdState.cwd,
37
+ previous: ctx.cwdState.previousCwd,
38
+ },
39
+ // Ask-user queue snapshot is empty at the core layer — the code-agent
40
+ // host is responsible for pushing pending prompts through this store
41
+ // via `AskUserService` integration. Carrying the shape from day one
42
+ // means future producers don't bump the schema version.
43
+ askUser: [],
44
+ itemLog: {
45
+ items: [
46
+ ...ctx.itemLog.items,
47
+ ],
48
+ },
49
+ capturedAt: new Date().toISOString(),
50
+ };
51
+ try {
52
+ await store.save(snapshot);
53
+ }
54
+ catch (err) {
55
+ console.warn(`AgentHarness.checkpoint: failed to persist snapshot for execution "${ctx.id}":`, err);
56
+ }
57
+ }
58
+ //#endregion
59
+ //#region restoreFromCheckpoint
60
+ /**
61
+ * Rebuild a `Context` from a previously-persisted snapshot. Returns
62
+ * `null` if no snapshot is recorded for `executionId`. Layer state is
63
+ * replayed into `layerStateStore` keyed by the original executionId so
64
+ * the restored context observes the pre-crash state through
65
+ * `readLayerState` and the memory projectors.
66
+ *
67
+ * Preserves the original executionId on the returned context via
68
+ * `Object.defineProperty` — adapter correlation across crash/restart
69
+ * requires a stable id.
70
+ *
71
+ * @internal
72
+ */
73
+ export async function restoreFromCheckpoint(h, executionId) {
74
+ const store = h.checkpointStore;
75
+ if (!store) {
76
+ return null;
77
+ }
78
+ const snapshot = await store.load(executionId);
79
+ if (!snapshot) {
80
+ return null;
81
+ }
82
+ for (const [layerId, state] of Object.entries(snapshot.layers)) {
83
+ h.layerStateStore.set(executionId, layerId, state);
84
+ }
85
+ const items = h.itemSchemas.parseMany(snapshot.itemLog.items);
86
+ const cwdInit = snapshot.cwd?.current ?? undefined;
87
+ const ctx = h.createContext({
88
+ items,
89
+ threadId: snapshot.threadId,
90
+ resourceId: snapshot.resourceId,
91
+ cwdInit,
92
+ memory: h._memory,
93
+ });
94
+ if (ctx instanceof ContextImpl) {
95
+ Object.defineProperty(ctx, 'id', {
96
+ value: executionId,
97
+ configurable: false,
98
+ writable: false,
99
+ enumerable: true,
100
+ });
101
+ }
102
+ return ctx;
103
+ }
104
+ //#endregion
@@ -0,0 +1,10 @@
1
+ export type { CheckpointStore, CreateCheckpointStoreOptions } from './checkpoint-store';
2
+ export { CheckpointKeys, createCheckpointStore } from './checkpoint-store';
3
+ export type { DetachedSignal } from './detached-signal';
4
+ export { createDetachedSignal } from './detached-signal';
5
+ export type { AfterFirstTurnContext, RunnableLoopHarness, RunnableLoopOpts } from './runnable-loop';
6
+ export { runnableLoop } from './runnable-loop';
7
+ export type { SessionSeedHarness } from './session-seed';
8
+ export { seedFromItems } from './session-seed';
9
+ export type { CreateNudgeMessageOpts, StallNudgeOpts } from './stall-nudge';
10
+ export { createNudgeMessage, createStallNudgeHook, DEFAULT_NUDGE_MESSAGE_TEXT, } from './stall-nudge';
@@ -0,0 +1,5 @@
1
+ export { CheckpointKeys, createCheckpointStore } from './checkpoint-store';
2
+ export { createDetachedSignal } from './detached-signal';
3
+ export { runnableLoop } from './runnable-loop';
4
+ export { seedFromItems } from './session-seed';
5
+ export { createNudgeMessage, createStallNudgeHook, DEFAULT_NUDGE_MESSAGE_TEXT, } from './stall-nudge';
@@ -0,0 +1,100 @@
1
+ /**
2
+ * Drive a generic agent-harness chat session for a detached runner.
3
+ *
4
+ * The loop is intentionally minimal. A "runnable" is any harness
5
+ * driven through the standard `execute()` path plus a single-shot
6
+ * `DetachedSignal` its terminal tool resolves:
7
+ *
8
+ * 1. **Resume spawn** — when `priorItems` is non-empty, seed the
9
+ * session history and hand control straight to the caller's
10
+ * signal. The agent's prior turns are already on record; the next
11
+ * turn comes from external input (IPC `send` frame, autopilot
12
+ * respawn, etc.) rather than being re-kicked here.
13
+ * 2. **Fresh spawn** — when `priorItems` is empty (or omitted) and
14
+ * an `initialMessage` is supplied, execute that framing message
15
+ * as the first turn. The harness's own stream pumps persist the
16
+ * emitted items via whatever session store the caller wired up.
17
+ * 3. **Post-turn hook** — if `afterFirstTurn` is supplied, run it
18
+ * once the first turn has settled microtasks. The hook sees
19
+ * whether the signal already resolved during the turn so it can
20
+ * short-circuit its own logic (e.g. the stall-nudge helper skips
21
+ * its nudge when the agent already called a terminal tool).
22
+ * 4. **Await outcome** — finally, return `signal.done`. If no
23
+ * terminal tool ever resolves it, the signal rejects externally
24
+ * (SIGTERM handler, process abort) or simply never settles — the
25
+ * caller owns that concern.
26
+ *
27
+ * The loop has no opinions about where `priorItems` come from, how the
28
+ * `initialMessage` is built, or what the terminal outcome looks like.
29
+ * Task-specific logic (chat.jsonl reads, role framing, stall
30
+ * escalation) lives in the caller and composes via the `afterFirstTurn`
31
+ * hook.
32
+ */
33
+ import type { HarnessResponse } from '../../types/harness-result';
34
+ import type { ExecuteInput, Item } from '../../types/items';
35
+ import type { ExecuteOptions, SessionScope } from '../../types/runtime';
36
+ import type { DetachedSignal } from './detached-signal';
37
+ /**
38
+ * Minimum harness surface the loop calls into. Defining this
39
+ * structurally keeps the loop loosely coupled — the concrete
40
+ * `AgentHarness<P>` already satisfies the shape, as does a test stub
41
+ * that only stands up `seedSessionHistory` + `execute` + `getAgentResponse`.
42
+ *
43
+ * `getAgentResponse` is REQUIRED because `execute()` only enqueues a
44
+ * message and returns before the turn actually runs. The loop awaits
45
+ * `getAgentResponse(scope)` to observe real turn completion before
46
+ * handing to `afterFirstTurn` — without this, stall detection fires
47
+ * before the agent has produced any output and flags every run as
48
+ * stalled.
49
+ */
50
+ export interface RunnableLoopHarness {
51
+ seedSessionHistory(threadId: string, items: ReadonlyArray<Item>): void;
52
+ execute(input: ExecuteInput, options?: ExecuteOptions): Promise<void>;
53
+ getAgentResponse(scope?: SessionScope): Promise<HarnessResponse>;
54
+ }
55
+ /**
56
+ * Read-only view the `afterFirstTurn` hook uses to decide whether the
57
+ * signal settled during the turn it just observed. Consumers call
58
+ * `signalSettled()` (not a direct property read) so a late-settle
59
+ * caused by microtask ordering still propagates.
60
+ */
61
+ export interface AfterFirstTurnContext {
62
+ /** True if `signal.done` has resolved or rejected. */
63
+ signalSettled(): boolean;
64
+ }
65
+ export interface RunnableLoopOpts<TOutcome> {
66
+ readonly harness: RunnableLoopHarness;
67
+ readonly threadId: string;
68
+ /**
69
+ * Prior session items to seed before the first turn. When non-empty
70
+ * the loop skips `initialMessage` entirely (resume path); when empty
71
+ * or omitted it proceeds with the fresh-spawn sequence.
72
+ */
73
+ readonly priorItems?: ReadonlyArray<Item>;
74
+ /**
75
+ * Developer-role framing message that kicks off a fresh run. Omit to
76
+ * run the loop in "seed + wait" mode where the first turn is driven
77
+ * externally (e.g. by an IPC client's `send` frame).
78
+ */
79
+ readonly initialMessage?: ExecuteInput;
80
+ readonly signal: DetachedSignal<TOutcome>;
81
+ /**
82
+ * Optional post-turn hook. Only runs on the fresh-spawn path once
83
+ * `initialMessage` has settled microtasks. Receives a context whose
84
+ * `signalSettled()` query reflects whether the signal is already
85
+ * resolved/rejected so the hook can no-op when the agent already
86
+ * produced its terminal outcome.
87
+ */
88
+ readonly afterFirstTurn?: (ctx: AfterFirstTurnContext) => Promise<void>;
89
+ }
90
+ /**
91
+ * Execute the runnable-loop lifecycle and return the final outcome by
92
+ * awaiting the caller-provided signal. See the module doc for the full
93
+ * sequence; short version:
94
+ *
95
+ * - `priorItems` present → seed + wait on signal.
96
+ * - `initialMessage` present → seed (if any) + execute first turn +
97
+ * run `afterFirstTurn` (if any) + wait on signal.
98
+ * - Neither present → just wait on signal (pure listener mode).
99
+ */
100
+ export declare function runnableLoop<TOutcome>(opts: RunnableLoopOpts<TOutcome>): Promise<TOutcome>;
@@ -0,0 +1,90 @@
1
+ /**
2
+ * Drive a generic agent-harness chat session for a detached runner.
3
+ *
4
+ * The loop is intentionally minimal. A "runnable" is any harness
5
+ * driven through the standard `execute()` path plus a single-shot
6
+ * `DetachedSignal` its terminal tool resolves:
7
+ *
8
+ * 1. **Resume spawn** — when `priorItems` is non-empty, seed the
9
+ * session history and hand control straight to the caller's
10
+ * signal. The agent's prior turns are already on record; the next
11
+ * turn comes from external input (IPC `send` frame, autopilot
12
+ * respawn, etc.) rather than being re-kicked here.
13
+ * 2. **Fresh spawn** — when `priorItems` is empty (or omitted) and
14
+ * an `initialMessage` is supplied, execute that framing message
15
+ * as the first turn. The harness's own stream pumps persist the
16
+ * emitted items via whatever session store the caller wired up.
17
+ * 3. **Post-turn hook** — if `afterFirstTurn` is supplied, run it
18
+ * once the first turn has settled microtasks. The hook sees
19
+ * whether the signal already resolved during the turn so it can
20
+ * short-circuit its own logic (e.g. the stall-nudge helper skips
21
+ * its nudge when the agent already called a terminal tool).
22
+ * 4. **Await outcome** — finally, return `signal.done`. If no
23
+ * terminal tool ever resolves it, the signal rejects externally
24
+ * (SIGTERM handler, process abort) or simply never settles — the
25
+ * caller owns that concern.
26
+ *
27
+ * The loop has no opinions about where `priorItems` come from, how the
28
+ * `initialMessage` is built, or what the terminal outcome looks like.
29
+ * Task-specific logic (chat.jsonl reads, role framing, stall
30
+ * escalation) lives in the caller and composes via the `afterFirstTurn`
31
+ * hook.
32
+ */
33
+ //#endregion
34
+ //#region Public API
35
+ /**
36
+ * Execute the runnable-loop lifecycle and return the final outcome by
37
+ * awaiting the caller-provided signal. See the module doc for the full
38
+ * sequence; short version:
39
+ *
40
+ * - `priorItems` present → seed + wait on signal.
41
+ * - `initialMessage` present → seed (if any) + execute first turn +
42
+ * run `afterFirstTurn` (if any) + wait on signal.
43
+ * - Neither present → just wait on signal (pure listener mode).
44
+ */
45
+ export async function runnableLoop(opts) {
46
+ const prior = opts.priorItems;
47
+ if (prior !== undefined && prior.length > 0) {
48
+ opts.harness.seedSessionHistory(opts.threadId, prior);
49
+ return opts.signal.done;
50
+ }
51
+ if (opts.initialMessage === undefined) {
52
+ return opts.signal.done;
53
+ }
54
+ // Track signal settlement without blocking so the `afterFirstTurn`
55
+ // hook can detect a synchronous resolve that fired during `execute()`
56
+ // without racing the deferred.
57
+ let signalSettled = false;
58
+ void opts.signal.done.then(() => {
59
+ signalSettled = true;
60
+ }, () => {
61
+ signalSettled = true;
62
+ });
63
+ await opts.harness.execute(opts.initialMessage, {
64
+ threadId: opts.threadId,
65
+ });
66
+ // `execute()` only enqueues the framing message; the turn itself runs
67
+ // asynchronously through the session runner. Await `getAgentResponse`
68
+ // to observe REAL turn completion before dispatching `afterFirstTurn`.
69
+ // Without this the stall-nudge hook fires before any turn output and
70
+ // always classifies the run as stalled. We race against `signal.done`
71
+ // so a terminal tool that resolves the signal mid-turn still short-
72
+ // circuits the wait — in that case `getAgentResponse` may not have
73
+ // settled yet and we skip the post-turn hook entirely since the
74
+ // terminal outcome has already fired.
75
+ await Promise.race([
76
+ opts.harness
77
+ .getAgentResponse({
78
+ threadId: opts.threadId,
79
+ })
80
+ .catch(() => undefined),
81
+ opts.signal.done.catch(() => undefined),
82
+ ]);
83
+ if (opts.afterFirstTurn !== undefined) {
84
+ await opts.afterFirstTurn({
85
+ signalSettled: () => signalSettled,
86
+ });
87
+ }
88
+ return opts.signal.done;
89
+ }
90
+ //#endregion
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Seed an agent harness's session history from a pre-parsed `Item[]`.
3
+ *
4
+ * Callers who have chat history on disk (JSONL, DB, etc.) read + parse
5
+ * the persisted format into `Item[]` themselves — this helper is
6
+ * deliberately path-free so it composes with any storage layer.
7
+ *
8
+ * The helper is a thin typed wrapper around `harness.seedSessionHistory`
9
+ * and exists so the runnable-loop (and any other caller that wants to
10
+ * restore a prior session) has one documented, named entrypoint rather
11
+ * than reaching for the harness method inline.
12
+ */
13
+ import type { Item } from '../../types/items';
14
+ /**
15
+ * Minimum harness surface `seedFromItems` requires. Defined structurally
16
+ * so tests (and out-of-process children) can pass a stub without casts;
17
+ * `AgentHarness<P>` from core satisfies the shape directly.
18
+ */
19
+ export interface SessionSeedHarness {
20
+ seedSessionHistory(threadId: string, items: ReadonlyArray<Item>): void;
21
+ }
22
+ /**
23
+ * Seed `harness`'s session for `threadId` with the given items so the
24
+ * next `execute()` call sees them in the item log. No-op when `items`
25
+ * is empty (the harness call itself is also safe to make with an empty
26
+ * array; short-circuiting here keeps the intent crisp).
27
+ */
28
+ export declare function seedFromItems(harness: SessionSeedHarness, threadId: string, items: ReadonlyArray<Item>): void;
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Seed an agent harness's session history from a pre-parsed `Item[]`.
3
+ *
4
+ * Callers who have chat history on disk (JSONL, DB, etc.) read + parse
5
+ * the persisted format into `Item[]` themselves — this helper is
6
+ * deliberately path-free so it composes with any storage layer.
7
+ *
8
+ * The helper is a thin typed wrapper around `harness.seedSessionHistory`
9
+ * and exists so the runnable-loop (and any other caller that wants to
10
+ * restore a prior session) has one documented, named entrypoint rather
11
+ * than reaching for the harness method inline.
12
+ */
13
+ //#endregion
14
+ //#region Public API
15
+ /**
16
+ * Seed `harness`'s session for `threadId` with the given items so the
17
+ * next `execute()` call sees them in the item log. No-op when `items`
18
+ * is empty (the harness call itself is also safe to make with an empty
19
+ * array; short-circuiting here keeps the intent crisp).
20
+ */
21
+ export function seedFromItems(harness, threadId, items) {
22
+ if (items.length === 0) {
23
+ return;
24
+ }
25
+ harness.seedSessionHistory(threadId, items);
26
+ }
27
+ //#endregion