@noetic-tools/core 5.4.0 → 5.4.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 (35) hide show
  1. package/dist/builders/acp-agent-tool.d.ts +67 -0
  2. package/dist/builders/acp-agent-tool.js +87 -0
  3. package/dist/builders/dynamic-workflow.js +2 -2
  4. package/dist/builders/step-builders.d.ts +42 -42
  5. package/dist/builders/step-builders.js +38 -62
  6. package/dist/builders/workflow-hydrator.d.ts +8 -4
  7. package/dist/builders/workflow-hydrator.js +16 -24
  8. package/dist/builders/workflow-step.d.ts +3 -3
  9. package/dist/builders/workflow-step.js +1 -1
  10. package/dist/harness/agent-harness.d.ts +36 -7
  11. package/dist/harness/agent-harness.js +49 -6
  12. package/dist/harness/deps/types.d.ts +1 -1
  13. package/dist/index.d.ts +8 -4
  14. package/dist/index.js +3 -3
  15. package/dist/interpreter/acp-events.d.ts +67 -0
  16. package/dist/interpreter/acp-events.js +256 -0
  17. package/dist/interpreter/acp-history.d.ts +22 -0
  18. package/dist/interpreter/acp-history.js +58 -0
  19. package/dist/interpreter/collect-tools.js +3 -6
  20. package/dist/interpreter/execute-acp-agent.d.ts +10 -0
  21. package/dist/interpreter/execute-acp-agent.js +505 -0
  22. package/dist/interpreter/execute.js +4 -11
  23. package/dist/interpreter/structured-output.d.ts +1 -1
  24. package/dist/interpreter/structured-output.js +1 -1
  25. package/dist/portable/builders.d.ts +2 -0
  26. package/dist/portable/builders.js +3 -1
  27. package/dist/runtime/acp-session-store.d.ts +75 -0
  28. package/dist/runtime/acp-session-store.js +0 -0
  29. package/dist/schemas/workflow.d.ts +1 -1
  30. package/package.json +3 -3
  31. package/schema/noetic-workflow.schema.json +215 -249
  32. package/dist/interpreter/execute-sub-harness.d.ts +0 -8
  33. package/dist/interpreter/execute-sub-harness.js +0 -222
  34. package/dist/interpreter/sub-harness-events.d.ts +0 -48
  35. package/dist/interpreter/sub-harness-events.js +0 -208
