@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
@@ -1,6 +1,7 @@
1
1
  import { frameworkCast, ItemSchemaRegistry, NoeticConfigError } from '@noetic-tools/types';
2
2
  import { SpanImpl } from '../observability/span-impl.js';
3
3
  import { NoopExporter } from '../observability/trace-exporter.js';
4
+ import { AcpSessionStore } from '../runtime/acp-session-store.js';
4
5
  import { createInMemoryFsAdapter, createInMemoryShellAdapter, createInMemorySubprocessAdapter, OpenRouter, } from './deps/adapters.js';
5
6
  import { afterModelCallLayers, allocateBudgets, assembleView, beforeToolCallLayers, completeLayers, contextToExecCtx, createContextCacheStore, createLayerStateStore, createRecallCache, DEFAULT_PROJECTION, disposeLayers, executeRerender, initLayers, projectHistoryLayers, recallLayers, recallLayersAtomic, recallLayersEventual, resolveLayerTools, runAppendPipeline, storeLayers, } from './deps/context.js';
6
7
  import { collectAllTools, deduplicateTools, dispatchStepThroughAdapter, execute, prepareBandedView, } from './deps/interpreter.js';
@@ -192,12 +193,53 @@ export class AgentHarness {
192
193
  defaultDeliveryMode;
193
194
  sessions = new Map();
194
195
  /**
195
- * @internal Cross-step harness sessions keyed by `step.session.reuse`, kept
196
- * alive for the life of this harness so reused coding-agent sessions span
197
- * multiple steps. Reached by the interpreter's harness handler via
198
- * `frameworkCast`; do not access from outside core.
196
+ * @internal Live ACP connections. Reached by the interpreter via
197
+ * `frameworkCast`; use {@link listAcpSessions}, {@link getAcpSession},
198
+ * {@link cancelAcpSession}, and {@link closeAcpSessions} from outside core.
199
199
  */
200
- subHarnessSessions = new Map();
200
+ acpSessions = new AcpSessionStore();
201
+ /**
202
+ * Snapshot of every live ACP sub-agent, for a UI that wants to show what is
203
+ * running and let a user act on it. Read-only: a session's turns are driven
204
+ * by steps, so nothing here can start work behind the runtime's back.
205
+ * @public
206
+ */
207
+ listAcpSessions() {
208
+ return this.acpSessions.list();
209
+ }
210
+ /**
211
+ * The live connection + session behind a handle from {@link listAcpSessions}
212
+ * (or a step's `session.reuse` key), or `undefined` when nothing matches.
213
+ * @public
214
+ */
215
+ getAcpSession(key) {
216
+ return this.acpSessions.get(key);
217
+ }
218
+ /**
219
+ * Interrupt whatever the sub-agent under `key` is doing, via `session/cancel`.
220
+ * The connection stays open and the turn still returns — with the `cancelled`
221
+ * stop reason, which the running step surfaces as a `cancelled` error.
222
+ * Returns false when nothing matches the key.
223
+ * @public
224
+ */
225
+ async cancelAcpSession(key) {
226
+ const entry = this.acpSessions.get(key);
227
+ if (!entry) {
228
+ return false;
229
+ }
230
+ await entry.session.cancel();
231
+ return true;
232
+ }
233
+ /**
234
+ * Close every ACP connection this harness still holds, whatever its
235
+ * keep-alive scope. A connection owns a live agent — usually a child process
236
+ * whose stdio keeps the event loop alive — so a `keepAlive: 'harness'`
237
+ * session must be closed by its owner or the host will not exit. Idempotent.
238
+ * @public
239
+ */
240
+ async closeAcpSessions() {
241
+ await this.acpSessions.closeAll();
242
+ }
201
243
  layerStateStore;
202
244
  /** Per-harness memoization cache for `recallMode: 'eventual'` layers. */
203
245
  recallCache;
@@ -520,6 +562,7 @@ export class AgentHarness {
520
562
  else {
521
563
  this.rootRunDepth.delete(ctx.id);
522
564
  this.channelStore.closeExecution(ctx.id);
565
+ await this.acpSessions.closeOwnedBy(ctx.id);
523
566
  }
524
567
  }
525
568
  }
