@noetic-tools/core 0.0.1 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (254) hide show
  1. package/README.md +31 -11
  2. package/dist/adapters/in-memory-fs-adapter.d.ts +1 -1
  3. package/dist/adapters/in-memory-fs-adapter.js +1 -1
  4. package/dist/adapters/in-memory-shell-adapter.d.ts +1 -1
  5. package/dist/adapters/in-memory-subprocess/manifest-persistence.d.ts +8 -0
  6. package/dist/adapters/in-memory-subprocess/manifest-persistence.js +56 -0
  7. package/dist/adapters/in-memory-subprocess/metadata.d.ts +12 -0
  8. package/dist/adapters/in-memory-subprocess/metadata.js +58 -0
  9. package/dist/adapters/in-memory-subprocess/step-completion.d.ts +3 -0
  10. package/dist/adapters/in-memory-subprocess/step-completion.js +86 -0
  11. package/dist/adapters/in-memory-subprocess/types.d.ts +88 -0
  12. package/dist/adapters/in-memory-subprocess/types.js +2 -0
  13. package/dist/adapters/in-memory-subprocess-adapter.d.ts +12 -4
  14. package/dist/adapters/in-memory-subprocess-adapter.js +164 -55
  15. package/dist/adapters/openrouter.d.ts +16 -7
  16. package/dist/adapters/openrouter.js +68 -7
  17. package/dist/builders/channel-builder.d.ts +1 -1
  18. package/dist/builders/control-flow-builders.d.ts +3 -3
  19. package/dist/builders/control-flow-builders.js +9 -4
  20. package/dist/builders/every.d.ts +3 -3
  21. package/dist/builders/every.js +5 -2
  22. package/dist/builders/layer-provides-builders.d.ts +1 -32
  23. package/dist/builders/layer-provides-builders.js +1 -33
  24. package/dist/builders/loop-builder.d.ts +2 -2
  25. package/dist/builders/loop-builder.js +5 -2
  26. package/dist/builders/memory-builder.d.ts +1 -1
  27. package/dist/builders/memory-builder.js +1 -1
  28. package/dist/builders/provide-builder.d.ts +2 -2
  29. package/dist/builders/provide-builder.js +5 -2
  30. package/dist/builders/spawn-builder.d.ts +22 -9
  31. package/dist/builders/spawn-builder.js +17 -3
  32. package/dist/builders/step-builders.d.ts +51 -30
  33. package/dist/builders/step-builders.js +31 -12
  34. package/dist/builders/tool-builder.d.ts +7 -3
  35. package/dist/builders/tool-builder.js +3 -1
  36. package/dist/builders/workflow-hydrator.d.ts +38 -0
  37. package/dist/builders/workflow-hydrator.js +336 -0
  38. package/dist/conditions/conditions.d.ts +4 -4
  39. package/dist/conditions/conditions.js +2 -2
  40. package/dist/errors/noetic-error.d.ts +1 -1
  41. package/dist/errors/noetic-error.js +2 -0
  42. package/dist/{runtime → harness}/agent-harness.d.ts +65 -45
  43. package/dist/harness/agent-harness.js +576 -0
  44. package/dist/harness/deps/adapters.d.ts +4 -0
  45. package/dist/harness/deps/adapters.js +4 -0
  46. package/dist/harness/deps/interpreter.d.ts +5 -0
  47. package/dist/harness/deps/interpreter.js +4 -0
  48. package/dist/harness/deps/memory.d.ts +5 -0
  49. package/dist/harness/deps/memory.js +4 -0
  50. package/dist/harness/deps/runtime.d.ts +10 -0
  51. package/dist/harness/deps/runtime.js +7 -0
  52. package/dist/harness/deps/types.d.ts +17 -0
  53. package/dist/harness/deps/types.js +1 -0
  54. package/dist/harness/model-call.d.ts +50 -0
  55. package/dist/harness/model-call.js +694 -0
  56. package/dist/harness/model-schema.d.ts +19 -0
  57. package/dist/harness/model-schema.js +46 -0
  58. package/dist/harness/step-bootstrap.d.ts +25 -0
  59. package/dist/harness/step-bootstrap.js +170 -0
  60. package/dist/index.d.ts +114 -76
  61. package/dist/index.js +74 -50
  62. package/dist/internal-test.d.ts +4 -4
  63. package/dist/internal-test.js +4 -4
  64. package/dist/interpreter/action-deps.d.ts +12 -0
  65. package/dist/interpreter/action-deps.js +10 -0
  66. package/dist/interpreter/action-types.d.ts +8 -0
  67. package/dist/interpreter/action-types.js +1 -0
  68. package/dist/interpreter/collect-tools.d.ts +2 -2
  69. package/dist/interpreter/collect-tools.js +6 -2
  70. package/dist/interpreter/execute-action.d.ts +22 -0
  71. package/dist/interpreter/execute-action.js +527 -0
  72. package/dist/interpreter/{execute-every.d.ts → execute-control.d.ts} +8 -2
  73. package/dist/interpreter/execute-control.js +530 -0
  74. package/dist/interpreter/execute.d.ts +16 -3
  75. package/dist/interpreter/execute.js +154 -51
  76. package/dist/interpreter/message-helpers.d.ts +1 -9
  77. package/dist/interpreter/message-helpers.js +1 -42
  78. package/dist/interpreter/step-dispatcher.d.ts +61 -0
  79. package/dist/interpreter/step-dispatcher.js +67 -0
  80. package/dist/interpreter/typeguards.d.ts +8 -15
  81. package/dist/interpreter/typeguards.js +4 -21
  82. package/dist/memory/budget.d.ts +2 -2
  83. package/dist/memory/budget.js +1 -1
  84. package/dist/{runtime → memory}/exec-context-factory.d.ts +2 -2
  85. package/dist/{runtime → memory}/exec-context-factory.js +1 -1
  86. package/dist/memory/flow-schema.d.ts +52 -0
  87. package/dist/memory/flow-schema.js +102 -0
  88. package/dist/memory/function-call-utils.d.ts +1 -1
  89. package/dist/memory/function-call-utils.js +4 -2
  90. package/dist/memory/layer-api.d.ts +4 -4
  91. package/dist/memory/layer-api.js +2 -2
  92. package/dist/memory/layer-lifecycle.d.ts +6 -6
  93. package/dist/memory/layer-lifecycle.js +6 -6
  94. package/dist/memory/layer-provides.d.ts +32 -0
  95. package/dist/memory/layer-provides.js +33 -0
  96. package/dist/memory/layer-usage.d.ts +4 -3
  97. package/dist/memory/layer-usage.js +9 -3
  98. package/dist/memory/layers/durable-task-state.d.ts +8 -8
  99. package/dist/memory/layers/durable-task-state.js +2 -2
  100. package/dist/memory/layers/file-reference.d.ts +1 -1
  101. package/dist/memory/layers/file-reference.js +303 -247
  102. package/dist/memory/layers/history-window.d.ts +1 -1
  103. package/dist/memory/layers/history-window.js +4 -4
  104. package/dist/memory/layers/observational-memory.d.ts +5 -5
  105. package/dist/memory/layers/observational-memory.js +2 -3
  106. package/dist/memory/layers/plan.d.ts +2 -2
  107. package/dist/memory/layers/plan.js +15 -10
  108. package/dist/memory/layers/static-content.d.ts +2 -2
  109. package/dist/memory/layers/static-content.js +1 -1
  110. package/dist/memory/layers/steering.d.ts +7 -7
  111. package/dist/memory/layers/steering.js +5 -5
  112. package/dist/memory/layers/tool-memory-layer.d.ts +2 -2
  113. package/dist/memory/layers/tool-memory-layer.js +1 -1
  114. package/dist/memory/layers/working-memory.d.ts +7 -7
  115. package/dist/memory/layers/working-memory.js +4 -4
  116. package/dist/memory/projector.d.ts +2 -2
  117. package/dist/memory/scope.d.ts +1 -1
  118. package/dist/memory/strip-unresolved.d.ts +1 -1
  119. package/dist/observability/span-impl.d.ts +1 -1
  120. package/dist/observability/trace-exporter.d.ts +2 -2
  121. package/dist/observability/trace-exporter.js +1 -1
  122. package/dist/patterns/dynamic-workflow.d.ts +55 -0
  123. package/dist/patterns/dynamic-workflow.js +194 -0
  124. package/dist/patterns/flow.d.ts +2 -52
  125. package/dist/patterns/flow.js +1 -103
  126. package/dist/patterns/interview.d.ts +2 -2
  127. package/dist/patterns/interview.js +5 -5
  128. package/dist/patterns/plans.d.ts +6 -6
  129. package/dist/patterns/plans.js +4 -4
  130. package/dist/patterns/ralph-wiggum.d.ts +4 -4
  131. package/dist/patterns/ralph-wiggum.js +5 -5
  132. package/dist/patterns/react.d.ts +3 -3
  133. package/dist/patterns/react.js +5 -5
  134. package/dist/portable/adapters.d.ts +6 -0
  135. package/dist/portable/adapters.js +6 -0
  136. package/dist/portable/ask-user.d.ts +4 -0
  137. package/dist/portable/ask-user.js +2 -0
  138. package/dist/portable/builders.d.ts +12 -0
  139. package/dist/portable/builders.js +12 -0
  140. package/dist/portable/memory-layers.d.ts +12 -0
  141. package/dist/portable/memory-layers.js +12 -0
  142. package/dist/portable/runtime.d.ts +4 -0
  143. package/dist/portable/runtime.js +4 -0
  144. package/dist/portable/until.d.ts +4 -0
  145. package/dist/portable/until.js +4 -0
  146. package/dist/portable.d.ts +8 -26
  147. package/dist/portable.js +8 -34
  148. package/dist/runtime/broadcaster-utils.d.ts +7 -3
  149. package/dist/runtime/broadcaster-utils.js +11 -6
  150. package/dist/runtime/channel-store.d.ts +1 -1
  151. package/dist/runtime/channel-store.js +2 -2
  152. package/dist/runtime/context-deps.d.ts +4 -0
  153. package/dist/runtime/context-deps.js +2 -0
  154. package/dist/runtime/context-impl.d.ts +42 -12
  155. package/dist/runtime/context-impl.js +52 -5
  156. package/dist/runtime/context-type-deps.d.ts +14 -0
  157. package/dist/runtime/context-type-deps.js +1 -0
  158. package/dist/runtime/cwd-helpers.d.ts +1 -1
  159. package/dist/runtime/detached-handle.d.ts +52 -4
  160. package/dist/runtime/detached-handle.js +218 -14
  161. package/dist/runtime/durable/checkpoint-store.d.ts +38 -0
  162. package/dist/runtime/durable/checkpoint-store.js +101 -0
  163. package/dist/runtime/durable/detached-signal.d.ts +37 -0
  164. package/dist/runtime/durable/detached-signal.js +52 -0
  165. package/dist/runtime/durable/harness-checkpoints.d.ts +51 -0
  166. package/dist/runtime/durable/harness-checkpoints.js +104 -0
  167. package/dist/runtime/durable/index.d.ts +10 -0
  168. package/dist/runtime/durable/index.js +5 -0
  169. package/dist/runtime/durable/runnable-loop.d.ts +100 -0
  170. package/dist/runtime/durable/runnable-loop.js +90 -0
  171. package/dist/runtime/durable/session-seed.d.ts +28 -0
  172. package/dist/runtime/durable/session-seed.js +27 -0
  173. package/dist/runtime/durable/stall-nudge.d.ts +82 -0
  174. package/dist/runtime/durable/stall-nudge.js +87 -0
  175. package/dist/runtime/event-broadcaster.d.ts +1 -1
  176. package/dist/runtime/in-memory-storage.d.ts +1 -1
  177. package/dist/runtime/in-memory-storage.js +1 -1
  178. package/dist/runtime/item-log-impl.d.ts +3 -3
  179. package/dist/runtime/item-log-impl.js +1 -1
  180. package/dist/runtime/message-queue.d.ts +2 -2
  181. package/dist/runtime/session-runner.d.ts +7 -7
  182. package/dist/runtime/session-runner.js +3 -3
  183. package/dist/runtime/session-streams.d.ts +3 -3
  184. package/dist/runtime/session-streams.js +1 -1
  185. package/dist/runtime/step-registry.d.ts +42 -0
  186. package/dist/runtime/step-registry.js +77 -0
  187. package/dist/runtime/tool-memory.d.ts +3 -3
  188. package/dist/schemas/item.d.ts +1 -1
  189. package/dist/schemas/workflow.d.ts +139 -0
  190. package/dist/schemas/workflow.js +232 -0
  191. package/dist/types/ask-user-types.d.ts +57 -0
  192. package/dist/types/ask-user-types.js +74 -0
  193. package/dist/types/checkpoint.d.ts +98 -0
  194. package/dist/types/checkpoint.js +81 -0
  195. package/dist/types/common.d.ts +1 -50
  196. package/dist/types/context-parts/item-log.d.ts +6 -0
  197. package/dist/types/context-parts/item-log.js +1 -0
  198. package/dist/types/context-parts/layer-usage.d.ts +18 -0
  199. package/dist/types/context-parts/layer-usage.js +1 -0
  200. package/dist/types/context.d.ts +140 -31
  201. package/dist/types/error.d.ts +15 -0
  202. package/dist/types/harness-result.d.ts +2 -2
  203. package/dist/types/items.d.ts +18 -2
  204. package/dist/types/memory-context.d.ts +39 -0
  205. package/dist/types/memory-context.js +1 -0
  206. package/dist/types/memory.d.ts +38 -68
  207. package/dist/types/mutable-context.d.ts +3 -3
  208. package/dist/types/runtime.d.ts +14 -13
  209. package/dist/types/steering.d.ts +2 -2
  210. package/dist/types/step-registrar.d.ts +46 -0
  211. package/dist/types/step-registrar.js +56 -0
  212. package/dist/types/step.d.ts +34 -9
  213. package/dist/types/subprocess-adapter.d.ts +119 -3
  214. package/dist/types/subprocess-adapter.js +2 -0
  215. package/dist/types/tool-context.d.ts +6 -6
  216. package/dist/types/tool.d.ts +64 -0
  217. package/dist/types/tool.js +1 -0
  218. package/dist/unstable.d.ts +9 -9
  219. package/dist/unstable.js +7 -7
  220. package/dist/until/combinators.d.ts +1 -1
  221. package/dist/until/never.d.ts +1 -1
  222. package/dist/until/predicates.d.ts +13 -4
  223. package/dist/until/predicates.js +16 -2
  224. package/dist/util/message-helpers.d.ts +17 -0
  225. package/dist/util/message-helpers.js +64 -0
  226. package/package.json +6 -4
  227. package/dist/adapters/local-fs-adapter.d.ts +0 -3
  228. package/dist/adapters/local-fs-adapter.js +0 -43
  229. package/dist/adapters/local-shell-adapter.d.ts +0 -51
  230. package/dist/adapters/local-shell-adapter.js +0 -167
  231. package/dist/adapters/local-subprocess-adapter.d.ts +0 -16
  232. package/dist/adapters/local-subprocess-adapter.js +0 -187
  233. package/dist/conditions/index.d.ts +0 -3
  234. package/dist/conditions/index.js +0 -2
  235. package/dist/interpreter/execute-branch.d.ts +0 -3
  236. package/dist/interpreter/execute-branch.js +0 -10
  237. package/dist/interpreter/execute-every.js +0 -143
  238. package/dist/interpreter/execute-fork.d.ts +0 -3
  239. package/dist/interpreter/execute-fork.js +0 -215
  240. package/dist/interpreter/execute-llm.d.ts +0 -4
  241. package/dist/interpreter/execute-llm.js +0 -217
  242. package/dist/interpreter/execute-loop.d.ts +0 -3
  243. package/dist/interpreter/execute-loop.js +0 -171
  244. package/dist/interpreter/execute-provide.d.ts +0 -10
  245. package/dist/interpreter/execute-provide.js +0 -64
  246. package/dist/interpreter/execute-run.d.ts +0 -3
  247. package/dist/interpreter/execute-run.js +0 -39
  248. package/dist/interpreter/execute-spawn.d.ts +0 -11
  249. package/dist/interpreter/execute-spawn.js +0 -113
  250. package/dist/interpreter/execute-tool.d.ts +0 -5
  251. package/dist/interpreter/execute-tool.js +0 -81
  252. package/dist/runtime/agent-harness.js +0 -1170
  253. /package/dist/{interpreter → util}/framework-cast.d.ts +0 -0
  254. /package/dist/{interpreter → util}/framework-cast.js +0 -0