@@ -0,0 +1,67 @@
1
+ /**
2
+ * Exposes an ACP coding agent as a `Tool`, so a `callModel` step can delegate
3
+ * work to it by calling it.
4
+ *
5
+ * `step.acpAgent` puts the agent in the step tree, where the *author* decides
6
+ * when it runs. This puts it in the model's hands instead: the model decides
7
+ * that a task wants a coding agent and calls the tool, the same way it would
8
+ * call any other. Everything else — permissions, session policy, the event
9
+ * bridge, usage accounting — is identical, because the tool runs the very same
10
+ * step underneath.
11
+ */
12
+ import type { AcpAgent, AcpClientCapabilityConfig, AcpMcpServer, AcpPermissionHandler, AcpPermissionPolicy, AcpSessionPolicy, Tool } from '@noetic-tools/types';
13
+ import { z } from 'zod';
14
+ /** @public Options for {@link acpAgentTool}. */
15
+ export interface AcpAgentToolOptions {
16
+ /** The ACP agent adapter to delegate to, e.g. `claudeCode()`. */
17
+ agent: AcpAgent;
18
+ /**
19
+ * Tool name the model calls. Defaults to `delegate_to_<agentId>` with
20
+ * non-identifier characters replaced, e.g. `delegate_to_claude_code`.
21
+ */
22
+ name?: string;
23
+ /**
24
+ * What the model is told the tool does. Defaults to a generic description —
25
+ * override it to steer *when* the model delegates, which matters far more
26
+ * than the default wording.
27
+ */
28
+ description?: string;
29
+ /** Working directory for the agent's session. Defaults to the runtime cwd. */
30
+ cwd?: string;
31
+ /** Session mode to switch to before each delegated turn. */
32
+ mode?: string;
33
+ /** Model to select before each delegated turn. */
34
+ model?: string;
35
+ /** MCP servers to expose to the agent. */
36
+ mcpServers?: ReadonlyArray<AcpMcpServer>;
37
+ /** Declarative answer to the agent's permission requests. Defaults to denying. */
38
+ permissions?: AcpPermissionPolicy;
39
+ /** Async resolver consulted when policy and steering both abstain. */
40
+ onPermissionRequest?: AcpPermissionHandler;
41
+ /** Which client capabilities to advertise to the agent. */
42
+ clientCapabilities?: AcpClientCapabilityConfig;
43
+ /**
44
+ * Session policy for the delegated turns. Give it a `reuse` key with
45
+ * `keepAlive: 'run'` (or `'harness'`) to let the model hold a *conversation*
46
+ * with the agent across several tool calls instead of starting cold each
47
+ * time — usually what you want, since the second call almost always builds
48
+ * on the first.
49
+ */
50
+ session?: AcpSessionPolicy;
51
+ }
52
+ declare const AcpAgentToolInputSchema: z.ZodObject<{
53
+ prompt: z.ZodString;
54
+ }, z.core.$strip>;
55
+ declare const AcpAgentToolOutputSchema: z.ZodObject<{
56
+ text: z.ZodString;
57
+ }, z.core.$strip>;
58
+ /**
59
+ * Build a {@link Tool} that hands a prompt to an ACP coding agent.
60
+ *
61
+ * @public
62
+ * @param opts.agent - The agent adapter, e.g. `claudeCode()` from `@noetic-tools/acp`.
63
+ * @returns A `Tool` accepting `{ prompt }` and returning `{ text }`.
64
+ * @throws `NoeticConfigError` `MISSING_ACP_AGENT` when no adapter is given.
65
+ */
66
+ export declare function acpAgentTool(opts: AcpAgentToolOptions): Tool<typeof AcpAgentToolInputSchema, typeof AcpAgentToolOutputSchema>;
67
+ export {};
@@ -0,0 +1,87 @@
1
+ /**
2
+ * Exposes an ACP coding agent as a `Tool`, so a `callModel` step can delegate
3
+ * work to it by calling it.
4
+ *
5
+ * `step.acpAgent` puts the agent in the step tree, where the *author* decides
6
+ * when it runs. This puts it in the model's hands instead: the model decides
7
+ * that a task wants a coding agent and calls the tool, the same way it would
8
+ * call any other. Everything else — permissions, session policy, the event
9
+ * bridge, usage accounting — is identical, because the tool runs the very same
10
+ * step underneath.
11
+ */
12
+ import { NoeticConfigError } from '@noetic-tools/types';
13
+ import { z } from 'zod';
14
+ import { step } from './step-builders.js';
15
+ import { tool } from './tool-builder.js';
16
+ const AcpAgentToolInputSchema = z.object({
17
+ prompt: z.string().min(1).describe('What the coding agent should do, in plain language.'),
18
+ });
19
+ const AcpAgentToolOutputSchema = z.object({
20
+ text: z.string().describe("The coding agent's final response."),
21
+ });
22
+ //#endregion
23
+ //#region Helpers
24
+ /** `claude-code` → `delegate_to_claude_code`. */
25
+ function defaultToolName(agentId) {
26
+ return `delegate_to_${agentId.replace(/[^a-zA-Z0-9_]/g, '_')}`;
27
+ }
28
+ function defaultDescription(agentId) {
29
+ return [
30
+ `Delegate a coding task to the ${agentId} agent, which works directly in the workspace.`,
31
+ 'Give it a complete, self-contained instruction — it does not see this conversation.',
32
+ 'Prefer it for multi-file edits, running builds or tests, and exploring an unfamiliar codebase.',
33
+ ].join(' ');
34
+ }
35
+ //#endregion
36
+ //#region Public API
37
+ /**
38
+ * Build a {@link Tool} that hands a prompt to an ACP coding agent.
39
+ *
40
+ * @public
41
+ * @param opts.agent - The agent adapter, e.g. `claudeCode()` from `@noetic-tools/acp`.
42
+ * @returns A `Tool` accepting `{ prompt }` and returning `{ text }`.
43
+ * @throws `NoeticConfigError` `MISSING_ACP_AGENT` when no adapter is given.
44
+ */
45
+ export function acpAgentTool(opts) {
46
+ if (!opts.agent) {
47
+ throw new NoeticConfigError({
48
+ code: 'MISSING_ACP_AGENT',
49
+ message: 'acpAgentTool() requires an agent adapter.',
50
+ hint: 'Pass an agent factory result, e.g. agent: claudeCode() from @noetic-tools/acp.',
51
+ });
52
+ }
53
+ const name = opts.name ?? defaultToolName(opts.agent.agentId);
54
+ // Built once, not per call: `step.acpAgent` registers itself in the step
55
+ // registry, so constructing one per invocation would fill the registry with
56
+ // duplicate ids. An empty `prompt` means "use the step's runtime input",
57
+ // which is exactly the tool's argument.
58
+ const delegated = step.acpAgent({
59
+ id: `${name}:step`,
60
+ agent: opts.agent,
61
+ prompt: '',
62
+ cwd: opts.cwd,
63
+ mode: opts.mode,
64
+ model: opts.model,
65
+ mcpServers: opts.mcpServers,
66
+ permissions: opts.permissions,
67
+ onPermissionRequest: opts.onPermissionRequest,
68
+ clientCapabilities: opts.clientCapabilities,
69
+ session: opts.session,
70
+ });
71
+ return tool({
72
+ name,
73
+ description: opts.description ?? defaultDescription(opts.agent.agentId),
74
+ input: AcpAgentToolInputSchema,
75
+ output: AcpAgentToolOutputSchema,
76
+ async execute(args, toolCtx) {
77
+ // Running through the harness on the tool's own context means the
78
+ // delegated turn lands in the same item log, usage totals, and event
79
+ // stream as any other step — it is a real step, not a side channel.
80
+ const text = await toolCtx.harness.run(delegated, args.prompt, toolCtx.ctx);
81
+ return {
82
+ text,
83
+ };
84
+ },
85
+ });
86
+ }
87
+ //#endregion
@@ -32,11 +32,11 @@ A WorkflowNode is one of:
32
32
  - { "kind": "withContext", "id": "<unique>", "child": <WorkflowNode>, "layers": ["<layer-name>", ...] } (run the child with named context layers; only emit if the task names layers that exist, since this planner provides no layer registry and unknown names resolve to NO layers)
