@noetic-tools/types 3.2.1 → 3.2.2

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.
@@ -0,0 +1,525 @@
1
+ /**
2
+ * Agent Client Protocol (ACP) contract.
3
+ *
4
+ * Noetic drives external coding agents — Claude Code, Codex, Gemini CLI, any
5
+ * ACP-speaking binary — as an **ACP Client**. The agent is a JSON-RPC 2.0 peer
6
+ * reached over a duplex byte stream, so there is no vendor SDK per agent and no
7
+ * closed set of supported agents.
8
+ *
9
+ * This file is the direct analogue of the `ContextLayer` contract: defined in
10
+ * the foundation package so both `@noetic-tools/core` (which executes ACP steps)
11
+ * and `@noetic-tools/acp` (which implements the protocol) depend on it without
12
+ * forming a cycle. Core calls only the methods declared here — the ACP library's
13
+ * `ClientSideConnection` never enters core's dependency graph.
14
+ *
15
+ * Protocol types are re-used verbatim from `@zed-industries/agent-client-protocol`
16
+ * (spec version 1) rather than mirrored, so the wire surface cannot drift from
17
+ * the specification.
18
+ *
19
+ * @see https://agentclientprotocol.com/
20
+ */
21
+ import type * as acp from '@zed-industries/agent-client-protocol';
22
+ import type { TokenUsage } from './common.js';
23
+ import type { Context } from './context.js';
24
+ import type { FsAdapter } from './fs-adapter.js';
25
+ import type { Item } from './items.js';
26
+ import type { ShellAdapter } from './shell-adapter.js';
27
+ /**
28
+ * The single step kind backing every ACP agent. Unlike the closed harness enum
29
+ * it replaces, the *agent* is identified by a free-form `agentId` on the
30
+ * adapter — so adding an agent never touches core or the published JSON Schema.
31
+ * @public
32
+ */
33
+ export declare const ACP_AGENT_STEP_KIND = "acp-agent";
34
+ /**
35
+ * A bidirectional byte stream to an ACP agent — the substrate JSON-RPC frames
36
+ * ride on.
37
+ *
38
+ * This is deliberately *not* built on {@link SubprocessAdapter}: that contract
39
+ * returns a durable handle (`id`/`status`/`metadata`) with no attached stdio,
40
+ * and {@link ShellAdapter} is one-shot. ACP needs a long-lived duplex pipe.
41
+ * @public
42
+ */
43
+ export interface AcpTransport {
44
+ /** Frames arriving from the agent. */
45
+ readonly readable: ReadableStream<Uint8Array>;
46
+ /** Frames sent to the agent. */
47
+ readonly writable: WritableStream<Uint8Array>;
48
+ /** Tear the stream down and release the underlying resource (process, socket, …). */
49
+ close(): Promise<void>;
50
+ }
51
+ /** @public Options a transport factory receives when a step opens a connection. */
52
+ export interface AcpTransportOptions {
53
+ /** Working directory the agent process should start in. */
54
+ cwd: string;
55
+ /** Environment overrides layered onto the host environment. */
56
+ env?: Record<string, string | undefined>;
57
+ /** Aborting this signal closes the transport. */
58
+ signal?: AbortSignal;
59
+ }
60
+ /**
61
+ * Opens a transport on demand. Local agents use a stdio transport over a child
62
+ * process; tests use an in-memory loopback; remote agents can use anything that
63
+ * yields a duplex byte stream.
64
+ * @public
65
+ */
66
+ export type AcpTransportFactory = (opts: AcpTransportOptions) => Promise<AcpTransport>;
67
+ /** @public How a permission request was resolved. */
68
+ export declare const AcpPermissionDecision: {
69
+ readonly Allow: 'allow';
70
+ readonly Deny: 'deny';
71
+ readonly Cancel: 'cancel';
72
+ };
73
+ /** @public Union of permission decisions. */
74
+ export type AcpPermissionDecision = (typeof AcpPermissionDecision)[keyof typeof AcpPermissionDecision];
75
+ /**
76
+ * The resolved answer to an ACP `session/request_permission` call. The client
77
+ * translates it into the concrete {@link acp.PermissionOption} the agent
78
+ * offered — `optionId` pins an exact option when the resolver picked one.
79
+ * @public
80
+ */
81
+ export interface AcpPermissionOutcome {
82
+ decision: AcpPermissionDecision;
83
+ /** Exact option id to select. When omitted the client picks by `decision`. */
84
+ optionId?: string;
85
+ /** Why this decision was made — surfaced on framework events and the steering ledger. */
86
+ reason?: string;
87
+ }
88
+ /**
89
+ * Matches a tool call the agent wants to run. An omitted field matches anything;
90
+ * all present fields must match.
91
+ * @public
92
+ */
93
+ export interface AcpPermissionRule {
94
+ /** ACP tool classification (`read`, `edit`, `execute`, …). */
95
+ kind?: acp.ToolKind;
96
+ /** Matches the tool call title — substring (case-insensitive) or pattern. */
97
+ title?: string | RegExp;
98
+ }
99
+ /**
100
+ * Declarative permission policy evaluated before the steering pipeline and the
101
+ * `onPermissionRequest` handler.
102
+ * @public
103
+ */
104
+ export interface AcpPermissionPolicy {
105
+ /**
106
+ * Decision when no rule matches and no later resolver decides. Defaults to
107
+ * `'deny'` — an unattended agent must not get blanket approval by omission.
108
+ */
109
+ default?: AcpPermissionDecision;
110
+ /** Rules granting permission. Evaluated before {@link deny}. */
111
+ allow?: ReadonlyArray<AcpPermissionRule>;
112
+ /** Rules refusing permission. */
113
+ deny?: ReadonlyArray<AcpPermissionRule>;
114
+ /**
115
+ * Prefer the agent's `allow_always` / `reject_always` options over the
116
+ * `*_once` variants, so a decision persists for the rest of the session.
117
+ */
118
+ persist?: boolean;
119
+ }
120
+ /**
121
+ * Async escape hatch for decisions a declarative policy cannot express —
122
+ * human-in-the-loop approval, a remote policy service, an LLM judge.
123
+ *
124
+ * Receives the executing {@link Context} because reaching a human almost always
125
+ * means reaching a channel: `ctx.send` the request out, `ctx.recv` the decision
126
+ * back. Without it a handler could only answer from what it closed over.
127
+ * @public
128
+ */
129
+ export type AcpPermissionHandler = (request: acp.RequestPermissionRequest, ctx: Context, info: AcpPermissionRequestInfo) => Promise<AcpPermissionOutcome>;
130
+ /**
131
+ * Which agent and step raised a permission request. The protocol request says
132
+ * what the agent wants to do but not who is asking, and a human answering
133
+ * needs both to make sense of the prompt.
134
+ * @public
135
+ */
136
+ export interface AcpPermissionRequestInfo {
137
+ /** `agentId` of the adapter whose turn raised the request. */
138
+ agentId: string;
139
+ /** Id of the step whose turn raised it. */
140
+ stepId: string;
141
+ }
142
+ /**
143
+ * The handler as the protocol client sees it — already bound to the executing
144
+ * context by the runtime. Keeps `Context` out of the client, which only needs
145
+ * to ask the question and get an answer.
146
+ * @public
147
+ */
148
+ export type AcpBoundPermissionHandler = (request: acp.RequestPermissionRequest) => Promise<AcpPermissionOutcome>;
149
+ /**
150
+ * The steering tier of permission resolution. Returning `undefined` abstains,
151
+ * handing the decision to the next tier. The runtime wires this to the same
152
+ * `beforeToolCall` pipeline that governs first-party tool calls, so one rule set
153
+ * covers both.
154
+ * @public
155
+ */
156
+ export type AcpPermissionSteerer = (request: acp.RequestPermissionRequest) => Promise<AcpPermissionOutcome | undefined>;
157
+ /**
158
+ * Which ACP client capabilities to advertise during `initialize`. Every flag
159
+ * defaults to enabled when the host can back it; setting one to `false`
160
+ * withdraws it, so the agent never attempts the call.
161
+ * @public
162
+ */
163
+ export interface AcpClientCapabilityConfig {
164
+ /** Serve `fs/read_text_file` from the host's {@link FsAdapter}. */
165
+ readTextFile?: boolean;
166
+ /** Serve `fs/write_text_file` from the host's {@link FsAdapter}. */
167
+ writeTextFile?: boolean;
168
+ /** Serve the `terminal/*` family from the host's {@link ShellAdapter}. */
169
+ terminal?: boolean;
170
+ /**
171
+ * Directories the agent may reach in addition to the session working
172
+ * directory, which is always allowed. Use for a monorepo sibling, a shared
173
+ * cache, or a scratch dir outside the workspace.
174
+ */
175
+ additionalDirectories?: ReadonlyArray<string>;
176
+ /**
177
+ * Lift filesystem confinement entirely, letting the agent read and write any
178
+ * absolute path the host process can.
179
+ *
180
+ * Off by default, and worth leaving off: ACP places boundary enforcement on
181
+ * the client, and a `permissions` policy does not substitute for it — that
182
+ * answers `session/request_permission`, which covers the agent's tool calls,
183
+ * not the `fs/*` methods it invokes on the client directly.
184
+ */
185
+ allowAnyPath?: boolean;
186
+ }
187
+ /**
188
+ * The client methods an agent can invoke on us. Closed by ACP spec version 1,
189
+ * so a literal union rather than `string`: a typo in a descriptor is a compile
190
+ * error, and consumers narrowing on `activity.method` get exhaustiveness.
191
+ * @public
192
+ */
193
+ export type AcpClientMethod = 'fs/read_text_file' | 'fs/write_text_file' | 'terminal/create' | 'terminal/output' | 'terminal/wait_for_exit' | 'terminal/kill' | 'terminal/release';
194
+ /**
195
+ * One client-side operation an agent performed — or was refused. Emitted for
196
+ * every `fs/*` and `terminal/*` call the agent makes.
197
+ *
198
+ * This is an *observed* record, not the agent's self-report: `tool_call`
199
+ * updates say what the agent claims it did, while this says what it actually
200
+ * asked the client to do, including the calls that were turned down.
201
+ * @public
202
+ */
203
+ export interface AcpClientActivity {
204
+ /** The ACP client method invoked, e.g. `fs/read_text_file`. */
205
+ method: AcpClientMethod;
206
+ /** Path involved, for the `fs/*` family and `terminal/create`'s cwd. */
207
+ path?: string;
208
+ /** Command line, for `terminal/create`. */
209
+ command?: string;
210
+ /** Terminal handle, for the `terminal/*` family. */
211
+ terminalId?: string;
212
+ /** False when the call was refused — by a withdrawn capability or confinement. */
213
+ allowed: boolean;
214
+ /** Why it was refused. */
215
+ reason?: string;
216
+ }
217
+ /**
218
+ * The runtime surface an ACP connection is given to satisfy client-side
219
+ * requests. Because file reads/writes and terminal commands are served from
220
+ * Noetic's own adapters, everything the sub-agent touches passes through the
221
+ * same sandboxing, virtual-filesystem, and audit machinery as first-party steps.
222
+ * @public
223
+ */
224
+ export interface AcpClientHost {
225
+ /** Working directory for the session (resolved from `ctx.cwdState.cwd`). */
226
+ readonly cwd: string;
227
+ readonly fs: FsAdapter;
228
+ readonly shell: ShellAdapter;
229
+ /** Conversation thread id, for hosts that scope state per thread. */
230
+ readonly threadId: string;
231
+ /** Abort signal for the surrounding execution. */
232
+ readonly signal?: AbortSignal;
233
+ /**
234
+ * Capability advertisement overrides. Connection-level: ACP negotiates the
235
+ * client capability set once during `initialize`, so this cannot change for
236
+ * the life of a connection.
237
+ */
238
+ readonly capabilities?: AcpClientCapabilityConfig;
239
+ /**
240
+ * Permission resolution, in the order it is consulted: the declarative
241
+ * policy, then steering, then the async handler. The protocol client owns the
242
+ * resolution logic; the host only supplies the inputs.
243
+ *
244
+ * These are **per-turn**, and deliberately mutable: a session reused across
245
+ * several steps must answer with the CURRENT step's policy, not the one that
246
+ * happened to open the connection. The client reads them at call time, so
247
+ * the runtime updates them before each turn.
248
+ */
249
+ permissions?: AcpPermissionPolicy;
250
+ steerPermission?: AcpPermissionSteerer;
251
+ onPermissionRequest?: AcpBoundPermissionHandler;
252
+ /**
253
+ * Receive every `session/update` notification for the active session. Also
254
+ * per-turn — it routes to the running step's event bridge, so a reused
255
+ * session's output is attributed to the step that actually asked for it.
256
+ */
257
+ onSessionUpdate: (notification: acp.SessionNotification) => void;
258
+ /**
259
+ * Receive every `fs/*` and `terminal/*` call the agent makes, allowed or
260
+ * refused. Per-turn like {@link onSessionUpdate}, so activity is attributed
261
+ * to the step that actually caused it.
262
+ */
263
+ onClientActivity?: (activity: AcpClientActivity) => void;
264
+ }
265
+ /**
266
+ * Why the agent stopped a turn. Derived from `PromptResponse` because the
267
+ * protocol package inlines the union rather than exporting it by name.
268
+ * @public
269
+ */
270
+ export type AcpStopReason = acp.PromptResponse['stopReason'];
271
+ /** @public Input for one `session/prompt` turn. */
272
+ export interface AcpPromptOptions {
273
+ /**
274
+ * Prompt content blocks. Validated against the agent's advertised
275
+ * `promptCapabilities` before sending — the spec requires clients to restrict
276
+ * content types to what the agent accepts.
277
+ */
278
+ content: ReadonlyArray<acp.ContentBlock>;
279
+ /** Aborting this signal issues `session/cancel` and awaits the `cancelled` stop reason. */
280
+ signal?: AbortSignal;
281
+ }
282
+ /**
283
+ * Result of a completed turn. The interpreter appends `items` to the item log,
284
+ * charges `usage`/`cost`, records `stopReason`, and returns `text` (or parses it
285
+ * through the step's output schema).
286
+ * @public
287
+ */
288
+ export interface AcpTurnResult {
289
+ /** Why the agent stopped — drives the interpreter's success/error decision. */
290
+ stopReason: AcpStopReason;
291
+ /** Noetic Items produced this turn (assistant message, tool calls, tool results). */
292
+ items: Item[];
293
+ /** Concatenated assistant text of the turn. */
294
+ text: string;
295
+ usage?: TokenUsage;
296
+ cost?: number;
297
+ /** Plan entries reported via `plan` updates, latest snapshot wins. */
298
+ plan?: ReadonlyArray<acp.PlanEntry>;
299
+ /** Slash commands the agent advertised during the turn. */
300
+ availableCommands?: ReadonlyArray<acp.AvailableCommand>;
301
+ /** Mode the session ended the turn in, when the agent reports modes. */
302
+ currentModeId?: string;
303
+ }
304
+ /** @public Options for creating a session. */
305
+ export interface AcpNewSessionOptions {
306
+ /** Absolute working directory for the session. */
307
+ cwd: string;
308
+ /** MCP servers the agent should connect to, gated by its `mcpCapabilities`. */
309
+ mcpServers?: ReadonlyArray<acp.McpServer>;
310
+ }
311
+ /** @public Options for resuming a previously created session via `session/load`. */
312
+ export interface AcpLoadSessionOptions extends AcpNewSessionOptions {
313
+ sessionId: string;
314
+ }
315
+ /**
316
+ * One ACP session: a conversation with its own history and state, living on the
317
+ * far side of a connection.
318
+ * @public
319
+ */
320
+ export interface AcpSession {
321
+ readonly sessionId: string;
322
+ /** Mode state when the agent supports session modes. */
323
+ readonly modes?: acp.SessionModeState;
324
+ /** Slash commands currently offered, refreshed by `available_commands_update`. */
325
+ readonly availableCommands: ReadonlyArray<acp.AvailableCommand>;
326
+ /** Run one prompt turn to completion. */
327
+ prompt(opts: AcpPromptOptions): Promise<AcpTurnResult>;
328
+ /** Send `session/cancel` for an in-flight turn. */
329
+ cancel(): Promise<void>;
330
+ /** Switch mode. Throws when the agent advertised no modes. */
331
+ setMode(modeId: string): Promise<void>;
332
+ /** Switch model. Throws when the agent advertised no models. */
333
+ setModel(modelId: string): Promise<void>;
334
+ }
335
+ /**
336
+ * A negotiated connection to an ACP agent — one `initialize` handshake and one
337
+ * transport, able to carry several sessions.
338
+ * @public
339
+ */
340
+ export interface AcpAgentConnection {
341
+ /** Everything the agent advertised during `initialize`. */
342
+ readonly agentCapabilities: acp.AgentCapabilities;
343
+ /** Authentication methods the agent offers, empty when it needs none. */
344
+ readonly authMethods: ReadonlyArray<acp.AuthMethod>;
345
+ /** Protocol version agreed with the agent. */
346
+ readonly protocolVersion: number;
347
+ /** Run the `authenticate` flow for one of {@link authMethods}. */
348
+ authenticate(methodId: string): Promise<void>;
349
+ /** Create a session (`session/new`). */
350
+ newSession(opts: AcpNewSessionOptions): Promise<AcpSession>;
351
+ /**
352
+ * Resume a session (`session/load`). Throws {@link AcpCapabilityError} when
353
+ * the agent did not advertise `loadSession`.
354
+ */
355
+ loadSession(opts: AcpLoadSessionOptions): Promise<AcpSession>;
356
+ /** Close the connection and release the transport. */
357
+ close(): Promise<void>;
358
+ }
359
+ /**
360
+ * A live connection plus the session running on it. The runtime keeps these
361
+ * keyed by {@link AcpSessionPolicy.reuse} so a session can span several steps.
362
+ * @public
363
+ */
364
+ export interface AcpLiveSession {
365
+ connection: AcpAgentConnection;
366
+ session: AcpSession;
367
+ /**
368
+ * The host this connection was opened with. The runtime rebinds its
369
+ * per-turn fields before every turn, so a session shared by several steps
370
+ * honours each step's own permissions, steering, and event stream.
371
+ */
372
+ host: AcpClientHost;
373
+ /** `agentId` of the adapter that opened this connection, for reuse conflict checks. */
374
+ agentId: string;
375
+ /** How long this connection is kept alive; decides who closes it and when. */
376
+ keepAlive: AcpKeepAlive;
377
+ /** Working directory the session was created in — fixed for its lifetime. */
378
+ cwd: string;
379
+ /** MCP servers attached at creation — also fixed for its lifetime. */
380
+ mcpServers?: ReadonlyArray<acp.McpServer>;
381
+ }
382
+ /** @public Options handed to {@link AcpAgent.connect}. */
383
+ export interface AcpConnectOptions {
384
+ /** The runtime surface backing client-side ACP methods. */
385
+ host: AcpClientHost;
386
+ signal?: AbortSignal;
387
+ }
388
+ /**
389
+ * An ACP agent adapter. `@noetic-tools/acp` exports presets (`claudeCode()`,
390
+ * `codex()`, `gemini()`, `customAcpAgent()`) that return one of these.
391
+ * @public
392
+ */
393
+ export interface AcpAgent {
394
+ /** Spec tag for forward-compatibility. */
395
+ readonly specificationVersion: 'acp-v1';
396
+ /**
397
+ * Stable identifier for the agent (`'claude-code'`, `'codex'`, …). Free-form:
398
+ * the set of agents is open, and this is only used for registry lookup,
399
+ * observability, and error messages.
400
+ */
401
+ readonly agentId: string;
402
+ /** Open a transport, run `initialize`, and return the negotiated connection. */
403
+ connect(opts: AcpConnectOptions): Promise<AcpAgentConnection>;
404
+ }
405
+ /**
406
+ * Controls how an ACP connection is reused and torn down across steps.
407
+ * @public
408
+ */
409
+ /**
410
+ * How long an ACP connection outlives the step that opened it.
411
+ *
412
+ * A connection owns a live agent — usually a child process whose stdio keeps
413
+ * the event loop alive — so keeping one is never inferred. Every level beyond
414
+ * the default is something the step asks for explicitly.
415
+ * @public
416
+ */
417
+ export declare const AcpKeepAlive: {
418
+ /** Closed when the step finishes. The default. */
419
+ readonly Step: 'step';
420
+ /** Kept for the rest of the root run, then closed by the harness. */
421
+ readonly Run: 'run';
422
+ /** Kept until {@link AcpSessionDisposer.closeAcpSessions} is called. Nothing closes it for you. */
423
+ readonly Harness: 'harness';
424
+ };
425
+ /** @public Union of connection keep-alive scopes. */
426
+ export type AcpKeepAlive = (typeof AcpKeepAlive)[keyof typeof AcpKeepAlive];
427
+ /**
428
+ * Controls how an ACP connection is shared and how long it is kept alive.
429
+ * @public
430
+ */
431
+ export interface AcpSessionPolicy {
432
+ /**
433
+ * Share one live connection + session under this id, so later steps take
434
+ * their turns against the same agent. Requires {@link keepAlive} `'run'` or
435
+ * `'harness'` — a connection closed at the end of its step has nothing left
436
+ * to share.
437
+ */
438
+ reuse?: string;
439
+ /**
440
+ * How long to keep the connection alive. Defaults to `'step'`: the
441
+ * connection is closed as soon as the step finishes, so nothing is
442
+ * maintained unless the step says so.
443
+ *
444
+ * `'harness'` hands ownership to the caller — the runtime will not close it,
445
+ * and an undisposed connection keeps the host process from exiting. Call
446
+ * `harness.closeAcpSessions()` when the work is done.
447
+ */
448
+ keepAlive?: AcpKeepAlive;
449
+ /**
450
+ * Resume this ACP session id via `session/load` instead of creating a new one.
451
+ * Requires the agent's `loadSession` capability.
452
+ */
453
+ load?: string;
454
+ }
455
+ /**
456
+ * A snapshot of one live ACP sub-agent — what a UI needs to show it and offer
457
+ * actions on it, without handing out the connection itself.
458
+ * @public
459
+ */
460
+ export interface AcpSessionInfo {
461
+ /** Opaque handle for this session — pass it back to `getAcpSession`/`cancelAcpSession`. */
462
+ key: string;
463
+ /** The `session.reuse` key the step named, when it named one. */
464
+ reuseKey?: string;
465
+ /** `agentId` of the adapter driving it. */
466
+ agentId: string;
467
+ /** The agent's own session id. */
468
+ sessionId: string;
469
+ keepAlive: AcpKeepAlive;
470
+ /** Current mode, when the agent supports modes. */
471
+ currentModeId?: string;
472
+ /** Modes the agent offers, for a mode switcher. */
473
+ availableModes: ReadonlyArray<acp.SessionMode>;
474
+ /** Slash commands the agent currently advertises. */
475
+ availableCommands: ReadonlyArray<acp.AvailableCommand>;
476
+ }
477
+ /**
478
+ * The disposal surface for connections kept past their run. Implemented by
479
+ * `AgentHarness`; declared here so callers can hold the capability without
480
+ * depending on the concrete runtime.
481
+ * @public
482
+ */
483
+ export interface AcpSessionDisposer {
484
+ /** Close every ACP connection this harness still holds. Idempotent. */
485
+ closeAcpSessions(): Promise<void>;
486
+ }
487
+ /**
488
+ * Thrown when an ACP agent is asked for a capability it did not advertise
489
+ * during `initialize` — loading a session, switching mode, sending image
490
+ * content. Raised before anything reaches the wire.
491
+ * @public
492
+ */
493
+ export declare class AcpCapabilityError extends Error {
494
+ readonly agentId: string;
495
+ readonly capability: string;
496
+ constructor(opts: {
497
+ agentId: string;
498
+ capability: string;
499
+ message?: string;
500
+ });
501
+ }
502
+ /** @public Type guard for {@link AcpCapabilityError}. */
503
+ export declare function isAcpCapabilityError(e: unknown): e is AcpCapabilityError;
504
+ /**
505
+ * Thrown when a connection cannot be established — the agent binary is missing,
506
+ * the handshake fails, or authentication is required and unsatisfied.
507
+ * @public
508
+ */
509
+ export declare class AcpConnectError extends Error {
510
+ readonly agentId: string;
511
+ readonly connectCause?: unknown;
512
+ constructor(opts: {
513
+ agentId: string;
514
+ message: string;
515
+ cause?: unknown;
516
+ });
517
+ }
518
+ /** @public Type guard for {@link AcpConnectError}. */
519
+ export declare function isAcpConnectError(e: unknown): e is AcpConnectError;
520
+ /**
521
+ * The ACP protocol surface, re-exported so consumers get the wire types without
522
+ * taking a direct dependency on the protocol package.
523
+ * @public
524
+ */
525
+ export type { AgentCapabilities as AcpAgentCapabilities, AuthMethod as AcpAuthMethod, AvailableCommand as AcpAvailableCommand, ContentBlock as AcpContentBlock, McpServer as AcpMcpServer, PermissionOption as AcpPermissionOption, PlanEntry as AcpPlanEntry, PromptCapabilities as AcpPromptCapabilities, RequestPermissionRequest as AcpRequestPermissionRequest, SessionMode as AcpSessionMode, SessionModeState as AcpSessionModeState, SessionNotification as AcpSessionNotification, ToolCallContent as AcpToolCallContent, ToolCallStatus as AcpToolCallStatus, ToolKind as AcpToolKind, } from '@zed-industries/agent-client-protocol';
@@ -0,0 +1,101 @@
1
+ /**
2
+ * Agent Client Protocol (ACP) contract.
3
+ *
4
+ * Noetic drives external coding agents — Claude Code, Codex, Gemini CLI, any
5
+ * ACP-speaking binary — as an **ACP Client**. The agent is a JSON-RPC 2.0 peer
6
+ * reached over a duplex byte stream, so there is no vendor SDK per agent and no
7
+ * closed set of supported agents.
8
+ *
9
+ * This file is the direct analogue of the `ContextLayer` contract: defined in
10
+ * the foundation package so both `@noetic-tools/core` (which executes ACP steps)
11
+ * and `@noetic-tools/acp` (which implements the protocol) depend on it without
12
+ * forming a cycle. Core calls only the methods declared here — the ACP library's
13
+ * `ClientSideConnection` never enters core's dependency graph.
14
+ *
15
+ * Protocol types are re-used verbatim from `@zed-industries/agent-client-protocol`
16
+ * (spec version 1) rather than mirrored, so the wire surface cannot drift from
17
+ * the specification.
18
+ *
19
+ * @see https://agentclientprotocol.com/
20
+ */
21
+ //#region Step-kind discriminator
22
+ /**
23
+ * The single step kind backing every ACP agent. Unlike the closed harness enum
24
+ * it replaces, the *agent* is identified by a free-form `agentId` on the
25
+ * adapter — so adding an agent never touches core or the published JSON Schema.
26
+ * @public
27
+ */
28
+ export const ACP_AGENT_STEP_KIND = 'acp-agent';
29
+ //#endregion
30
+ //#region Permissions
31
+ /** @public How a permission request was resolved. */
32
+ export const AcpPermissionDecision = {
33
+ Allow: 'allow',
34
+ Deny: 'deny',
35
+ Cancel: 'cancel',
36
+ };
37
+ //#endregion
38
+ //#region Session policy
39
+ /**
40
+ * Controls how an ACP connection is reused and torn down across steps.
41
+ * @public
42
+ */
43
+ /**
44
+ * How long an ACP connection outlives the step that opened it.
45
+ *
46
+ * A connection owns a live agent — usually a child process whose stdio keeps
47
+ * the event loop alive — so keeping one is never inferred. Every level beyond
48
+ * the default is something the step asks for explicitly.
49
+ * @public
50
+ */
51
+ export const AcpKeepAlive = {
52
+ /** Closed when the step finishes. The default. */
53
+ Step: 'step',
54
+ /** Kept for the rest of the root run, then closed by the harness. */
55
+ Run: 'run',
56
+ /** Kept until {@link AcpSessionDisposer.closeAcpSessions} is called. Nothing closes it for you. */
57
+ Harness: 'harness',
58
+ };
59
+ //#endregion
60
+ //#region Errors
61
+ /**
62
+ * Thrown when an ACP agent is asked for a capability it did not advertise
63
+ * during `initialize` — loading a session, switching mode, sending image
64
+ * content. Raised before anything reaches the wire.
65
+ * @public
66
+ */
67
+ export class AcpCapabilityError extends Error {
68
+ agentId;
69
+ capability;
70
+ constructor(opts) {
71
+ super(opts.message ??
72
+ `ACP agent '${opts.agentId}' did not advertise the '${opts.capability}' capability.`);
73
+ this.name = 'AcpCapabilityError';
74
+ this.agentId = opts.agentId;
75
+ this.capability = opts.capability;
76
+ }
77
+ }
78
+ /** @public Type guard for {@link AcpCapabilityError}. */
79
+ export function isAcpCapabilityError(e) {
80
+ return e instanceof AcpCapabilityError;
81
+ }
82
+ /**
83
+ * Thrown when a connection cannot be established — the agent binary is missing,
84
+ * the handshake fails, or authentication is required and unsatisfied.
85
+ * @public
86
+ */
87
+ export class AcpConnectError extends Error {
88
+ agentId;
89
+ connectCause;
90
+ constructor(opts) {
91
+ super(opts.message);
92
+ this.name = 'AcpConnectError';
93
+ this.agentId = opts.agentId;
94
+ this.connectCause = opts.cause;
95
+ }
96
+ }
97
+ /** @public Type guard for {@link AcpConnectError}. */
98
+ export function isAcpConnectError(e) {
99
+ return e instanceof AcpConnectError;
100
+ }
101
+ //#endregion
@@ -84,6 +84,12 @@ export interface StepMeta {
84
84
  usage?: RoundUsage;
85
85
  cost?: number;
86
86
  responseItems?: ReadonlyArray<Item>;
87
+ /**
88
+ * Why an ACP agent stopped its turn. Set only by `acp-agent` steps, and the
89
+ * only way a caller can tell a truncated turn (`max_tokens`,
90
+ * `max_turn_requests`) from a complete one — both return normally.
91
+ */
92
+ acpStopReason?: string;
87
93
  }