@@ -0,0 +1,104 @@
1
+ import { CheckpointSchemaVersion } from '../../types/checkpoint.js';
2
+ import { ContextImpl } from '../context-impl.js';
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.js';
2
+ export { CheckpointKeys, createCheckpointStore } from './checkpoint-store.js';
3
+ export type { DetachedSignal } from './detached-signal.js';
4
+ export { createDetachedSignal } from './detached-signal.js';
5
+ export type { AfterFirstTurnContext, RunnableLoopHarness, RunnableLoopOpts } from './runnable-loop.js';
6
+ export { runnableLoop } from './runnable-loop.js';
7
+ export type { SessionSeedHarness } from './session-seed.js';
8
+ export { seedFromItems } from './session-seed.js';
9
+ export type { CreateNudgeMessageOpts, StallNudgeOpts } from './stall-nudge.js';
10
+ export { createNudgeMessage, createStallNudgeHook, DEFAULT_NUDGE_MESSAGE_TEXT, } from './stall-nudge.js';
@@ -0,0 +1,5 @@
1
+ export { CheckpointKeys, createCheckpointStore } from './checkpoint-store.js';
2
+ export { createDetachedSignal } from './detached-signal.js';
3
+ export { runnableLoop } from './runnable-loop.js';
4
+ export { seedFromItems } from './session-seed.js';
5
+ export { createNudgeMessage, createStallNudgeHook, DEFAULT_NUDGE_MESSAGE_TEXT, } from './stall-nudge.js';
@@ -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.js';
34
+ import type { ExecuteInput, Item } from '../../types/items.js';
35
+ import type { ExecuteOptions, SessionScope } from '../../types/runtime.js';
36
+ import type { DetachedSignal } from './detached-signal.js';
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.js';
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
@@ -0,0 +1,82 @@
1
+ /**
2
+ * Two-strike stall-nudge helper composable with `runnableLoop`.
3
+ *
4
+ * A runner's first turn can end without settling the outcome signal
5
+ * and without the agent asking for external input — i.e. the agent
6
+ * stopped without calling either a terminal tool or an ask-user-style
7
+ * tool. That's a soft failure: rather than wait forever, we nudge the
8
+ * agent once to remind it of its options. If the second turn also
9
+ * ends in the same stalled shape we escalate: the caller-supplied
10
+ * `onStall` callback records the escalation (task pause, metrics,
11
+ * whatever the domain demands) and we settle the signal ourselves with
12
+ * a canonical "stalled" outcome so the parent unwinds cleanly.
13
+ *
14
+ * The helper factors the state machine out of the loop so callers can
15
+ * opt in per-runner. Task-specific concerns (how "pending external
16
+ * input" is detected, how escalation is recorded, what outcome
17
+ * represents a stalled failure) stay with the caller and flow in via
18
+ * the config.
19
+ */
20
+ import type { InputMessageItem } from '../../types/items.js';
21
+ import type { DetachedSignal } from './detached-signal.js';
22
+ import type { AfterFirstTurnContext, RunnableLoopHarness } from './runnable-loop.js';
23
+ /**
24
+ * Default developer-role text sent when the agent finishes a turn
25
+ * without calling a terminal tool or an ask-user tool. Kept short and
26
+ * unambiguous — the agent should be reminded of its options without
27
+ * being lectured. Callers may override via `createNudgeMessage(text)`.
28
+ */
29
+ export declare const DEFAULT_NUDGE_MESSAGE_TEXT = "You finished a turn without calling a terminal tool or AskUserQuestion. If you need user input or instruction to continue, call AskUserQuestion now. Otherwise call your terminal tool to complete this phase.";
30
+ export interface CreateNudgeMessageOpts {
31
+ /** Unique id for the message so item-log dedupe can distinguish re-spawns. */
32
+ readonly id: string;
33
+ /** Override the developer-role text. Defaults to `DEFAULT_NUDGE_MESSAGE_TEXT`. */
34
+ readonly text?: string;
35
+ }
36
+ export interface StallNudgeOpts<TOutcome> {
37
+ readonly harness: RunnableLoopHarness;
38
+ readonly threadId: string;
39
+ readonly signal: DetachedSignal<TOutcome>;
40
+ /**
41
+ * The framing message sent as the nudge. Use `createNudgeMessage`
42
+ * for the canonical developer-role item, or pass your own when a
43
+ * domain needs a custom shape.
44
+ */
45
+ readonly nudgeMessage: InputMessageItem;
46
+ /**
47
+ * True when the agent intentionally awaits external input (e.g. an
48
+ * outstanding ask-user request). When true the nudge is suppressed
49
+ * — the agent is not stalled, it is waiting for us.
50
+ */
51
+ hasPendingExternal(): boolean;
52
+ /**
53
+ * Record-side-effect callback fired when the agent stalls a second
54
+ * time. Use this to mark the task paused, emit a metric, append an
55
+ * event log entry — whatever the domain needs before the signal
56
+ * settles.
57
+ */
58
+ onStall(): Promise<void>;
59
+ /** Build the outcome value the signal resolves to on stall escalation. */
60
+ buildStalledOutcome(): TOutcome;
61
+ }
62
+ /**
63
+ * Build the canonical developer-role nudge message item. Callers who
64
+ * want a custom framing build their own `InputMessageItem`; this
65
+ * helper exists so the common case stays one line.
66
+ */
67
+ export declare function createNudgeMessage(opts: CreateNudgeMessageOpts): InputMessageItem;
68
+ /**
69
+ * Build a `runnableLoop`-compatible `afterFirstTurn` hook that
70
+ * implements the two-strike nudge state machine described in the
71
+ * module doc. The hook is a no-op when:
72
+ *
73
+ * - the signal already settled (agent called a terminal tool), or
74
+ * - the caller reports pending external input (`hasPendingExternal`
75
+ * returns true — e.g. an outstanding ask-user request).
76
+ *
77
+ * Otherwise it sends the nudge message via `harness.execute`, waits
78
+ * for microtasks to flush, and re-checks the same two conditions. If
79
+ * the agent still hasn't progressed, the hook fires `onStall` and
80
+ * resolves the signal with `buildStalledOutcome()`.
81
+ */
82
+ export declare function createStallNudgeHook<TOutcome>(opts: StallNudgeOpts<TOutcome>): (ctx: AfterFirstTurnContext) => Promise<void>;
@@ -0,0 +1,87 @@
1
+ /**
2
+ * Two-strike stall-nudge helper composable with `runnableLoop`.
3
+ *
4
+ * A runner's first turn can end without settling the outcome signal
5
+ * and without the agent asking for external input — i.e. the agent
6
+ * stopped without calling either a terminal tool or an ask-user-style
7
+ * tool. That's a soft failure: rather than wait forever, we nudge the
8
+ * agent once to remind it of its options. If the second turn also
9
+ * ends in the same stalled shape we escalate: the caller-supplied
10
+ * `onStall` callback records the escalation (task pause, metrics,
11
+ * whatever the domain demands) and we settle the signal ourselves with
12
+ * a canonical "stalled" outcome so the parent unwinds cleanly.
13
+ *
14
+ * The helper factors the state machine out of the loop so callers can
15
+ * opt in per-runner. Task-specific concerns (how "pending external
16
+ * input" is detected, how escalation is recorded, what outcome
17
+ * represents a stalled failure) stay with the caller and flow in via
18
+ * the config.
19
+ */
20
+ //#region Constants
21
+ /**
22
+ * Default developer-role text sent when the agent finishes a turn
23
+ * without calling a terminal tool or an ask-user tool. Kept short and
24
+ * unambiguous — the agent should be reminded of its options without
25
+ * being lectured. Callers may override via `createNudgeMessage(text)`.
26
+ */
27
+ export const DEFAULT_NUDGE_MESSAGE_TEXT = 'You finished a turn without calling a terminal tool or AskUserQuestion. If you need user input or instruction to continue, call AskUserQuestion now. Otherwise call your terminal tool to complete this phase.';
28
+ //#endregion
29
+ //#region Public API
30
+ /**
31
+ * Build the canonical developer-role nudge message item. Callers who
32
+ * want a custom framing build their own `InputMessageItem`; this
33
+ * helper exists so the common case stays one line.
34
+ */
35
+ export function createNudgeMessage(opts) {
36
+ return {
37
+ id: opts.id,
38
+ type: 'message',
39
+ role: 'developer',
40
+ status: 'completed',
41
+ content: [
42
+ {
43
+ type: 'input_text',
44
+ text: opts.text ?? DEFAULT_NUDGE_MESSAGE_TEXT,
45
+ },
46
+ ],
47
+ };
48
+ }
49
+ /**
50
+ * Build a `runnableLoop`-compatible `afterFirstTurn` hook that
51
+ * implements the two-strike nudge state machine described in the
52
+ * module doc. The hook is a no-op when:
53
+ *
54
+ * - the signal already settled (agent called a terminal tool), or
55
+ * - the caller reports pending external input (`hasPendingExternal`
56
+ * returns true — e.g. an outstanding ask-user request).
57
+ *
58
+ * Otherwise it sends the nudge message via `harness.execute`, waits
59
+ * for microtasks to flush, and re-checks the same two conditions. If
60
+ * the agent still hasn't progressed, the hook fires `onStall` and
61
+ * resolves the signal with `buildStalledOutcome()`.
62
+ */
63
+ export function createStallNudgeHook(opts) {
64
+ return async (ctx) => {
65
+ if (ctx.signalSettled()) {
66
+ return;
67
+ }
68
+ if (opts.hasPendingExternal()) {
69
+ return;
70
+ }
71
+ // First stall — send one nudge.
72
+ await opts.harness.execute(opts.nudgeMessage, {
73
+ threadId: opts.threadId,
74
+ });
75
+ await Promise.resolve();
76
+ if (ctx.signalSettled()) {
77
+ return;
78
+ }
79
+ if (opts.hasPendingExternal()) {
80
+ return;
81
+ }
82
+ // Second stall — escalate and settle.
83
+ await opts.onStall();
84
+ opts.signal.resolve(opts.buildStalledOutcome());
85
+ };
86
+ }
87
+ //#endregion
@@ -1,4 +1,4 @@
1
- import type { StreamEvent } from '../types/harness-result';
1
+ import type { StreamEvent } from '../types/harness-result.js';
2
2
  interface EventBroadcasterOpts {
3
3
  maxBufferSize?: number;
4
4
  }