33
33
  - { "kind": "schedule", "id": "<unique>", "step": <WorkflowNode>, "interval": <ms>, "onError": "continue"|"fail" } (re-runs the step forever on an interval and never returns — only emit for an explicitly daemon-style task)
34
34
  - { "kind": "subflow", "id": "<unique>", "document": { "version": 1, "root": <WorkflowNode> } } (an inline sub-workflow run as one step; only emit the inline form — named refs require a registry this planner does not provide)
35
- - { "kind": "claude-code"|"codex"|"opencode"|"pi", "id": "<unique>", "prompt": "<turn prompt>", "settings": { "model": "<optional>", "permissionMode": "<optional>" } }
35
+ - { "kind": "acp-agent", "id": "<unique>", "agent": "<registered agent id, e.g. claude-code>", "prompt": "<turn prompt>", "mode": "<optional session mode>", "model": "<optional>" }
36
36
 
37
37
  Every entry of a callModel node's "tools" is an OBJECT, never a bare string. Use { "type": "<tool-name>" } to let the model call one of the tools listed below. Two provider-executed tools are also available without being listed: { "type": "openrouter:web_search" } and { "type": "openrouter:web_fetch" }, each accepting an optional "parameters" object.
38
38
 
39
- SubHarness nodes (claude-code, codex, opencode, pi) delegate a turn to an external coding agent; only emit one if a matching harness adapter is registered for the workflow.
39
+ An acp-agent node delegates a turn to an external coding agent over the Agent Client Protocol; only emit one if a matching agent adapter is registered for the workflow.
40
40
 
41
41
  An UntilPredicate (the "until" field of a loop) is one of:
42
42
  { "kind": "maxSteps", "n": <positive int> }, { "kind": "maxCost", "usd": <positive number> }, { "kind": "maxDuration", "duration": <ms> }, { "kind": "noToolCalls" }, { "kind": "never" } (never stops — pair with "maxIterations"), { "kind": "outputContains", "marker": "<text>" }, { "kind": "outputEquals", "sentinel": "<text>" }, { "kind": "converged", "threshold": <optional 0-1> }, { "kind": "any", "predicates": [<UntilPredicate>, ...] }, { "kind": "all", "predicates": [<UntilPredicate>, ...] }.
@@ -1,5 +1,5 @@
1
1
  import type { ContextData } from '@noetic-tools/context';