@@ -848,7 +891,7 @@ export class AgentHarness {
848
891
  *
849
892
  * Aborting a context rejects everything blocked on it (channel `recv`
850
893
  * waiters, parked senders) with `{ kind: 'cancelled' }`, stops its in-flight
851
- * model call and sub-harness turn, and makes the next step boundary throw
894
+ * model call and ACP agent turn, and makes the next step boundary throw
852
895
  * `cancelled`.
853
896
  *
854
897
  * Cancellation is cooperative: a layer hook already in flight is allowed to
@@ -1,4 +1,4 @@
1
1
  export type { ContextCacheConfig, ContextCacheStore, ContextData, ContextLayer, ExecutionContext, ProjectionPolicy, StorageAdapter, } from '@noetic-tools/context';
2
- export type { AgentConfig, AgentHarnessContract, AgentHooks, CallModelRequest, Channel, ChannelHandle, Context, CwdState, DeliveryMode, DetachedHandle, ExecuteInput, ExecuteOptions, ExternalChannel, FsAdapter, HarnessResponse, HarnessStatus, Item, ItemSchemaConfig, ItemSchemaExtensions, LLMResponse, LlmProviderConfig, RecallLayerOutput, SessionScope, SessionUsage, ShellAdapter, Span, SteeringDecision, Step, StreamEvent, StreamingItem, SubHarnessSession, SubprocessAdapter, Tool, TraceExporter, } from '@noetic-tools/types';
2
+ export type { AcpKeepAlive, AcpLiveSession, AcpSessionInfo, AgentConfig, AgentHarnessContract, AgentHooks, CallModelRequest, Channel, ChannelHandle, Context, CwdState, DeliveryMode, DetachedHandle, ExecuteInput, ExecuteOptions, ExternalChannel, FsAdapter, HarnessResponse, HarnessStatus, Item, ItemSchemaConfig, ItemSchemaExtensions, LLMResponse, LlmProviderConfig, RecallLayerOutput, SessionScope, SessionUsage, ShellAdapter, Span, SteeringDecision, Step, StreamEvent, StreamingItem, SubprocessAdapter, Tool, TraceExporter, } from '@noetic-tools/types';
3
3
  export { SteeringAction } from '@noetic-tools/types';
4
4
  export type { ZodType } from 'zod';
package/dist/index.d.ts CHANGED
@@ -7,6 +7,10 @@ export { createInMemorySubprocessAdapter } from './adapters/in-memory-subprocess
7
7
  /** @public */
8
8
  export { createOpenRouterEmbed } from './adapters/openrouter.js';
9
9
  /** @public */
10
+ export type { AcpAgentToolOptions } from './builders/acp-agent-tool.js';
11
+ /** @public */
12
+ export { acpAgentTool } from './builders/acp-agent-tool.js';
13
+ /** @public */
10
14
  export { channel } from './builders/channel-builder.js';
11
15
  /** @public */
12
16
  export { context } from './builders/context-builder.js';
@@ -100,7 +104,7 @@ export { getRegistry, lookupStep, registerStep } from './runtime/step-registry.j
100
104
  /** @public */
101
105
  export { defaultItemSchemaRegistry, ItemSchema, ItemSchemaRegistry } from '@noetic-tools/types';
102
106
  /** @public */
103
- export type { CallModelWorkflowNode, ConditionalRoute, ConditionalWorkflowNode, InParallelWorkflowNode, InvokeToolWorkflowNode, LoopWorkflowNode, MergeStrategy, ScheduleWorkflowNode, SequenceWorkflowNode, SpawnWorkflowNode, SubflowWorkflowNode, SubHarnessWorkflowNode, UntilPredicate, WithContextWorkflowNode, WorkflowDocument, WorkflowNode, } from './schemas/workflow.js';
107
+ export type { AcpAgentWorkflowNode, CallModelWorkflowNode, ConditionalRoute, ConditionalWorkflowNode, InParallelWorkflowNode, InvokeToolWorkflowNode, LoopWorkflowNode, MergeStrategy, ScheduleWorkflowNode, SequenceWorkflowNode, SpawnWorkflowNode, SubflowWorkflowNode, UntilPredicate, WithContextWorkflowNode, WorkflowDocument, WorkflowNode, } from './schemas/workflow.js';
104
108
  /** @public */
105
109
  export { MergeStrategySchema, UntilPredicateSchema, validateWorkflow, WorkflowDocumentSchema, WorkflowNodeSchema, walkWorkflow, workflowDepth, } from './schemas/workflow.js';
106
110
  /** @public */
@@ -147,11 +151,11 @@ export type { AfterModelCallParams, AfterModelCallResult, BeforeToolCallParams,
147
151
  /** @public */
148
152
  export { LedgerEntryKind, SteeringAction } from '@noetic-tools/types';
149
153
  /** @public */
150
- export type { ExecuteStepFn, SettleResult, Snapshot, Step, StepCallModel, StepConditional, StepInParallel, StepInParallelAll, StepInParallelRace, StepInParallelSettle, StepInvokeTool, StepLoop, StepRunCode, StepSpawn, StepSubHarness, StepWithContext, Until, Verdict, } from '@noetic-tools/types';
154
+ export type { ExecuteStepFn, SettleResult, Snapshot, Step, StepAcpAgent, StepCallModel, StepConditional, StepInParallel, StepInParallelAll, StepInParallelRace, StepInParallelSettle, StepInvokeTool, StepLoop, StepRunCode, StepSpawn, StepWithContext, Until, Verdict, } from '@noetic-tools/types';
151
155
  /** @public */
152
- export type { SubHarness, SubHarnessBuiltinTool, SubHarnessContinueState, SubHarnessContinueTurnOptions, SubHarnessFinishReason, SubHarnessPromptTurnOptions, SubHarnessResumeState, SubHarnessRunContext, SubHarnessSession, SubHarnessSessionPolicy, SubHarnessSettings, SubHarnessStartOptions, SubHarnessStreamPart, SubHarnessTurnResult, } from '@noetic-tools/types';
156
+ export type { AcpAgent, AcpAgentCapabilities, AcpAgentConnection, AcpAuthMethod, AcpAvailableCommand, AcpBoundPermissionHandler, AcpClientCapabilityConfig, AcpClientHost, AcpConnectOptions, AcpContentBlock, AcpKeepAlive, AcpLiveSession, AcpLoadSessionOptions, AcpMcpServer, AcpNewSessionOptions, AcpPermissionHandler, AcpPermissionOption, AcpPermissionOutcome, AcpPermissionPolicy, AcpPermissionRequestInfo, AcpPermissionRule, AcpPermissionSteerer, AcpPlanEntry, AcpPromptCapabilities, AcpPromptOptions, AcpRequestPermissionRequest, AcpSession, AcpSessionDisposer, AcpSessionInfo, AcpSessionMode, AcpSessionModeState, AcpSessionNotification, AcpSessionPolicy, AcpStopReason, AcpToolCallContent, AcpToolCallStatus, AcpToolKind, AcpTransport, AcpTransportFactory, AcpTransportOptions, AcpTurnResult, } from '@noetic-tools/types';
153
157
  /** @public */
154
- export { SUB_HARNESS_KINDS, SubHarnessKind, SubHarnessStreamPartSchema } from '@noetic-tools/types';
158
+ export { ACP_AGENT_STEP_KIND, AcpCapabilityError, AcpConnectError, AcpPermissionDecision, isAcpCapabilityError, isAcpConnectError, } from '@noetic-tools/types';
155
159
  /** @public */
156
160
  export type { FsAdapter, FsStats } from '@noetic-tools/types';
157
161
  /** @public */
package/dist/index.js CHANGED
@@ -7,8 +7,8 @@ export { createInMemoryShellAdapter } from './adapters/in-memory-shell-adapter.j
7
7
  export { createInMemorySubprocessAdapter } from './adapters/in-memory-subprocess-adapter.js';
8
8
  /** @public */
9
9
  export { createOpenRouterEmbed } from './adapters/openrouter.js';
10
- //#endregion
11
- //#region Builders
10
+ /** @public */
11
+ export { acpAgentTool } from './builders/acp-agent-tool.js';
12
12
  /** @public */
13
13
  export { channel } from './builders/channel-builder.js';
14
14
  /** @public */
@@ -100,7 +100,7 @@ export { Slot } from '@noetic-tools/context';
100
100
  /** @public */
101
101
  export { LedgerEntryKind, SteeringAction } from '@noetic-tools/types';
102
102
  /** @public */
103
- export { SUB_HARNESS_KINDS, SubHarnessKind, SubHarnessStreamPartSchema } from '@noetic-tools/types';
103
+ export { ACP_AGENT_STEP_KIND, AcpCapabilityError, AcpConnectError, AcpPermissionDecision, isAcpCapabilityError, isAcpConnectError, } from '@noetic-tools/types';
104
104
  /** @public */
105
105
  export { TIMEOUT_ERROR_PREFIX } from '@noetic-tools/types';
106
106
  /** @public Reusable error-serialiser used by custom SubprocessAdapter implementations. */
@@ -0,0 +1,67 @@
1
+ /**
2
+ * Maps an ACP turn's `session/update` notifications onto the harness's
3
+ * observable event surface.
4
+ *
5
+ * Every one of the protocol's eight update variants is translated into the same
6
+ * `source: 'sdk'` broadcaster events the model-call path emits, so a coding
7
+ * agent's text, reasoning, and tool calls flow through `getTextStream()` /
8
+ * `getReasoningStream()` / `getItemStream()` / `getFullStream()` exactly like an
9
+ * LLM step's output. Each notification is *also* re-emitted raw as an
10
+ * `acp_event` framework event for consumers that want the protocol payload.
11
+ *
12
+ * `finalize()` guarantees a turn always emits its output: if an agent returns a
13
+ * stop reason without streaming any text or tool calls, the result's content is
14
+ * synthesized into events.
15
+ */
16
+ import type { AcpClientActivity, AcpSessionNotification, AcpTurnResult, Context, ContextData, StepAcpAgent } from '@noetic-tools/types';
17
+ export declare class AcpEventBridge {
18
+ private readonly broadcaster?;
19
+ private readonly agentName;
20
+ private readonly stepId;
21
+ private readonly agentId;
22
+ private readonly emitOption;
23
+ private started;
24
+ private completed;
25
+ private nextOutputIndex;
26
+ private messageOutputIndex;
27
+ private sawText;
28
+ /** Tool call ids already announced, so a `tool_call_update` never re-opens one. */
29
+ private readonly announcedToolCalls;
30
+ constructor(step: Pick<StepAcpAgent, 'id' | 'emit'>, agentId: string, ctx: Context<ContextData>);
31
+ /**
32
+ * Open the turn on the event surface. Emits `response.created` so every turn
33
+ * brackets with a lifecycle marker — even an agent that streams nothing.
34
+ */
35
+ begin(): void;
36
+ /**
37
+ * Record one client-side operation the agent performed. Unlike a `tool_call`
38
+ * update — which is the agent's own account of what it did — this is what it
39
+ * actually asked us to do, refusals included.
40
+ */
41
+ forwardActivity(activity: AcpClientActivity): void;
42
+ /** Forward one notification: a structured framework event + the mapped sdk events. */
43
+ forward(notification: AcpSessionNotification): void;
44
+ /**
45
+ * Ensure the turn's output reached the event surface. For an agent that
46
+ * returns a result without streaming, synthesize events from the result.
47
+ */
48
+ finalize(result: AcpTurnResult): void;
49
+ /**
50
+ * Close a turn that failed. The bracket a consumer waits on must be honoured
51
+ * even when the turn dies mid-stream, so an open message is closed and the
52
+ * response completes with an error rather than the stream simply stopping.
53
+ */
54
+ abort(cause: unknown): void;
55
+ /**
56
+ * Emit one framework event under this step's envelope. The `sdk` sibling
57
+ * below does the same job for the model-stream surface; both gate on
58
+ * `emitOption` so `emit: false` silences every surface at once.
59
+ */
60
+ private framework;
61
+ private sdk;
62
+ private start;
63
+ private openMessage;
64
+ private closeMessage;
65
+ private emitToolCall;
66
+ private translate;
67
+ }
@@ -0,0 +1,256 @@
1
+ /**
2
+ * Maps an ACP turn's `session/update` notifications onto the harness's
3
+ * observable event surface.
4
+ *
5
+ * Every one of the protocol's eight update variants is translated into the same
6
+ * `source: 'sdk'` broadcaster events the model-call path emits, so a coding
7
+ * agent's text, reasoning, and tool calls flow through `getTextStream()` /
8
+ * `getReasoningStream()` / `getItemStream()` / `getFullStream()` exactly like an
9
+ * LLM step's output. Each notification is *also* re-emitted raw as an
10
+ * `acp_event` framework event for consumers that want the protocol payload.
11
+ *
12
+ * `finalize()` guarantees a turn always emits its output: if an agent returns a
13
+ * stop reason without streaming any text or tool calls, the result's content is
14
+ * synthesized into events.
15
+ */
16
+ import { emitFrameworkEvent, getBroadcaster, shouldEmit } from '../runtime/broadcaster-utils.js';
17
+ import { isFunctionCall } from './typeguards.js';
18
+ /** Flatten an ACP content block for the text event surface. */
19
+ function blockText(block) {
20
+ if (block.type === 'text') {
21
+ return block.text;
22
+ }
23
+ if (block.type === 'resource_link') {
24
+ return `[resource: ${block.uri}]`;
25
+ }
26
+ if (block.type === 'resource') {
27
+ return 'text' in block.resource ? block.resource.text : `[resource: ${block.resource.uri}]`;
28
+ }
29
+ return `[${block.type}: ${block.mimeType}]`;
30
+ }
31
+ export class AcpEventBridge {
32
+ broadcaster;
33
+ agentName;
34
+ stepId;
35
+ agentId;
36
+ emitOption;
37
+ started = false;
38
+ completed = false;
39
+ nextOutputIndex = 0;
40
+ messageOutputIndex = null;
41
+ sawText = false;
42
+ /** Tool call ids already announced, so a `tool_call_update` never re-opens one. */
43
+ announcedToolCalls = new Set();
44
+ constructor(step, agentId, ctx) {
45
+ this.broadcaster = getBroadcaster(ctx);
46
+ this.agentName = ctx.harness.config.name;
47
+ this.stepId = step.id;
48
+ this.agentId = agentId;
49
+ this.emitOption = step.emit;
50
+ }
51
+ /**
52
+ * Open the turn on the event surface. Emits `response.created` so every turn
53
+ * brackets with a lifecycle marker — even an agent that streams nothing.
54
+ */
55
+ begin() {
56
+ this.start();
57
+ }
58
+ /**
59
+ * Record one client-side operation the agent performed. Unlike a `tool_call`
60
+ * update — which is the agent's own account of what it did — this is what it
61
+ * actually asked us to do, refusals included.
62
+ */
63
+ forwardActivity(activity) {
64
+ this.framework('acp_client_activity', {
65
+ ...activity,
66
+ });
67
+ }
68
+ /** Forward one notification: a structured framework event + the mapped sdk events. */
69
+ forward(notification) {
70
+ if (!this.broadcaster) {
71
+ return;
72
+ }
73
+ this.framework('acp_event', {
74
+ sessionId: notification.sessionId,
75
+ update: notification.update,
76
+ });
77
+ this.translate(notification.update);
78
+ }
79
+ /**
80
+ * Ensure the turn's output reached the event surface. For an agent that
81
+ * returns a result without streaming, synthesize events from the result.
82
+ */
83
+ finalize(result) {
84
+ if (!this.broadcaster) {
85
+ return;
86
+ }
87
+ if (!this.sawText && result.text.length > 0) {
88
+ this.openMessage();
89
+ this.sdk('response.output_text.delta', {
90
+ delta: result.text,
91
+ }, this.messageOutputIndex ?? 0);
92
+ this.sawText = true;
93
+ }
94
+ // Close the message before synthesizing tool calls, so output items finish
95
+ // in index order. Emitting them first left item 0 closing after items 1..n,
96
+ // which reads as interleaved output to a consumer tracking `outputIndex`.
97
+ this.closeMessage();
98
+ for (const item of result.items) {
99
+ if (isFunctionCall(item) && !this.announcedToolCalls.has(item.callId)) {
100
+ this.emitToolCall(item.callId, item.name, item.arguments);
101
+ }
102
+ }
103
+ if (!this.completed) {
104
+ this.start();
105
+ this.sdk('response.completed', {
106
+ stopReason: result.stopReason,
107
+ });
108
+ this.completed = true;
109
+ }
110
+ }
111
+ /**
112
+ * Close a turn that failed. The bracket a consumer waits on must be honoured
113
+ * even when the turn dies mid-stream, so an open message is closed and the
114
+ * response completes with an error rather than the stream simply stopping.
115
+ */
116
+ abort(cause) {
117
+ if (!this.broadcaster || this.completed) {
118
+ return;
119
+ }
120
+ this.closeMessage();
121
+ this.start();
122
+ this.sdk('response.completed', {
123
+ stopReason: 'error',
124
+ error: cause instanceof Error ? cause.message : String(cause),
125
+ });
126
+ this.completed = true;
127
+ }
128
+ //#region internals
129
+ /**
130
+ * Emit one framework event under this step's envelope. The `sdk` sibling
131
+ * below does the same job for the model-stream surface; both gate on
132
+ * `emitOption` so `emit: false` silences every surface at once.
133
+ */
134
+ framework(eventType, payload) {
135
+ if (!this.broadcaster) {
136
+ return;
137
+ }
138
+ const data = {
139
+ stepId: this.stepId,
140
+ agentId: this.agentId,
141
+ ...payload,
142
+ };
143
+ if (!shouldEmit(this.emitOption, eventType, data)) {
144
+ return;
145
+ }
146
+ emitFrameworkEvent({
147
+ broadcaster: this.broadcaster,
148
+ agentName: this.agentName,
149
+ eventType,
150
+ data,
151
+ });
152
+ }
153
+ sdk(type, data, outputIndex) {
154
+ if (!this.broadcaster) {
155
+ return;
156
+ }
157
+ if (!shouldEmit(this.emitOption, type, data)) {
158
+ return;
159
+ }
160
+ this.broadcaster.emit({
161
+ source: 'sdk',
162
+ type,
163
+ data,
164
+ outputIndex,
165
+ });
166
+ }
167
+ start() {
168
+ if (this.started) {
169
+ return;
170
+ }
171
+ this.started = true;
172
+ this.sdk('response.created', {});
173
+ }
174
+ openMessage() {
175
+ if (this.messageOutputIndex !== null) {
176
+ return;
177
+ }
178
+ this.start();
179
+ const id = `acp-msg-${crypto.randomUUID()}`;
180
+ this.messageOutputIndex = this.nextOutputIndex++;
181
+ this.sdk('response.output_item.added', {
182
+ item: {
183
+ id,
184
+ type: 'message',
185
+ role: 'assistant',
186
+ status: 'in_progress',
187
+ content: [
188
+ {
189
+ type: 'output_text',
190
+ text: '',
191
+ },
192
+ ],
193
+ },
194
+ }, this.messageOutputIndex);
195
+ }
196
+ closeMessage() {
197
+ if (this.messageOutputIndex === null) {
198
+ return;
199
+ }
200
+ const index = this.messageOutputIndex;
201
+ this.sdk('response.output_text.done', {}, index);
202
+ this.sdk('response.output_item.done', {}, index);
203
+ this.messageOutputIndex = null;
204
+ }
205
+ emitToolCall(toolCallId, name, input) {
206
+ this.start();
207
+ const index = this.nextOutputIndex++;
208
+ const args = typeof input === 'string' ? input : JSON.stringify(input ?? {});
209
+ this.sdk('response.output_item.added', {
210
+ item: {
211
+ id: toolCallId,
212
+ type: 'function_call',
213
+ status: 'in_progress',
214
+ callId: toolCallId,
215
+ name,
216
+ arguments: '',
217
+ },
218
+ }, index);
219
+ this.sdk('response.function_call_arguments.delta', {
220
+ delta: args,
221
+ }, index);
222
+ this.sdk('response.function_call_arguments.done', {
223
+ arguments: args,
224
+ }, index);
225
+ this.sdk('response.output_item.done', {}, index);
226
+ this.announcedToolCalls.add(toolCallId);
227
+ }
228
+ translate(update) {
229
+ if (update.sessionUpdate === 'agent_message_chunk') {
230
+ this.openMessage();
231
+ this.sdk('response.output_text.delta', {
232
+ delta: blockText(update.content),
233
+ }, this.messageOutputIndex ?? 0);
234
+ this.sawText = true;
235
+ return;
236
+ }
237
+ if (update.sessionUpdate === 'agent_thought_chunk') {
238
+ this.start();
239
+ this.sdk('response.reasoning.delta', {
240
+ delta: blockText(update.content),
241
+ });
242
+ return;
243
+ }
244
+ if (update.sessionUpdate === 'tool_call') {
245
+ this.closeMessage();
246
+ this.emitToolCall(update.toolCallId, update.title, update.rawInput);
247
+ return;
248
+ }
249
+ // `user_message_chunk` (replayed history), `tool_call_update`, `plan`,
250
+ // `available_commands_update`, and `current_mode_update` have no model-stream
251
+ // analogue; they surface on the full stream under their protocol names.
252
+ this.sdk(`acp.${update.sessionUpdate}`, {
253
+ update,
254
+ });
255
+ }
256
+ }
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Gives an ACP agent the prior conversation.
3
+ *
4
+ * ACP sessions own their own history on the agent side, but a freshly opened
5
+ * session knows nothing about the Noetic steps that ran before it. Seeding the
6
+ * first prompt with a transcript of the conversation so far means a coding agent
7
+ * running after a chain of `callModel` steps understands what was already
8
+ * established instead of answering out of context.
9
+ */
10
+ import type { Item } from '@noetic-tools/types';
11
+ /** @public Render conversation history as a plain-text transcript. */
12
+ export declare function formatConversation(items: ReadonlyArray<Item>): string;
13
+ /**
14
+ * Fold the conversation so far into a turn's prompt. Returns the prompt
15
+ * unchanged when there is no history, so the very first step of a run is not
16
+ * padded with an empty preamble.
17
+ * @public
18
+ */
19
+ export declare function withHistoryPrompt(input: {
20
+ prompt: string;
21
+ history: ReadonlyArray<Item>;
22
+ }): string;
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Gives an ACP agent the prior conversation.
3
+ *
4
+ * ACP sessions own their own history on the agent side, but a freshly opened
5
+ * session knows nothing about the Noetic steps that ran before it. Seeding the
6
+ * first prompt with a transcript of the conversation so far means a coding agent
7
+ * running after a chain of `callModel` steps understands what was already
8
+ * established instead of answering out of context.
9
+ */
10
+ function roleLabel(role) {
11
+ if (role === 'assistant') {
12
+ return 'Assistant';
13
+ }
14
+ if (role === 'user') {
15
+ return 'User';
16
+ }
17
+ return 'System';
18
+ }
19
+ function messageText(item) {
20
+ if (item.type !== 'message') {
21
+ return null;
22
+ }
23
+ const text = item.content
24
+ .map((part) => ('text' in part && typeof part.text === 'string' ? part.text : ''))
25
+ .join('');
26
+ return text.length > 0 ? `${roleLabel(item.role)}: ${text}` : null;
27
+ }
28
+ /** @public Render conversation history as a plain-text transcript. */
29
+ export function formatConversation(items) {
30
+ const lines = [];
31
+ for (const item of items) {
32
+ const line = messageText(item);
33
+ if (line) {
34
+ lines.push(line);
35
+ continue;
36
+ }
37
+ if (item.type === 'function_call') {
38
+ lines.push(`Assistant called ${item.name}(${item.arguments})`);
39
+ }
40
+ }
41
+ return lines.join('\n');
42
+ }
43
+ /**
44
+ * Fold the conversation so far into a turn's prompt. Returns the prompt
45
+ * unchanged when there is no history, so the very first step of a run is not
46
+ * padded with an empty preamble.
47
+ * @public
48
+ */
49
+ export function withHistoryPrompt(input) {
50
+ if (input.history.length === 0) {
51
+ return input.prompt;
52
+ }
53
+ const transcript = formatConversation(input.history);
54
+ if (transcript.length === 0) {
55
+ return input.prompt;
56
+ }
57
+ return `Conversation so far:\n${transcript}\n\nCurrent request:\n${input.prompt}`;
58
+ }
@@ -43,12 +43,9 @@ function walkStep(step, out) {
43
43
  return;
44
44
  case 'runCode':
45
45
  case 'invokeTool':
46
- // SubHarness steps run their own built-in tools inside the external agent;
47
- // they contribute nothing to the host unified tool set.
48
- case 'claude-code':
49
- case 'codex':
50
- case 'opencode':
51
- case 'pi':
46
+ // An ACP agent runs its own tools inside the external agent process; it
47
+ // contributes nothing to the host unified tool set.
48
+ case 'acp-agent':
52
49
  return;
53
50
  case 'conditional':
54
51
  if (step._optimizable) {
@@ -0,0 +1,10 @@
1
+ /**
2
+ * ACP step handler: drives an external coding agent over the Agent Client
3
+ * Protocol for one prompt turn and folds its output back into the Noetic
4
+ * execution context — the ACP analogue of `executeCallModel`.
5
+ *
6
+ * Core reaches the agent only through the `@noetic-tools/types` contract, so no
7
+ * protocol library or agent SDK enters its dependency graph.
8
+ */
9
+ import type { Context, ContextLayer, StepAcpAgent } from '@noetic-tools/types';
10
+ export declare function executeAcpAgent<TContext, I, O>(step: StepAcpAgent<TContext, I, O>, input: I, ctx: Context<TContext>, layers?: ContextLayer[]): Promise<O>;