@@ -1,4 +1,4 @@
1
- import type { StorageAdapter } from '../types/memory';
1
+ import type { StorageAdapter } from '../types/memory.js';
2
2
  /**
3
3
  * @public Process-local, Map-backed StorageAdapter.
4
4
  *
@@ -1,4 +1,4 @@
1
- import { frameworkCast } from '../interpreter/framework-cast';
1
+ import { frameworkCast } from '../util/framework-cast.js';
2
2
  //#region Public API
3
3
  /**
4
4
  * @public Process-local, Map-backed StorageAdapter.
@@ -1,6 +1,6 @@
1
- import type { ItemSchemaRegistry } from '../schemas/item';
2
- import type { ItemLog } from '../types/context';
3
- import type { Item } from '../types/items';
1
+ import type { ItemSchemaRegistry } from '../schemas/item.js';
2
+ import type { ItemLog } from '../types/context-parts/item-log.js';
3
+ import type { Item } from '../types/items.js';
4
4
  export declare class ItemLogImpl implements ItemLog {
5
5
  private readonly itemSchemas;
6
6
  private readonly _items;
@@ -1,4 +1,4 @@
1
- import { defaultItemSchemaRegistry } from '../schemas/item';
1
+ import { defaultItemSchemaRegistry } from '../schemas/item.js';
2
2
  export class ItemLogImpl {
3
3
  itemSchemas;
4
4
  _items = [];
@@ -1,5 +1,5 @@
1
- import type { ExecuteInput } from '../types/items';
2
- import type { DeliveryMode, ExecuteOptions } from '../types/runtime';
1
+ import type { ExecuteInput } from '../types/items.js';
2
+ import type { DeliveryMode, ExecuteOptions } from '../types/runtime.js';
3
3
  /** @internal A message accepted by `AgentHarness.execute()` and held until the
4
4
  * session runner processes it. The effective `DeliveryMode` is resolved at
5
5
  * enqueue time so the runner never has to look at harness defaults again. */