2
- import type { Context, Lazy, ModelParams, OutputCodec, RetryPolicy, ServerToolSpec, StandardSchemaV1, StepCallModel, StepInvokeTool, StepRunCode, StepSubHarness, SubHarness, SubHarnessSessionPolicy, SubHarnessSettings, SubprocessAdapter, Tool } from '@noetic-tools/types';
2
+ import type { AcpAgent, AcpClientCapabilityConfig, AcpContentBlock, AcpMcpServer, AcpPermissionHandler, AcpPermissionPolicy, AcpSessionPolicy, Context, Lazy, ModelParams, OutputCodec, RetryPolicy, ServerToolSpec, StandardSchemaV1, StepAcpAgent, StepCallModel, StepInvokeTool, StepRunCode, SubprocessAdapter, Tool } from '@noetic-tools/types';
3
3
  export interface RunCodeOpts<TContext, I, O> {
4
4
  id: string;
5
5
  execute: (input: I, ctx: Context<TContext>) => Promise<O>;
@@ -38,20 +38,36 @@ export interface InvokeToolOpts<I, O> {
38
38
  tool: Tool<StandardSchemaV1<unknown, I>, StandardSchemaV1<unknown, O>>;
39
39
  args?: Partial<I>;
40
40
  }
41
- export interface StepSubHarnessOpts<TContext, O> {
41
+ export interface StepAcpAgentOpts<TContext, O> {
42
42
  id: string;
43
- /** The harness adapter created by a `@noetic-tools/sub-harness-*` factory. Eager or `(ctx) => SubHarness`. */
44
- harness: Lazy<SubHarness, TContext>;
45
- /** Turn prompt. Eager string or `(ctx) => string` getter. */
46
- prompt: Lazy<string, TContext>;
47
- /** Shared harness settings (model, permission mode, …). */
48
- settings?: SubHarnessSettings;
49
- /** System instructions applied on the first message of a fresh session. */
50
- instructions?: Lazy<string | undefined, TContext>;
43
+ /** An ACP agent adapter, e.g. `claudeCode()` from `@noetic-tools/acp`. Eager or `(ctx) => AcpAgent`. */
44
+ agent: Lazy<AcpAgent, TContext>;
45
+ /** Turn prompt as plain text. Eager string or `(ctx) => string` getter. */
46
+ prompt?: Lazy<string, TContext>;
47
+ /**
48
+ * Full ACP prompt content — images, audio, resource links, embedded context.
49
+ * Appended after `prompt`. Rejected before the turn is sent when the agent
50
+ * did not advertise the matching prompt capability.
51
+ */
52
+ content?: Lazy<ReadonlyArray<AcpContentBlock> | undefined, TContext>;
53
+ /** MCP servers to expose to the agent for this session. */
54
+ mcpServers?: Lazy<ReadonlyArray<AcpMcpServer> | undefined, TContext>;
55
+ /** Working directory for the session. Defaults to `ctx.cwdState.cwd`. */
56
+ cwd?: Lazy<string | undefined, TContext>;
57
+ /** Session mode to switch to before prompting (e.g. `'plan'`). */
58
+ mode?: Lazy<string | undefined, TContext>;
59
+ /** Model to select before prompting, for agents that expose model selection. */
60
+ model?: Lazy<string | undefined, TContext>;
61
+ /** Declarative answer to the agent's permission requests. Defaults to denying. */
62
+ permissions?: AcpPermissionPolicy;
63
+ /** Async resolver consulted when the policy and steering both abstain. */
64
+ onPermissionRequest?: AcpPermissionHandler;
65
+ /** Which client capabilities to advertise to the agent. */
66
+ clientCapabilities?: AcpClientCapabilityConfig;
51
67
  /** Optional Standard Schema; when set the assistant text is JSON-parsed and validated. */
52
68
  output?: StandardSchemaV1<unknown, O>;
53
69
  /** Session reuse + teardown policy across steps. */
54
- session?: SubHarnessSessionPolicy;
70
+ session?: AcpSessionPolicy;
55
71
  emit?: boolean | ((eventType: string, data: Record<string, unknown>) => boolean);
56
72
  }
57
73
  /**
@@ -100,45 +116,29 @@ export declare function callModel<TContext = ContextData, I = unknown, O = unkno
100
116
  */
101
117
  export declare function invokeTool<TContext = ContextData, I = unknown, O = unknown>(opts: InvokeToolOpts<I, O>): StepInvokeTool<TContext, I, O>;
102
118
  /**
103
- * The sub-harness step builder namespace. Unlike the flattened base builders
104
- * (`runCode`, `callModel`, `invokeTool`), the coding-agent harness builders
105
- * stay grouped under `step` — their `step.<harness>()` API is unchanged.
119
+ * The ACP step builder namespace. Unlike the flattened base builders
120
+ * (`runCode`, `callModel`, `invokeTool`), the coding-agent builder stays
121
+ * grouped under `step`.
106
122
  *
107
123
  * @public
108
124
  */
109
125
  export declare const step: {
110
126
  /**
111
- * Creates a step that delegates a turn to the Claude Code harness.
127
+ * Creates a step that delegates a turn to an external coding agent over the
128
+ * Agent Client Protocol.
129
+ *
130
+ * One builder covers every ACP-speaking agent: the agent itself is supplied
131
+ * as an adapter, so adding an agent never changes this API or the published
132
+ * workflow schema.
112
133
  *
113
134
  * @public
114
135
  * @param opts.id - Unique step identifier.
115
- * @param opts.harness - A `claudeCode(...)` adapter from `@noetic-tools/sub-harness-claude-code`.
136
+ * @param opts.agent - An agent adapter, e.g. `claudeCode()` from `@noetic-tools/acp`.
116
137
  * @param opts.prompt - The turn prompt; eager string or `(ctx) => string` getter.
117
- * @param opts.settings - Shared harness settings (model, permission mode, …).
118
- * @param opts.output - Optional Zod schema enabling structured output parsing.
119
- * @returns A `StepSubHarness` of kind `claude-code`, auto-registered in the step registry.
120
- * @throws `NoeticConfigError` `EMPTY_STEP_ID` / `MISSING_SUB_HARNESS` / `SUB_HARNESS_KIND_MISMATCH`.
121
- */
122
- claudeCode<TContext = ContextData, I = unknown, O = unknown>(opts: StepSubHarnessOpts<TContext, O>): StepSubHarness<TContext, I, O>;
123
- /**
124
- * Creates a step that delegates a turn to the Codex harness.
125
- * @public
126
- * @param opts - See {@link step.claudeCode}; `opts.harness` is a `codex(...)` adapter.
127
- * @returns A `StepSubHarness` of kind `codex`.
128
- */
129
- codex<TContext = ContextData, I = unknown, O = unknown>(opts: StepSubHarnessOpts<TContext, O>): StepSubHarness<TContext, I, O>;
130
- /**
131
- * Creates a step that delegates a turn to the opencode harness.
132
- * @public
133
- * @param opts - See {@link step.claudeCode}; `opts.harness` is an `opencode(...)` adapter.
134
- * @returns A `StepSubHarness` of kind `opencode`.
135
- */
136
- opencode<TContext = ContextData, I = unknown, O = unknown>(opts: StepSubHarnessOpts<TContext, O>): StepSubHarness<TContext, I, O>;
137
- /**
138
- * Creates a step that delegates a turn to the pi harness.
139
- * @public
140
- * @param opts - See {@link step.claudeCode}; `opts.harness` is a `pi(...)` adapter.
141
- * @returns A `StepSubHarness` of kind `pi`.
138
+ * @param opts.permissions - Declarative policy answering the agent's permission requests.
139
+ * @param opts.output - Optional Standard Schema enabling structured output parsing.
140
+ * @returns A `StepAcpAgent`, auto-registered in the step registry.
141
+ * @throws `NoeticConfigError` `EMPTY_STEP_ID` / `MISSING_ACP_AGENT` / `MISSING_PROMPT`.
142
142
  */
143
- pi<TContext = ContextData, I = unknown, O = unknown>(opts: StepSubHarnessOpts<TContext, O>): StepSubHarness<TContext, I, O>;
143
+ acpAgent<TContext = ContextData, I = unknown, O = unknown>(opts: StepAcpAgentOpts<TContext, O>): StepAcpAgent<TContext, I, O>;
144
144
  };
@@ -1,43 +1,35 @@
1
1
  import { NoeticConfigError } from '@noetic-tools/types';
2
2
  import { getDefaultRegistrar } from '../types/step-registrar.js';
3
3
  //#endregion
4
- //#region SubHarness builder helper
4
+ //#region ACP agent builder helper
5
5
  /**
6
- * Shared construction for every harness step kind. Each `step.<kind>()` is a
7
- * thin wrapper so the kinds stay individually typed while the validation and
8
- * registration live in one place.
6
+ * Validation shared by `step.acpAgent`. Kept separate from the builder so the
7
+ * checks stay readable and the builder stays a one-liner.
9
8
  */
10
- function buildSubHarnessStep(kind, builderName, opts) {
9
+ function validateAcpAgentOpts(opts) {
11
10
  if (!opts.id || opts.id.trim() === '') {
12
11
  throw new NoeticConfigError({
13
12
  code: 'EMPTY_STEP_ID',
14
- message: `${builderName}() requires a non-empty id.`,
15
- hint: `Pass a unique string as the id field, e.g. ${builderName}({ id: "review", ... }).`,
13
+ message: 'step.acpAgent() requires a non-empty id.',
14
+ hint: 'Pass a unique string as the id field, e.g. step.acpAgent({ id: "review", ... }).',
16
15
  });
17
16
  }
18
- if (!opts.harness) {
17
+ if (!opts.agent) {
19
18
  throw new NoeticConfigError({
20
- code: 'MISSING_SUB_HARNESS',
21
- message: `${builderName}() requires a harness adapter.`,
22
- hint: `Pass a harness factory result, e.g. harness: ${builderName.replace('step.', '')}({ model }).`,
19
+ code: 'MISSING_ACP_AGENT',
20
+ message: 'step.acpAgent() requires an agent adapter.',
21
+ hint: 'Pass an agent factory result, e.g. agent: claudeCode() from @noetic-tools/acp.',
23
22
  });
24
23
  }
25
- // Eager adapters are validated now; function-form adapters are validated
26
- // post-resolution in executeSubHarness so the same SUB_HARNESS_KIND_MISMATCH
27
- // error surfaces whether the caller passes an adapter or a getter.
28
- if (typeof opts.harness !== 'function' && opts.harness.harnessId !== kind) {
24
+ // Presence, not emptiness: an empty `prompt` is a valid way to say "use the
25
+ // step's runtime input as the prompt", which the handler supports.
26
+ if (opts.prompt === undefined && opts.content === undefined) {
29
27
  throw new NoeticConfigError({
30
- code: 'SUB_HARNESS_KIND_MISMATCH',
31
- message: `${builderName}() was given a '${opts.harness.harnessId}' harness.`,
32
- hint: `Use the matching builder, e.g. step.${opts.harness.harnessId}({ ... }).`,
28
+ code: 'MISSING_PROMPT',
29
+ message: 'step.acpAgent() requires a prompt or content blocks.',
30
+ hint: 'Pass `prompt` for plain text, or `content` for image/audio/resource blocks.',
33
31
  });
34
32
  }
35
- const built = {
36
- kind,
37
- ...opts,
38
- };
39
- getDefaultRegistrar().register(built);
40
- return built;
41
33
  }
42
34
  //#endregion
43
35
  //#region Builders
@@ -153,54 +145,38 @@ export function invokeTool(opts) {
153
145
  return built;
154
146
  }
155
147
  /**
156
- * The sub-harness step builder namespace. Unlike the flattened base builders
157
- * (`runCode`, `callModel`, `invokeTool`), the coding-agent harness builders
158
- * stay grouped under `step` — their `step.<harness>()` API is unchanged.
148
+ * The ACP step builder namespace. Unlike the flattened base builders
149
+ * (`runCode`, `callModel`, `invokeTool`), the coding-agent builder stays
150
+ * grouped under `step`.
159
151
  *
160
152
  * @public
161
153
  */
162
154
  export const step = {
163
155
  /**
164
- * Creates a step that delegates a turn to the Claude Code harness.
156
+ * Creates a step that delegates a turn to an external coding agent over the
157
+ * Agent Client Protocol.
158
+ *
159
+ * One builder covers every ACP-speaking agent: the agent itself is supplied
160
+ * as an adapter, so adding an agent never changes this API or the published
161
+ * workflow schema.
165
162
  *
166
163
  * @public
167
164
  * @param opts.id - Unique step identifier.
168
- * @param opts.harness - A `claudeCode(...)` adapter from `@noetic-tools/sub-harness-claude-code`.
165
+ * @param opts.agent - An agent adapter, e.g. `claudeCode()` from `@noetic-tools/acp`.
169
166
  * @param opts.prompt - The turn prompt; eager string or `(ctx) => string` getter.
170
- * @param opts.settings - Shared harness settings (model, permission mode, …).
171
- * @param opts.output - Optional Zod schema enabling structured output parsing.
172
- * @returns A `StepSubHarness` of kind `claude-code`, auto-registered in the step registry.
173
- * @throws `NoeticConfigError` `EMPTY_STEP_ID` / `MISSING_SUB_HARNESS` / `SUB_HARNESS_KIND_MISMATCH`.
174
- */
175
- claudeCode(opts) {
176
- return buildSubHarnessStep('claude-code', 'step.claudeCode', opts);
177
- },
178
- /**
179
- * Creates a step that delegates a turn to the Codex harness.
180
- * @public
181
- * @param opts - See {@link step.claudeCode}; `opts.harness` is a `codex(...)` adapter.
182
- * @returns A `StepSubHarness` of kind `codex`.
183
- */
184
- codex(opts) {
185
- return buildSubHarnessStep('codex', 'step.codex', opts);
186
- },
187
- /**
188
- * Creates a step that delegates a turn to the opencode harness.
189
- * @public
190
- * @param opts - See {@link step.claudeCode}; `opts.harness` is an `opencode(...)` adapter.
191
- * @returns A `StepSubHarness` of kind `opencode`.
192
- */
193
- opencode(opts) {
194
- return buildSubHarnessStep('opencode', 'step.opencode', opts);
195
- },
196
- /**
197
- * Creates a step that delegates a turn to the pi harness.
198
- * @public
199
- * @param opts - See {@link step.claudeCode}; `opts.harness` is a `pi(...)` adapter.
200
- * @returns A `StepSubHarness` of kind `pi`.
167
+ * @param opts.permissions - Declarative policy answering the agent's permission requests.
168
+ * @param opts.output - Optional Standard Schema enabling structured output parsing.
169
+ * @returns A `StepAcpAgent`, auto-registered in the step registry.
170
+ * @throws `NoeticConfigError` `EMPTY_STEP_ID` / `MISSING_ACP_AGENT` / `MISSING_PROMPT`.
201
171
  */
202
- pi(opts) {
203
- return buildSubHarnessStep('pi', 'step.pi', opts);
172
+ acpAgent(opts) {
173
+ validateAcpAgentOpts(opts);
174
+ const built = {
175
+ kind: 'acp-agent',
176
+ ...opts,
177
+ };
178
+ getDefaultRegistrar().register(built);
179
+ return built;
204
180
  },
205
181
  };
206
182
  //#endregion
@@ -6,20 +6,24 @@
6
6
  * ones — they register in the step registry, carry retry policies, etc.
7
7
  */
8
8
  import type { ContextData, ContextLayer } from '@noetic-tools/context';
9
- import type { ExecuteStepFn, OutputCodec, Step, SubHarness, SubHarnessKind, SubprocessAdapter, Tool } from '@noetic-tools/types';
9
+ import type { AcpAgent, ExecuteStepFn, OutputCodec, Step, SubprocessAdapter, Tool } from '@noetic-tools/types';
10
10
  import type { WorkflowDocument, WorkflowNode } from '../schemas/workflow.js';
11
11
  /** @public Context required to hydrate a JSON workflow into live steps. */
12
12
  export interface HydrationContext {
13
13
  tools: ReadonlyMap<string, Tool>;
14
14
  executeStep: ExecuteStepFn;
15
15
  layers?: ReadonlyMap<string, ContextLayer>;
16
- /** SubHarness adapters keyed by harness id, resolving `claude-code`/`codex`/… nodes. */
17
- subHarnesses?: ReadonlyMap<SubHarnessKind, SubHarness>;
16
+ /**
17
+ * ACP agent adapters keyed by `agentId`, resolving an `acp-agent` node's
18
+ * `agent` field. An OPEN registry — supporting a new agent needs no schema
19
+ * change, only another entry here.
20
+ */
21
+ acpAgents?: ReadonlyMap<string, AcpAgent>;
18
22
  /**
19
23
  * Output codecs keyed by the `library` ref a `callModel` node's `output` codec
20
24
  * reference names — the live codec built from a component library, e.g.
21
25
  * `new Map([['dashboard-lib', openUi(dashboardLibrary)]])` from
22
- * `@noetic-tools/openui`. Resolved the same way sub-harness adapters are.
26
+ * `@noetic-tools/openui`. Resolved the same way ACP agent adapters are.
23
27
  */
24
28
  uiLibraries?: ReadonlyMap<string, OutputCodec>;
25
29
  /**
@@ -374,33 +374,28 @@ function hydrateScheduleNode(node, ctx) {
374
374
  onError: node.onError,
375
375
  });
376
376
  }
377
- const SUB_HARNESS_BUILDERS = {
378
- 'claude-code': (opts) => step.claudeCode(opts),
379
- codex: (opts) => step.codex(opts),
380
- opencode: (opts) => step.opencode(opts),
381
- pi: (opts) => step.pi(opts),
382
- };
383
- function hydrateSubHarnessNode(node, ctx) {
384
- if (node.kind !== 'claude-code' &&
385
- node.kind !== 'codex' &&
386
- node.kind !== 'opencode' &&
387
- node.kind !== 'pi') {
377
+ function hydrateAcpAgentNode(node, ctx) {
378
+ if (node.kind !== 'acp-agent') {
388
379
  return frameworkCast(undefined);
389
380
  }
390
- const harness = ctx.subHarnesses?.get(node.kind);
391
- if (!harness) {
381
+ const agent = ctx.acpAgents?.get(node.agent);
382
+ if (!agent) {
392
383
  throw new NoeticConfigError({
393
- code: 'UNKNOWN_SUB_HARNESS_REFERENCE',
394
- message: `SubHarness '${node.kind}' referenced in workflow node '${node.id}' is not registered.`,
395
- hint: `Pass harness adapters via HydrationContext.subHarnesses, e.g. new Map([['${node.kind}', ${node.kind.replace(/-([a-z])/g, (_, c) => c.toUpperCase())}({ model })]]).`,
384
+ code: 'UNKNOWN_ACP_AGENT_REFERENCE',
385
+ message: `ACP agent '${node.agent}' referenced in workflow node '${node.id}' is not registered.`,
386
+ hint: 'Pass agent adapters via HydrationContext.acpAgents, e.g. createAcpAgentRegistry(claudeCode()) from @noetic-tools/acp.',
396
387
  });
397
388
  }
398
- return SUB_HARNESS_BUILDERS[node.kind]({
389
+ return step.acpAgent({
399
390
  id: node.id,
400
- harness,
391
+ agent,
401
392
  prompt: node.prompt,
402
- instructions: node.instructions,
403
- settings: node.settings,
393
+ cwd: node.cwd,
394
+ mode: node.mode,
395
+ model: node.model,
396
+ mcpServers: node.mcpServers,
397
+ permissions: node.permissions,
398
+ clientCapabilities: node.clientCapabilities,
404
399
  session: node.session,
405
400
  });
406
401
  }
@@ -487,10 +482,7 @@ const NODE_HYDRATORS = {
487
482
  sequence: hydrateSequenceNode,
488
483
  schedule: hydrateScheduleNode,
489
484
  subflow: hydrateSubflowNode,
490
- 'claude-code': hydrateSubHarnessNode,
491
- codex: hydrateSubHarnessNode,
492
- opencode: hydrateSubHarnessNode,
493
- pi: hydrateSubHarnessNode,
485
+ 'acp-agent': hydrateAcpAgentNode,
494
486
  };
495
487
  //#endregion
496
488
  //#region Helpers
@@ -6,7 +6,7 @@
6
6
  * import cycle.
7
7
  */
8
8
  import type { ContextData, ContextLayer } from '@noetic-tools/context';
9
- import type { OutputCodec, StepRunCode, SubHarness, SubHarnessKind, SubprocessAdapter, Tool, WorkflowDocument } from '@noetic-tools/types';
9
+ import type { AcpAgent, OutputCodec, StepRunCode, SubprocessAdapter, Tool, WorkflowDocument } from '@noetic-tools/types';
10
10
  /** @public Options for `workflow`. */
11
11
  export interface WorkflowOpts {
12
12
  id: string;
@@ -20,8 +20,8 @@ export interface WorkflowOpts {
20
20
  layers?: ReadonlyMap<string, ContextLayer>;
21
21
  /** Named sub-workflows for `subflow` nodes — and for resolving `ref` itself. */
22
22
  workflows?: ReadonlyMap<string, WorkflowDocument>;
23
- /** SubHarness adapters for `claude-code`/`codex`/`opencode`/`pi` nodes. */
24
- subHarnesses?: ReadonlyMap<SubHarnessKind, SubHarness>;
23
+ /** ACP agent adapters, keyed by `agentId`, for `acp-agent` nodes. */
24
+ acpAgents?: ReadonlyMap<string, AcpAgent>;
25
25
  /** Output codecs for `callModel` nodes' `output` codec references. */
26
26
  uiLibraries?: ReadonlyMap<string, OutputCodec>;
27
27
  /** Resolver for named subprocess adapters on `runCode` nodes. */
@@ -91,7 +91,7 @@ function hydrate(opts, executeStep) {
91
91
  executeStep,
92
92
  layers: opts.layers,
93
93
  workflows: opts.workflows,
94
- subHarnesses: opts.subHarnesses,
94
+ acpAgents: opts.acpAgents,
95
95
  uiLibraries: opts.uiLibraries,
96
96
  resolveSubprocess: opts.resolveSubprocess,
97
97
  subflowAncestry: opts.ref
@@ -1,8 +1,9 @@
1
1
  import { ItemSchemaRegistry } from '@noetic-tools/types';
2
+ import { AcpSessionStore } from '../runtime/acp-session-store.js';
2
3
  import type { LayerStateStore, RecallCache } from './deps/context.js';
3
4
  import type { DetachedSpawnOverrides } from './deps/interpreter.js';
4
5
  import type { CheckpointStore, EventBroadcaster, RestoreCheckpointOptions, StepLedgerRetention, StepLedgerStore } from './deps/runtime.js';
5
- import type { AgentConfig, AgentHarnessContract, AgentHooks, CallModelRequest, Channel, ChannelHandle, Context, ContextCacheConfig, ContextCacheStore, ContextData, ContextLayer, CwdState, DeliveryMode, DetachedHandle, ExecuteInput, ExecuteOptions, ExternalChannel, FsAdapter, HarnessResponse, HarnessStatus, Item, ItemSchemaConfig, LLMResponse, LlmProviderConfig, ProjectionPolicy, RecallLayerOutput, SessionScope, SessionUsage, ShellAdapter, Span, SteeringDecision, Step, StorageAdapter, StreamEvent, StreamingItem, SubHarnessSession, SubprocessAdapter, Tool, TraceExporter, ZodType } from './deps/types.js';
6
+ import type { AcpLiveSession, AcpSessionInfo, AgentConfig, AgentHarnessContract, AgentHooks, CallModelRequest, Channel, ChannelHandle, Context, ContextCacheConfig, ContextCacheStore, ContextData, ContextLayer, CwdState, DeliveryMode, DetachedHandle, ExecuteInput, ExecuteOptions, ExternalChannel, FsAdapter, HarnessResponse, HarnessStatus, Item, ItemSchemaConfig, LLMResponse, LlmProviderConfig, ProjectionPolicy, RecallLayerOutput, SessionScope, SessionUsage, ShellAdapter, Span, SteeringDecision, Step, StorageAdapter, StreamEvent, StreamingItem, SubprocessAdapter, Tool, TraceExporter, ZodType } from './deps/types.js';
6
7
  export { createStreamIdleWatchdog } from './model-call.js';
7
8
  /**
8
9
  * Storage-scoped environment configuration for `AgentHarness`.
@@ -160,12 +161,40 @@ export declare class AgentHarness<TParams extends Record<string, unknown> = Reco
160
161
  private readonly defaultDeliveryMode;
161
162
  private readonly sessions;
162
163
  /**
163
- * @internal Cross-step harness sessions keyed by `step.session.reuse`, kept
164
- * alive for the life of this harness so reused coding-agent sessions span
165
- * multiple steps. Reached by the interpreter's harness handler via
166
- * `frameworkCast`; do not access from outside core.
164
+ * @internal Live ACP connections. Reached by the interpreter via
165
+ * `frameworkCast`; use {@link listAcpSessions}, {@link getAcpSession},
166
+ * {@link cancelAcpSession}, and {@link closeAcpSessions} from outside core.
167
167
  */
168
- readonly subHarnessSessions: Map<string, SubHarnessSession>;
168
+ readonly acpSessions: AcpSessionStore;
169
+ /**
170
+ * Snapshot of every live ACP sub-agent, for a UI that wants to show what is
171
+ * running and let a user act on it. Read-only: a session's turns are driven
172
+ * by steps, so nothing here can start work behind the runtime's back.
173
+ * @public
174
+ */
175
+ listAcpSessions(): AcpSessionInfo[];
176
+ /**
177
+ * The live connection + session behind a handle from {@link listAcpSessions}
178
+ * (or a step's `session.reuse` key), or `undefined` when nothing matches.
179
+ * @public
180
+ */
181
+ getAcpSession(key: string): AcpLiveSession | undefined;
182
+ /**
183
+ * Interrupt whatever the sub-agent under `key` is doing, via `session/cancel`.
184
+ * The connection stays open and the turn still returns — with the `cancelled`
185
+ * stop reason, which the running step surfaces as a `cancelled` error.
186
+ * Returns false when nothing matches the key.
187
+ * @public
188
+ */
189
+ cancelAcpSession(key: string): Promise<boolean>;
190
+ /**
191
+ * Close every ACP connection this harness still holds, whatever its
192
+ * keep-alive scope. A connection owns a live agent — usually a child process
193
+ * whose stdio keeps the event loop alive — so a `keepAlive: 'harness'`
194
+ * session must be closed by its owner or the host will not exit. Idempotent.
195
+ * @public
196
+ */
197
+ closeAcpSessions(): Promise<void>;
169
198
  readonly layerStateStore: LayerStateStore;
170
199
  /** Per-harness memoization cache for `recallMode: 'eventual'` layers. */
171
200
  readonly recallCache: RecallCache;
@@ -346,7 +375,7 @@ export declare class AgentHarness<TParams extends Record<string, unknown> = Reco
346
375
  *
347
376
  * Aborting a context rejects everything blocked on it (channel `recv`
348
377
  * waiters, parked senders) with `{ kind: 'cancelled' }`, stops its in-flight
349
- * model call and sub-harness turn, and makes the next step boundary throw
378
+ * model call and ACP agent turn, and makes the next step boundary throw
350
379
  * `cancelled`.
351
380
  *
352
381
  * Cancellation is cooperative: a layer hook already in flight is allowed to