@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.
- package/dist/builders/acp-agent-tool.d.ts +67 -0
- package/dist/builders/acp-agent-tool.js +87 -0
- package/dist/builders/dynamic-workflow.js +2 -2
- package/dist/builders/step-builders.d.ts +42 -42
- package/dist/builders/step-builders.js +38 -62
- package/dist/builders/workflow-hydrator.d.ts +8 -4
- package/dist/builders/workflow-hydrator.js +16 -24
- package/dist/builders/workflow-step.d.ts +3 -3
- package/dist/builders/workflow-step.js +1 -1
- package/dist/harness/agent-harness.d.ts +36 -7
- package/dist/harness/agent-harness.js +49 -6
- package/dist/harness/deps/types.d.ts +1 -1
- package/dist/index.d.ts +8 -4
- package/dist/index.js +3 -3
- package/dist/interpreter/acp-events.d.ts +67 -0
- package/dist/interpreter/acp-events.js +256 -0
- package/dist/interpreter/acp-history.d.ts +22 -0
- package/dist/interpreter/acp-history.js +58 -0
- package/dist/interpreter/collect-tools.js +3 -6
- package/dist/interpreter/execute-acp-agent.d.ts +10 -0
- package/dist/interpreter/execute-acp-agent.js +505 -0
- package/dist/interpreter/execute.js +4 -11
- package/dist/interpreter/structured-output.d.ts +1 -1
- package/dist/interpreter/structured-output.js +1 -1
- package/dist/portable/builders.d.ts +2 -0
- package/dist/portable/builders.js +3 -1
- package/dist/runtime/acp-session-store.d.ts +75 -0
- package/dist/runtime/acp-session-store.js +0 -0
- package/dist/schemas/workflow.d.ts +1 -1
- package/package.json +3 -3
- package/schema/noetic-workflow.schema.json +215 -249
- package/dist/interpreter/execute-sub-harness.d.ts +0 -8
- package/dist/interpreter/execute-sub-harness.js +0 -222
- package/dist/interpreter/sub-harness-events.d.ts +0 -48
- 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": "
|
|
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
|
-
|
|
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,
|
|
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
|
|
41
|
+
export interface StepAcpAgentOpts<TContext, O> {
|
|
42
42
|
id: string;
|
|
43
|
-
/**
|
|
44
|
-
|
|
45
|
-
/** Turn prompt. Eager string or `(ctx) => string` getter. */
|
|
46
|
-
prompt
|
|
47
|
-
/**
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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?:
|
|
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
|
|
104
|
-
* (`runCode`, `callModel`, `invokeTool`), the coding-agent
|
|
105
|
-
*
|
|
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
|
|
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.
|
|
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.
|
|
118
|
-
* @param opts.output - Optional
|
|
119
|
-
* @returns A `
|
|
120
|
-
* @throws `NoeticConfigError` `EMPTY_STEP_ID` / `
|
|
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
|
-
|
|
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
|
|
4
|
+
//#region ACP agent builder helper
|
|
5
5
|
/**
|
|
6
|
-
*
|
|
7
|
-
*
|
|
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
|
|
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:
|
|
15
|
-
hint:
|
|
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.
|
|
17
|
+
if (!opts.agent) {
|
|
19
18
|
throw new NoeticConfigError({
|
|
20
|
-
code: '
|
|
21
|
-
message:
|
|
22
|
-
hint:
|
|
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
|
-
//
|
|
26
|
-
//
|
|
27
|
-
|
|
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: '
|
|
31
|
-
message:
|
|
32
|
-
hint: `
|
|
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
|
|
157
|
-
* (`runCode`, `callModel`, `invokeTool`), the coding-agent
|
|
158
|
-
*
|
|
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
|
|
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.
|
|
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.
|
|
171
|
-
* @param opts.output - Optional
|
|
172
|
-
* @returns A `
|
|
173
|
-
* @throws `NoeticConfigError` `EMPTY_STEP_ID` / `
|
|
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
|
-
|
|
203
|
-
|
|
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,
|
|
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
|
-
/**
|
|
17
|
-
|
|
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
|
|
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
|
-
|
|
378
|
-
'
|
|
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
|
|
391
|
-
if (!
|
|
381
|
+
const agent = ctx.acpAgents?.get(node.agent);
|
|
382
|
+
if (!agent) {
|
|
392
383
|
throw new NoeticConfigError({
|
|
393
|
-
code: '
|
|
394
|
-
message: `
|
|
395
|
-
hint:
|
|
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
|
|
389
|
+
return step.acpAgent({
|
|
399
390
|
id: node.id,
|
|
400
|
-
|
|
391
|
+
agent,
|
|
401
392
|
prompt: node.prompt,
|
|
402
|
-
|
|
403
|
-
|
|
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
|
-
'
|
|
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,
|
|
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
|
-
/**
|
|
24
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
|
164
|
-
*
|
|
165
|
-
*
|
|
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
|
|
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
|
|
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
|