@@ -1,10 +1,10 @@
1
- import type { Context } from '../types/context';
2
- import type { HarnessResponse } from '../types/harness-result';
3
- import type { Item } from '../types/items';
4
- import type { HarnessStatus } from '../types/runtime';
5
- import { EventBroadcaster } from './event-broadcaster';
6
- import type { QueuedMessage } from './message-queue';
7
- import { MessageQueue } from './message-queue';
1
+ import type { Context } from '../types/context.js';
2
+ import type { HarnessResponse } from '../types/harness-result.js';
3
+ import type { Item } from '../types/items.js';
4
+ import type { HarnessStatus } from '../types/runtime.js';
5
+ import { EventBroadcaster } from './event-broadcaster.js';
6
+ import type { QueuedMessage } from './message-queue.js';
7
+ import { MessageQueue } from './message-queue.js';
8
8
  /** @internal Callback the harness supplies to run a single turn against a
9
9
  * prepared context. The runner invokes this once per turn; it returns the
10
10
  * final text as a string and must append any produced items to `ctx.itemLog`. */
@@ -1,6 +1,6 @@
1
- import { emitFrameworkEvent } from './broadcaster-utils';
2
- import { EventBroadcaster } from './event-broadcaster';
3
- import { MessageQueue } from './message-queue';
1
+ import { emitFrameworkEvent } from './broadcaster-utils.js';
2
+ import { EventBroadcaster } from './event-broadcaster.js';
3
+ import { MessageQueue } from './message-queue.js';
4
4
  //#endregion
5
5
  //#region Helpers
6
6
  function inputToItems(input) {
@@ -1,6 +1,6 @@
1
- import type { ItemSchemaRegistry } from '../schemas/item';
2
- import type { StreamingItem } from '../types/harness-result';
3
- import type { EventBroadcaster } from './event-broadcaster';
1
+ import type { ItemSchemaRegistry } from '../schemas/item.js';
2
+ import type { StreamingItem } from '../types/harness-result.js';
3
+ import type { EventBroadcaster } from './event-broadcaster.js';
4
4
  /** @internal Yield text deltas from the session broadcaster. */
5
5
  export declare function filterTextStream(broadcaster: EventBroadcaster): AsyncIterable<string>;
6
6
  /** @internal Yield reasoning-token deltas from the session broadcaster. */
@@ -1,4 +1,4 @@
1
- import { defaultItemSchemaRegistry } from '../schemas/item';
1
+ import { defaultItemSchemaRegistry } from '../schemas/item.js';
2
2
  //#endregion
3
3
  //#region Helpers
4
4
  function isItemRecord(value) {