88
94
  /** @public Structured response returned by a model adapter after an LLM call. */
89
95
  export interface LLMResponse {
@@ -110,7 +110,7 @@ export interface Context<TContext = ContextData, TState = unknown> {
110
110
  *
111
111
  * Aborting rejects operations blocked on the context (channel `recv` waiters,
112
112
  * parked back-pressure senders) with `cancelled`, cuts short the in-flight
113
- * model call or sub-harness turn, and makes the next step boundary throw
113
+ * model call or ACP agent turn, and makes the next step boundary throw
114
114
  * `cancelled`. Context-layer teardown is NOT run — use
115
115
  * `harness.cancel(ctx, reason)` for that.
116
116
  */
@@ -5,7 +5,7 @@
5
5
  * `StepCallModel.output`: instead of JSON-parsing the assistant text once at turn
6
6
  * end, a codec is fed each text delta as it streams and produces a typed
7
7
  * value when the turn finishes. OpenUI Lang is one dialect; the contract
8
- * lives here — next to `ContextLayer` and `SubHarness` — so `core` and
8
+ * lives here — next to `ContextLayer` and `AcpAgent` — so `core` and
9
9
  * dialect packages both depend on it without forming a cycle.
10
10
  */
11
11
  /**
@@ -5,7 +5,7 @@
5
5
  * `StepCallModel.output`: instead of JSON-parsing the assistant text once at turn
6
6
  * end, a codec is fed each text delta as it streams and produces a typed
7
7
  * value when the turn finishes. OpenUI Lang is one dialect; the contract
8
- * lives here — next to `ContextLayer` and `SubHarness` — so `core` and
8
+ * lives here — next to `ContextLayer` and `AcpAgent` — so `core` and
9
9
  * dialect packages both depend on it without forming a cycle.
10
10
  */
11
11
  /**
@@ -308,7 +308,7 @@ export interface AgentHarnessContract<TParams extends Record<string, unknown> =
308
308
  * (`onComplete` with `outcome: 'aborted'`, then `dispose`) bottom-up.
309
309
  *
310
310
  * Blocked channel operations reject with `cancelled`, the in-flight model
311
- * call and sub-harness turn are cut short, and the next step boundary throws
311
+ * call and ACP agent turn are cut short, and the next step boundary throws
312
312
  * `cancelled`. Idempotent: cancelling an already-cancelled context is a
313
313
  * no-op. Use `ctx.abort()` when you want the signal without layer teardown.
314
314
  */