@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
package/README.md CHANGED
@@ -5,33 +5,53 @@ Core primitives for the [Noetic](https://github.com/mattapperson/noetic) agent f
5
5
  ## Install
6
6
 
7
7
  ```bash
8
- npm install @noetic-tools/core @openrouter/agent zod
8
+ npm install @noetic-tools/core
9
9
  ```
10
10
 
11
- `@openrouter/agent` is an optional peer dependency — required only if you use the OpenRouter adapter. `zod` is required.
11
+ `@noetic-tools/core` depends on `@openrouter/agent` (the OpenRouter LLM adapter) and `zod`; your package manager installs both automatically. Runs on Node.js 18+ and Bun.
12
+
13
+ ## Set your API key
14
+
15
+ Noetic talks to LLMs through [OpenRouter](https://openrouter.ai). Grab a key at [openrouter.ai/keys](https://openrouter.ai/keys) and export it — the runtime reads `OPENROUTER_API_KEY` by default:
16
+
17
+ ```bash
18
+ export OPENROUTER_API_KEY=sk-or-...
19
+ ```
12
20
 
13
21
  ## Quick start
14
22
 
23
+ A complete, runnable program. Save it as `agent.mjs` and run `node agent.mjs`:
24
+
15
25
  ```ts
16
26
  import { AgentHarness, step } from '@noetic-tools/core';
17
27
 
18
- const harness = new AgentHarness({
19
- model: 'anthropic/claude-sonnet-4',
20
- llm: { apiKey: process.env.OPENROUTER_API_KEY },
21
- });
22
-
28
+ // 1. Define a single LLM step. `model` and `instructions` live here.
23
29
  const greet = step.llm({
24
30
  id: 'greet',
25
- prompt: ({ input }) => `Say hi to ${input}.`,
31
+ model: 'openai/gpt-4o-mini', // any OpenRouter model id (provider/model)
32
+ instructions: 'You are a friendly assistant. Greet the user by name in one short sentence.',
26
33
  });
27
34
 
28
- const result = await harness.run(greet, 'Sam');
29
- console.log(result.output);
35
+ // 2. Create the harness. `name` and `params` are required; `llm` selects the provider.
36
+ const harness = new AgentHarness({
37
+ name: 'greeter',
38
+ params: {},
39
+ llm: { provider: 'openrouter' }, // apiKey defaults to process.env.OPENROUTER_API_KEY
40
+ });
41
+
42
+ // 3. Run the step with a context and print the model's reply.
43
+ const ctx = harness.createContext();
44
+ const reply = await harness.run(greet, 'Sam', ctx);
45
+ console.log(reply); // -> "Hello, Sam! How can I help you today?"
30
46
  ```
31
47
 
48
+ **Using TypeScript?** Save the file as `agent.ts` and run it with `bunx tsx agent.ts` (or `bun run agent.ts`) — both execute TypeScript directly, no build step.
49
+
50
+ Model ids use OpenRouter's `provider/model` format (e.g. `openai/gpt-4o`, `anthropic/claude-sonnet-4`) — see [openrouter.ai/models](https://openrouter.ai/models).
51
+
32
52
  ## Documentation
33
53
 
34
- Full docs, specs, and examples live in the [main repo](https://github.com/mattapperson/noetic).
54
+ Full docs, specs, and examples live at [noetic.tools/docs](https://noetic.tools/docs) and in the [main repo](https://github.com/mattapperson/noetic).
35
55
 
36
56
  ## License
37
57
 
@@ -1,3 +1,3 @@
1
- import type { FsAdapter } from '../types/fs-adapter';
1
+ import type { FsAdapter } from '../types/fs-adapter.js';
2
2
  /** @public Create a process-local, POSIX-like in-memory filesystem adapter. */
3
3
  export declare function createInMemoryFsAdapter(seed?: Record<string, string | Uint8Array>): FsAdapter;
@@ -1,4 +1,4 @@
1
- import { frameworkCast } from '../interpreter/framework-cast';
1
+ import { frameworkCast } from '../util/framework-cast.js';
2
2
  function normalizePath(path) {
3
3
  const raw = path.trim();
4
4
  if (raw.length === 0) {
@@ -1,4 +1,4 @@
1
- import type { ShellAdapter } from '../types/shell-adapter';
1
+ import type { ShellAdapter } from '../types/shell-adapter.js';
2
2
  /**
3
3
  * @public Create a portable shell adapter that does not execute host processes.
4
4
  *
@@ -0,0 +1,8 @@
1
+ import type { StorageAdapter } from '../../types/memory.js';
2
+ import type { StepSubprocessRequest, SubprocessHandle } from '../../types/subprocess-adapter.js';
3
+ import type { InMemoryStepManifest } from './types.js';
4
+ export declare function isStepManifest(value: unknown): value is InMemoryStepManifest;
5
+ export declare function persistStepManifest(storage: StorageAdapter, request: StepSubprocessRequest, handle: SubprocessHandle): Promise<void>;
6
+ export declare function clearManifest(storage: StorageAdapter, handleId: string): Promise<void>;
7
+ export declare function loadManifest(storage: StorageAdapter, handleId: string): Promise<InMemoryStepManifest | null>;
8
+ export declare function listManifests(storage: StorageAdapter): Promise<InMemoryStepManifest[]>;
@@ -0,0 +1,56 @@
1
+ import { isRecord } from './metadata.js';
2
+ //#region Key layout
3
+ const IN_MEMORY_MANIFEST_PREFIX = 'inMemorySubprocess:manifest:';
4
+ function manifestKey(handleId) {
5
+ return `${IN_MEMORY_MANIFEST_PREFIX}${handleId}`;
6
+ }
7
+ //#endregion
8
+ //#region Guards
9
+ export function isStepManifest(value) {
10
+ if (!isRecord(value)) {
11
+ return false;
12
+ }
13
+ return (value.kind === 'step' &&
14
+ typeof value.handleId === 'string' &&
15
+ typeof value.stepId === 'string' &&
16
+ typeof value.executionId === 'string' &&
17
+ typeof value.startedAt === 'string' &&
18
+ isRecord(value.overrides));
19
+ }
20
+ //#endregion
21
+ //#region CRUD
22
+ export async function persistStepManifest(storage, request, handle) {
23
+ const manifest = {
24
+ kind: 'step',
25
+ handleId: handle.id,
26
+ stepId: request.stepId,
27
+ executionId: request.executionId,
28
+ serializedInput: request.serializedInput,
29
+ overrides: request.overrides,
30
+ startedAt: handle.startedAt,
31
+ reattachMode: 'replay',
32
+ };
33
+ await storage.set(manifestKey(handle.id), manifest);
34
+ }
35
+ export async function clearManifest(storage, handleId) {
36
+ await storage.delete(manifestKey(handleId));
37
+ }
38
+ export async function loadManifest(storage, handleId) {
39
+ const raw = await storage.get(manifestKey(handleId));
40
+ if (raw === null) {
41
+ return null;
42
+ }
43
+ return isStepManifest(raw) ? raw : null;
44
+ }
45
+ export async function listManifests(storage) {
46
+ const keys = await storage.list(IN_MEMORY_MANIFEST_PREFIX);
47
+ const out = [];
48
+ for (const key of keys) {
49
+ const raw = await storage.get(key);
50
+ if (isStepManifest(raw)) {
51
+ out.push(raw);
52
+ }
53
+ }
54
+ return out;
55
+ }
56
+ //#endregion
@@ -0,0 +1,12 @@
1
+ import type { ProcessSubprocessRequest, SerializedError, StepSubprocessRequest, SubprocessHandleMetadata, SubprocessRequest } from '../../types/subprocess-adapter.js';
2
+ export declare function nowIso(): string;
3
+ export declare function isStepRequest(request: SubprocessRequest): request is StepSubprocessRequest;
4
+ export declare function isProcessRequest(request: SubprocessRequest): request is ProcessSubprocessRequest;
5
+ export declare function isRecord(value: unknown): value is Record<string, unknown>;
6
+ /** Convert a thrown value into a `SerializedError`. `NoeticErrorImpl`
7
+ * instances (and anything exposing `noeticError`) retain their structured
8
+ * payload so consumers can rehydrate typed kinds without a brittle message
9
+ * regex. */
10
+ export declare function serializeError(err: unknown): SerializedError;
11
+ export declare function buildProcessMetadata(request: ProcessSubprocessRequest): SubprocessHandleMetadata;
12
+ export declare function buildStepMetadata(request: StepSubprocessRequest): SubprocessHandleMetadata;
@@ -0,0 +1,58 @@
1
+ //#region Time
2
+ export function nowIso() {
3
+ return new Date().toISOString();
4
+ }
5
+ //#endregion
6
+ //#region Type guards
7
+ export function isStepRequest(request) {
8
+ return request.kind === 'step';
9
+ }
10
+ export function isProcessRequest(request) {
11
+ return request.kind !== 'step';
12
+ }
13
+ export function isRecord(value) {
14
+ return typeof value === 'object' && value !== null;
15
+ }
16
+ //#endregion
17
+ //#region Error serialisation
18
+ /** Convert a thrown value into a `SerializedError`. `NoeticErrorImpl`
19
+ * instances (and anything exposing `noeticError`) retain their structured
20
+ * payload so consumers can rehydrate typed kinds without a brittle message
21
+ * regex. */
22
+ export function serializeError(err) {
23
+ if (err instanceof Error) {
24
+ const payload = {
25
+ message: err.message,
26
+ name: err.name,
27
+ stack: err.stack,
28
+ };
29
+ if (isRecord(err) && 'noeticError' in err) {
30
+ payload.noeticError = err.noeticError;
31
+ }
32
+ return payload;
33
+ }
34
+ return {
35
+ message: typeof err === 'string' ? err : String(err),
36
+ };
37
+ }
38
+ //#endregion
39
+ //#region Metadata builders
40
+ export function buildProcessMetadata(request) {
41
+ return {
42
+ ...(request.metadata ?? {}),
43
+ runtime: 'in-memory',
44
+ command: request.command,
45
+ args: request.args ?? [],
46
+ cwd: request.cwd,
47
+ };
48
+ }
49
+ export function buildStepMetadata(request) {
50
+ return {
51
+ ...(request.metadata ?? {}),
52
+ runtime: 'in-memory',
53
+ kind: 'step',
54
+ stepId: request.stepId,
55
+ executionId: request.executionId,
56
+ };
57
+ }
58
+ //#endregion
@@ -0,0 +1,3 @@
1
+ import type { CompleteProcessRunParams, CompleteStepRunParams } from './types.js';
2
+ export declare function completeProcessRun({ request, handle, run, handles, active, save, clearIfDurable, }: CompleteProcessRunParams): Promise<void>;
3
+ export declare function completeStepRun({ request, handle, stepRunner, handles, active, save, clearIfDurable, }: CompleteStepRunParams): Promise<void>;
@@ -0,0 +1,86 @@
1
+ import { nowIso, serializeError } from './metadata.js';
2
+ //#region Completion handlers
3
+ export async function completeProcessRun({ request, handle, run, handles, active, save, clearIfDurable, }) {
4
+ try {
5
+ await run?.(request, handle);
6
+ const latest = handles.get(handle.id);
7
+ if (!latest || latest.status !== 'running') {
8
+ return;
9
+ }
10
+ active.delete(handle.id);
11
+ await save({
12
+ ...latest,
13
+ status: 'completed',
14
+ updatedAt: nowIso(),
15
+ });
16
+ // Clear the durable handle manifest once the run has terminated so
17
+ // `listLive()` stops returning this handle. Without this call the
18
+ // manifest would persist until `stop()` was called, making every
19
+ // completed step look live from the adapter's persistence layer.
20
+ await clearIfDurable(handle.id);
21
+ }
22
+ catch (err) {
23
+ active.delete(handle.id);
24
+ const latest = handles.get(handle.id) ?? handle;
25
+ await save({
26
+ ...latest,
27
+ status: 'failed',
28
+ updatedAt: nowIso(),
29
+ metadata: {
30
+ ...(latest.metadata ?? {}),
31
+ error: serializeError(err),
32
+ },
33
+ });
34
+ // Same rationale as the success branch above — clear the durable
35
+ // manifest so `listLive()` doesn't resurrect this handle on reboot.
36
+ await clearIfDurable(handle.id);
37
+ }
38
+ }
39
+ export async function completeStepRun({ request, handle, stepRunner, handles, active, save, clearIfDurable, }) {
40
+ try {
41
+ const executor = request._localExecutor;
42
+ let result;
43
+ if (executor) {
44
+ result = await executor();
45
+ }
46
+ else if (stepRunner) {
47
+ result = await stepRunner(request, handle);
48
+ }
49
+ else {
50
+ throw new Error(`In-memory subprocess adapter has no executor for step "${request.stepId}". ` +
51
+ 'Provide a stepRunner option or dispatch via AgentHarness.');
52
+ }
53
+ const latest = handles.get(handle.id);
54
+ if (!latest || latest.status !== 'running') {
55
+ return;
56
+ }
57
+ active.delete(handle.id);
58
+ await save({
59
+ ...latest,
60
+ status: 'completed',
61
+ updatedAt: nowIso(),
62
+ metadata: {
63
+ ...(latest.metadata ?? {}),
64
+ result,
65
+ },
66
+ });
67
+ // Clear the durable step manifest once the run has terminated —
68
+ // see `completeProcessRun` above for the rationale.
69
+ await clearIfDurable(handle.id);
70
+ }
71
+ catch (err) {
72
+ active.delete(handle.id);
73
+ const latest = handles.get(handle.id) ?? handle;
74
+ await save({
75
+ ...latest,
76
+ status: 'failed',
77
+ updatedAt: nowIso(),
78
+ metadata: {
79
+ ...(latest.metadata ?? {}),
80
+ error: serializeError(err),
81
+ },
82
+ });
83
+ await clearIfDurable(handle.id);
84
+ }
85
+ }
86
+ //#endregion
@@ -0,0 +1,88 @@
1
+ import type { StorageAdapter } from '../../types/memory.js';
2
+ import type { ProcessSubprocessRequest, StepSubprocessRequest, SubprocessHandle, SubprocessHandleMetadata, SubprocessRequest } from '../../types/subprocess-adapter.js';
3
+ /**
4
+ * @public
5
+ * Options for `createInMemorySubprocessAdapter`. All hooks are optional;
6
+ * when none is provided the adapter still handles `spawn` + `stop` +
7
+ * lifecycle transitions for test-double purposes.
8
+ */
9
+ export interface CreateInMemorySubprocessAdapterOptions {
10
+ /**
11
+ * Invoked for `kind: 'process'` requests (the classic test-double hook).
12
+ * When absent the adapter marks the handle `completed` on the next
13
+ * microtask without running any body.
14
+ */
15
+ run?: (request: SubprocessRequest, handle: SubprocessHandle) => Promise<void>;
16
+ /**
17
+ * Invoked for `kind: 'step'` requests that do not already carry a
18
+ * `_localExecutor` closure. Callers swap this out to simulate step
19
+ * execution in tests; the default harness-bound adapter provides an
20
+ * implementation that invokes the in-process execute pipeline.
21
+ */
22
+ stepRunner?: (request: StepSubprocessRequest, handle: SubprocessHandle) => Promise<unknown>;
23
+ /**
24
+ * Optional synchronous metadata injector. Called during `spawn()` before
25
+ * the handle is returned, its result is merged on top of the adapter's
26
+ * intrinsic metadata (runtime, command/args/cwd, etc.). Use this to stamp
27
+ * pid / pidStarttime / test-only fields without having to wrap the adapter.
28
+ *
29
+ * Keys returned here win over the adapter's defaults — tests can override
30
+ * `runtime` if they need to simulate a different adapter variant.
31
+ */
32
+ metadataInjector?: (request: SubprocessRequest) => Partial<SubprocessHandleMetadata>;
33
+ /**
34
+ * Optional `StorageAdapter` that gives the in-memory adapter a durable
35
+ * handle manifest. When supplied, `spawn()` persists a manifest per
36
+ * handle and `reattach(handleId)` / `listLive()` consult it across adapter
37
+ * restarts. When absent the adapter is purely ephemeral — matching the
38
+ * test-double semantics established in Phase A.
39
+ *
40
+ * For in-memory the "reattached" handle is an idempotent re-run of the
41
+ * step from the persisted `serializedInput`. We're honest about that in
42
+ * the manifest (`reattachMode: 'replay'`) — this is the sanest in-memory
43
+ * semantic because there is no long-lived process to bind back to.
44
+ */
45
+ storage?: StorageAdapter;
46
+ }
47
+ export interface CompleteProcessRunParams {
48
+ request: ProcessSubprocessRequest;
49
+ handle: SubprocessHandle;
50
+ run: CreateInMemorySubprocessAdapterOptions['run'];
51
+ handles: Map<string, SubprocessHandle>;
52
+ active: Set<string>;
53
+ save: (handle: SubprocessHandle) => Promise<SubprocessHandle>;
54
+ /** Invoked on terminal transitions so the durable handle manifest
55
+ * does not linger after the step is done — otherwise `listLive()`
56
+ * would return phantom handles for every step ever completed
57
+ * against this storage. No-op when no storage was configured. */
58
+ clearIfDurable: (handleId: string) => Promise<void>;
59
+ }
60
+ export interface CompleteStepRunParams {
61
+ request: StepSubprocessRequest;
62
+ handle: SubprocessHandle;
63
+ stepRunner: CreateInMemorySubprocessAdapterOptions['stepRunner'];
64
+ handles: Map<string, SubprocessHandle>;
65
+ active: Set<string>;
66
+ save: (handle: SubprocessHandle) => Promise<SubprocessHandle>;
67
+ /** See `CompleteProcessRunParams.clearIfDurable`. */
68
+ clearIfDurable: (handleId: string) => Promise<void>;
69
+ }
70
+ /**
71
+ * Persistable manifest for an in-memory handle. Written to the
72
+ * StorageAdapter on spawn and cleared on stop — a restart can list live
73
+ * handles via this prefix and re-run the step from `serializedInput`.
74
+ *
75
+ * Scoped to `step`-kind requests because `process`-kind handles carry no
76
+ * replayable body (they represent already-launched OS processes whose state
77
+ * is not ours to recreate).
78
+ */
79
+ export interface InMemoryStepManifest {
80
+ kind: 'step';
81
+ handleId: string;
82
+ stepId: string;
83
+ executionId: string;
84
+ serializedInput: unknown;
85
+ overrides: StepSubprocessRequest['overrides'];
86
+ startedAt: string;
87
+ reattachMode: 'replay';
88
+ }
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //#endregion
@@ -1,5 +1,13 @@
1
- import type { SubprocessAdapter, SubprocessHandle, SubprocessRequest } from '../types/subprocess-adapter';
2
- export interface CreateInMemorySubprocessAdapterOptions {
3
- run?: (request: SubprocessRequest, handle: SubprocessHandle) => Promise<void>;
4
- }
1
+ import type { SubprocessAdapter } from '../types/subprocess-adapter.js';
2
+ import type { CreateInMemorySubprocessAdapterOptions } from './in-memory-subprocess/types.js';
3
+ export type { CreateInMemorySubprocessAdapterOptions } from './in-memory-subprocess/types.js';
4
+ /**
5
+ * @public
6
+ * In-memory `SubprocessAdapter` for tests and non-durable in-process
7
+ * step execution. When an optional `StorageAdapter` is supplied the
8
+ * adapter persists per-handle manifests so `reattach()` / `listLive()`
9
+ * work across adapter instances — the reattached handle is an idempotent
10
+ * re-run of the step from the persisted `serializedInput`, not a bind to
11
+ * a live process (there is none to bind to for in-memory).
12
+ */
5
13
  export declare function createInMemorySubprocessAdapter(options?: CreateInMemorySubprocessAdapterOptions): SubprocessAdapter;
@@ -1,68 +1,135 @@
1
- function nowIso() {
2
- return new Date().toISOString();
3
- }
4
- async function completeInMemoryRun({ request, handle, run, handles, active, save, }) {
5
- try {
6
- await run?.(request, handle);
7
- const latest = handles.get(handle.id);
8
- if (!latest || latest.status !== 'running') {
1
+ import { clearManifest, listManifests, loadManifest, persistStepManifest, } from './in-memory-subprocess/manifest-persistence.js';
2
+ import { buildProcessMetadata, buildStepMetadata, isProcessRequest, isStepRequest, nowIso, } from './in-memory-subprocess/metadata.js';
3
+ import { completeProcessRun, completeStepRun } from './in-memory-subprocess/step-completion.js';
4
+ //#region Factory
5
+ /**
6
+ * @public
7
+ * In-memory `SubprocessAdapter` for tests and non-durable in-process
8
+ * step execution. When an optional `StorageAdapter` is supplied the
9
+ * adapter persists per-handle manifests so `reattach()` / `listLive()`
10
+ * work across adapter instances — the reattached handle is an idempotent
11
+ * re-run of the step from the persisted `serializedInput`, not a bind to
12
+ * a live process (there is none to bind to for in-memory).
13
+ */
14
+ export function createInMemorySubprocessAdapter(options = {}) {
15
+ const handles = new Map();
16
+ const active = new Set();
17
+ const storage = options.storage;
18
+ async function save(handle) {
19
+ handles.set(handle.id, handle);
20
+ return handle;
21
+ }
22
+ /**
23
+ * Persist a step manifest after spawn so `reattach` and `listLive` can
24
+ * find this handle again on a subsequent adapter instance. Errors are
25
+ * logged rather than propagated — a failed manifest write must not break
26
+ * otherwise-successful step dispatch.
27
+ */
28
+ async function persistIfDurable(request, handle) {
29
+ if (!storage) {
9
30
  return;
10
31
  }
11
- active.delete(handle.id);
12
- await save({
13
- ...latest,
14
- status: 'completed',
15
- updatedAt: nowIso(),
32
+ try {
33
+ await persistStepManifest(storage, request, handle);
34
+ }
35
+ catch (err) {
36
+ console.warn(`createInMemorySubprocessAdapter: failed to persist manifest for "${handle.id}":`, err);
37
+ }
38
+ }
39
+ async function clearIfDurable(handleId) {
40
+ if (!storage) {
41
+ return;
42
+ }
43
+ try {
44
+ await clearManifest(storage, handleId);
45
+ }
46
+ catch (err) {
47
+ console.warn(`createInMemorySubprocessAdapter: failed to clear manifest for "${handleId}":`, err);
48
+ }
49
+ }
50
+ function injectMetadata(request, intrinsic) {
51
+ if (!options.metadataInjector) {
52
+ return intrinsic;
53
+ }
54
+ return {
55
+ ...intrinsic,
56
+ ...options.metadataInjector(request),
57
+ };
58
+ }
59
+ async function spawnStepHandle(request) {
60
+ const now = nowIso();
61
+ const handle = {
62
+ id: `subprocess-${crypto.randomUUID()}`,
63
+ status: 'running',
64
+ startedAt: now,
65
+ updatedAt: now,
66
+ metadata: injectMetadata(request, buildStepMetadata(request)),
67
+ };
68
+ active.add(handle.id);
69
+ await save(handle);
70
+ await persistIfDurable(request, handle);
71
+ void completeStepRun({
72
+ request,
73
+ handle,
74
+ stepRunner: options.stepRunner,
75
+ handles,
76
+ active,
77
+ save,
78
+ clearIfDurable,
16
79
  });
80
+ return handle;
17
81
  }
18
- catch (err) {
19
- active.delete(handle.id);
20
- const latest = handles.get(handle.id) ?? handle;
21
- await save({
22
- ...latest,
23
- status: 'failed',
82
+ async function spawnProcessHandle(request) {
83
+ const now = nowIso();
84
+ const handle = {
85
+ id: `subprocess-${crypto.randomUUID()}`,
86
+ status: 'running',
87
+ startedAt: now,
88
+ updatedAt: now,
89
+ metadata: injectMetadata(request, buildProcessMetadata(request)),
90
+ };
91
+ active.add(handle.id);
92
+ await save(handle);
93
+ void completeProcessRun({
94
+ request,
95
+ handle,
96
+ run: options.run,
97
+ handles,
98
+ active,
99
+ save,
100
+ clearIfDurable,
101
+ });
102
+ return handle;
103
+ }
104
+ /**
105
+ * Build a hydrated handle from a persisted manifest. Status is `running`
106
+ * so consumers see the handle as live; a subsequent `reattach` call
107
+ * re-dispatches the step via the supplied `stepRunner`.
108
+ */
109
+ function handleFromManifest(manifest) {
110
+ return {
111
+ id: manifest.handleId,
112
+ status: 'running',
113
+ startedAt: manifest.startedAt,
24
114
  updatedAt: nowIso(),
25
115
  metadata: {
26
- ...(latest.metadata ?? {}),
27
- error: err instanceof Error ? err.message : String(err),
116
+ runtime: 'in-memory',
117
+ kind: 'step',
118
+ stepId: manifest.stepId,
119
+ executionId: manifest.executionId,
120
+ reattachMode: manifest.reattachMode,
28
121
  },
29
- });
30
- }
31
- }
32
- export function createInMemorySubprocessAdapter(options = {}) {
33
- const handles = new Map();
34
- const active = new Set();
35
- async function save(handle) {
36
- handles.set(handle.id, handle);
37
- return handle;
122
+ };
38
123
  }
39
124
  return {
40
125
  async spawn(request) {
41
- const now = nowIso();
42
- const handle = {
43
- id: `subprocess-${crypto.randomUUID()}`,
44
- status: 'running',
45
- startedAt: now,
46
- updatedAt: now,
47
- metadata: {
48
- ...(request.metadata ?? {}),
49
- runtime: 'in-memory',
50
- command: request.command,
51
- args: request.args ?? [],
52
- cwd: request.cwd,
53
- },
54
- };
55
- active.add(handle.id);
56
- await save(handle);
57
- void completeInMemoryRun({
58
- request,
59
- handle,
60
- run: options.run,
61
- handles,
62
- active,
63
- save,
64
- });
65
- return handle;
126
+ if (isStepRequest(request)) {
127
+ return spawnStepHandle(request);
128
+ }
129
+ if (isProcessRequest(request)) {
130
+ return spawnProcessHandle(request);
131
+ }
132
+ throw new Error('Unsupported subprocess request variant.');
66
133
  },
67
134
  async get(handleId) {
68
135
  return handles.get(handleId) ?? null;
@@ -76,6 +143,7 @@ export function createInMemorySubprocessAdapter(options = {}) {
76
143
  };
77
144
  }
78
145
  active.delete(handleId);
146
+ await clearIfDurable(handleId);
79
147
  const next = await save({
80
148
  ...handle,
81
149
  status: 'stopped',
@@ -122,5 +190,46 @@ export function createInMemorySubprocessAdapter(options = {}) {
122
190
  async isAlive(handle) {
123
191
  return active.has(handle.id);
124
192
  },
193
+ /**
194
+ * Idempotent re-run of a persisted step, not a "resume" — for in-memory
195
+ * there is no long-lived process to bind back to. The adapter's
196
+ * `reattachMode` on the manifest reflects this honestly. When no
197
+ * storage was configured we fall back to the ephemeral semantics
198
+ * established in Phase A (returning `null`).
199
+ */
200
+ async reattach(handleId) {
201
+ if (!storage) {
202
+ return null;
203
+ }
204
+ const manifest = await loadManifest(storage, handleId);
205
+ if (!manifest) {
206
+ return null;
207
+ }
208
+ const handle = handleFromManifest(manifest);
209
+ active.add(handle.id);
210
+ await save(handle);
211
+ return handle;
212
+ },
213
+ async listLive() {
214
+ const live = new Map();
215
+ for (const id of active) {
216
+ const handle = handles.get(id);
217
+ if (handle) {
218
+ live.set(handle.id, handle);
219
+ }
220
+ }
221
+ if (storage) {
222
+ const manifests = await listManifests(storage);
223
+ for (const manifest of manifests) {
224
+ if (!live.has(manifest.handleId)) {
225
+ live.set(manifest.handleId, handleFromManifest(manifest));
226
+ }
227
+ }
228
+ }
229
+ return [
230
+ ...live.values(),
231
+ ];
232
+ },
125
233
  };
126
234
  }
235
+ //#endregion