@zvada/agent-server 0.3.9 → 0.3.10
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/CHANGELOG.md +41 -0
- package/docs/consuming.md +110 -14
- package/docs/harnesses.md +8 -3
- package/package.json +1 -1
- package/src/core/agents/acp/acp-agent.ts +29 -29
- package/src/core/agents/base.ts +17 -8
- package/src/core/agents/claude-code/claude-agent.ts +71 -93
- package/src/core/agents/claude-code/generator-session.ts +65 -44
- package/src/core/agents/claude-code/session-manager.ts +20 -36
- package/src/core/agents/codex-app-server/codex-app-server-agent.ts +136 -35
- package/src/core/agents/codex-app-server/mcp.ts +139 -0
- package/src/core/agents/codex-sdk/codex-sdk-agent.ts +31 -57
- package/src/core/agents/mcp-config.ts +31 -0
- package/src/core/agents/session-agent.ts +91 -0
- package/src/core/agents/session-store.ts +6 -0
- package/src/core/diagnostics.ts +1 -8
- package/src/core/index.ts +2 -0
- package/src/core/presets.ts +1 -1
- package/src/core/runtime/agent-runtime.ts +8 -10
- package/src/core/runtime/event-processor.ts +10 -5
- package/src/core/utils/errors.ts +21 -0
- package/src/protocol/config.ts +1 -1
- package/src/protocol/errors.ts +11 -0
- package/src/protocol/lifecycle.ts +2 -1
- package/src/server/acp/binding.ts +12 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,46 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.3.10
|
|
4
|
+
|
|
5
|
+
- Support Codex app-server per-turn stdio and Streamable HTTP MCP configuration
|
|
6
|
+
in an explicitly isolated `CODEX_HOME`. Reuse unchanged connections, apply
|
|
7
|
+
credential changes before prompt submission, and remove omitted servers.
|
|
8
|
+
Verify effective settings before reload so repository overrides cannot
|
|
9
|
+
redirect supplied credentials; fail setup when requested tools are unavailable.
|
|
10
|
+
- Add the interactive CLI's explicit `--codex-home` path and preserve it for
|
|
11
|
+
continuation. Release the old logical session before `/new` reuses its home.
|
|
12
|
+
The ACP server binding rejects supplied Codex MCP configuration explicitly;
|
|
13
|
+
it has no session-specific home resource seam.
|
|
14
|
+
- Snapshot Claude MCP configuration so reusing and mutating a caller's server
|
|
15
|
+
map cannot hide credential changes or alter a queued configuration update.
|
|
16
|
+
- After host suspension, replace Claude and Codex app-server subprocesses
|
|
17
|
+
before the next prompt and strictly resume saved conversations. MCP reset
|
|
18
|
+
and unchanged-config reload can retain dead remote connections. Preserve
|
|
19
|
+
turn-admission receipts; ordinary warm turns still reuse the process.
|
|
20
|
+
CLI-owned stdio MCP processes and Claude host MCP instances are recreated.
|
|
21
|
+
- Fail closed when Claude, Codex, or ACP cannot resume saved context. Remove
|
|
22
|
+
automatic fresh-conversation retries, retain native targets across process
|
|
23
|
+
eviction and failed setup, and report missing context as `resume_failed`.
|
|
24
|
+
Starting a fresh conversation now requires an explicit new/closed session.
|
|
25
|
+
- Honor a different explicit Claude resume target on warm sessions; reject
|
|
26
|
+
mismatched Codex resume responses before accepting the replacement ID.
|
|
27
|
+
- Reject empty resume IDs. Share turn ownership and conversation preparation
|
|
28
|
+
across all built-in harnesses: overlapping turns on one logical session fail
|
|
29
|
+
before native setup or changes to saved context. Different sessions remain
|
|
30
|
+
parallel; custom harnesses retain their existing execution contract.
|
|
31
|
+
- Detach completed turns' cancellation signals so a late abort cannot interrupt
|
|
32
|
+
the next warm turn. Retain ownership until native cleanup finishes.
|
|
33
|
+
- Prevent Claude from creating an orphan query when release or shutdown occurs
|
|
34
|
+
during asynchronous startup configuration.
|
|
35
|
+
- Reject direct execution on built-in agents as soon as shutdown begins, before
|
|
36
|
+
starting native setup or creating another session.
|
|
37
|
+
- Preserve classified errors from the runtime in terminal events and client
|
|
38
|
+
results, including resume failures and overlapping turns. Cancellation cleanup
|
|
39
|
+
diagnostics no longer appear as terminal failures.
|
|
40
|
+
- Verify external MCP readiness on initial attachment and configuration changes,
|
|
41
|
+
including OAuth `needs-auth` states. Preserve early Claude resume errors when
|
|
42
|
+
startup MCP controls fail, and allow lazily initialized in-process SDK tools.
|
|
43
|
+
|
|
3
44
|
## 0.3.9
|
|
4
45
|
|
|
5
46
|
- Recover transient Claude MCP setup failures by reattaching with the current
|
package/docs/consuming.md
CHANGED
|
@@ -187,11 +187,25 @@ data disappears. A known type with a malformed body still fails loudly.
|
|
|
187
187
|
|
|
188
188
|
Store `state.nativeSessionId` (from `session.created`) keyed by your logical
|
|
189
189
|
`sessionId`, and pass it back as `config.resumeSessionId` to continue a
|
|
190
|
-
conversation later — across processes and machines
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
190
|
+
conversation later — across processes and machines where its native history is
|
|
191
|
+
available. Built-in harnesses fail the turn if the requested conversation is
|
|
192
|
+
missing, unsupported, or resolves to the wrong Codex thread. They never replay
|
|
193
|
+
the prompt on a new conversation. Missing or unsupported history reports
|
|
194
|
+
`error.category: "resume_failed"`; authentication and transport failures keep
|
|
195
|
+
their existing error categories.
|
|
196
|
+
|
|
197
|
+
Keep the saved ID when a turn fails. To retry after fixing the cause, submit a
|
|
198
|
+
new `turnId` with the same resume target. To deliberately start fresh, use a new
|
|
199
|
+
logical `sessionId`, or explicitly close the old session first. Do not overwrite
|
|
200
|
+
a saved native ID with an absent ID from a failed turn.
|
|
201
|
+
|
|
202
|
+
Within one engine, conversation identity survives process exit, idle eviction,
|
|
203
|
+
and failed setup. Changing the working directory or credential environment on
|
|
204
|
+
that logical session requires an explicit `resumeSessionId` or a new session.
|
|
205
|
+
Across engine restarts, callers still need to supply the persisted native ID.
|
|
206
|
+
A successful Claude resume can mint a new native ID; use the reported `resumed`
|
|
207
|
+
flag rather than comparing Claude IDs. The `resumed: false` wire value remains
|
|
208
|
+
readable for older engines and custom harnesses.
|
|
195
209
|
|
|
196
210
|
## Retry safely
|
|
197
211
|
|
|
@@ -203,9 +217,19 @@ identical request converges on the original execution — the wire acks it with
|
|
|
203
217
|
`TurnConflictError`). This is what makes at-least-once RPC layers (Durable
|
|
204
218
|
Object retries, queue redelivery) safe over the engine.
|
|
205
219
|
|
|
220
|
+
Built-in harnesses allow one active turn per logical session, including setup
|
|
221
|
+
and cancellation cleanup. Different sessions can run in parallel. Wait for the
|
|
222
|
+
current turn to end before starting another. Direct `agent.execute` rejects
|
|
223
|
+
overlap with `TurnActiveError`; embedded `runtime.run` completes the second
|
|
224
|
+
turn with `error.category: "invalid_request"` without starting native work or
|
|
225
|
+
changing its saved conversation. That result is memoized like other completed
|
|
226
|
+
turns, so retry after the first turn ends with a **new `turnId`**. The wire
|
|
227
|
+
rejects earlier with `turnActive`, before admission, so its rejected request
|
|
228
|
+
can reuse its turn ID. Custom harnesses retain their own concurrency contract.
|
|
229
|
+
|
|
206
230
|
## Cancel honestly
|
|
207
231
|
|
|
208
|
-
`turn/cancel`
|
|
232
|
+
`turn/cancel` accepts a `turnId` stamp — always pass
|
|
209
233
|
the id of the turn you mean, so a late cancel can never kill its successor
|
|
210
234
|
(a stale stamp returns `{outcome: "no_active_turn", activeTurnId}`). The
|
|
211
235
|
result is a single-outcome union: `cancelled` means the harness confirmed the
|
|
@@ -214,6 +238,10 @@ the `turn/start` quick-ack and the harness registering its abortable turn —
|
|
|
214
238
|
and the agent may still be running. On `unconfirmed`, report "stopping…" and
|
|
215
239
|
treat `turn.ended` as the source of truth, not the cancel response.
|
|
216
240
|
|
|
241
|
+
Embedded `runtime.cancel(harness, sessionId)` cancels the session's active
|
|
242
|
+
turn without a turn ID check. Prefer a per-turn `AbortSignal` when cancellation
|
|
243
|
+
can arrive late; a completed turn's signal is detached before the next turn.
|
|
244
|
+
|
|
217
245
|
## Verify the stream
|
|
218
246
|
|
|
219
247
|
`verifyStreamContract(events, {seqs?})` machine-checks a recorded stream
|
|
@@ -235,14 +263,77 @@ and unknown event/part types are forward-compat rather than violations. Pass
|
|
|
235
263
|
|
|
236
264
|
## Own your session resources
|
|
237
265
|
|
|
266
|
+
### Configure MCP between turns
|
|
267
|
+
|
|
268
|
+
For Claude and Codex app-server, `RunConfig.mcpServers` is the complete set of wire-configured
|
|
269
|
+
servers for that turn, not an incremental patch. Repeating an unchanged set
|
|
270
|
+
reuses the running process and its MCP connections. Changed entries are
|
|
271
|
+
applied through the harness's native configuration API before the prompt;
|
|
272
|
+
finishing a turn does not restart the agent or its servers. Omitting the map
|
|
273
|
+
on a later turn removes the previous wire-configured set, just like `{}`.
|
|
274
|
+
Claude's host `sdkMcpServers` remain attached. The native CLI may also load MCP servers
|
|
275
|
+
from settings or plugins; this map is not an exclusive tool allowlist.
|
|
276
|
+
|
|
277
|
+
The engine snapshots the supplied wire configuration, including headers,
|
|
278
|
+
before asynchronous setup. Credential rotation therefore works even if the
|
|
279
|
+
caller reuses and mutates its server map between turns. Apply changes between
|
|
280
|
+
turns and supply the current complete map on each turn. The explicit live
|
|
281
|
+
`setMcpServers` control does not establish separate persistent defaults: the
|
|
282
|
+
next turn's map takes precedence. External MCP servers enforce credential
|
|
283
|
+
expiry; ending a turn does not itself revoke an attached credential.
|
|
284
|
+
|
|
285
|
+
Codex supports stdio and Streamable HTTP servers; legacy SSE is rejected. Its
|
|
286
|
+
per-turn MCP configuration requires an existing, absolute, session-specific
|
|
287
|
+
`config.env.CODEX_HOME` directory. Create it with mode `0700`, retain it while
|
|
288
|
+
the conversation may resume, and delete it when that logical session is
|
|
289
|
+
released. It contains conversation history and MCP credentials. Never share
|
|
290
|
+
it between sessions or use the operator's normal Codex home. AGNT supplies
|
|
291
|
+
and owns these directories for its callers.
|
|
292
|
+
|
|
293
|
+
The repository's interactive CLI accepts `--codex-home=/absolute/private/path`
|
|
294
|
+
and remembers the path for `--continue`; the operator supplies and authenticates
|
|
295
|
+
that existing home. The ACP **server binding** (`createAcpAgentApp` /
|
|
296
|
+
`agent-server --acp --harness codex-app-server`) has no per-session environment
|
|
297
|
+
resource seam in this release. It rejects supplied Codex MCP servers explicitly
|
|
298
|
+
at `session/new` or `session/resume`. Use the engine or standard JSON-RPC API
|
|
299
|
+
with `config.env.CODEX_HOME` for that combination. This limitation is separate
|
|
300
|
+
from the engine's ACP **harness**, which forwards MCP to other ACP agents.
|
|
301
|
+
|
|
302
|
+
An explicitly supplied `CODEX_HOME` gives the engine ownership of that home's
|
|
303
|
+
user-layer `mcp_servers` table: each turn replaces that table, including
|
|
304
|
+
clearing it when the map is omitted. Without an explicit home or a per-turn
|
|
305
|
+
map, native Codex configuration continues to work normally. Repository and
|
|
306
|
+
plugin servers remain separate, but a higher-priority setting that changes a
|
|
307
|
+
requested server fails preparation before reload. Codex's `codex_apps` name
|
|
308
|
+
is reserved.
|
|
309
|
+
|
|
310
|
+
Codex writes changed configuration without reloading, verifies its effective
|
|
311
|
+
settings, then reloads MCP and checks readiness before submitting the prompt.
|
|
312
|
+
Servers still starting are awaited through native startup notifications, with
|
|
313
|
+
a bounded deadline; unavailable or failed servers fail preparation immediately.
|
|
314
|
+
Unchanged configuration is checked without reloading. A failed setup fails
|
|
315
|
+
the turn; an explicitly submitted next turn reapplies the configuration so a
|
|
316
|
+
previously failed native client can reconnect. No model turn or tool call is
|
|
317
|
+
automatically replayed.
|
|
318
|
+
|
|
238
319
|
### Resume a suspended host
|
|
239
320
|
|
|
240
321
|
After thawing a VM, call `runtime.invalidateMcpConnections()` before admitting
|
|
241
|
-
new turns.
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
322
|
+
new turns. Drain active turns before suspending the host. This embed-tier
|
|
323
|
+
lifecycle signal retains conversation state and turn-admission receipts.
|
|
324
|
+
Before the next prompt, the Claude and Codex app-server harnesses replace
|
|
325
|
+
retained subprocesses and strictly resume their saved conversations, using
|
|
326
|
+
that turn's current MCP credentials before reconnecting the tools.
|
|
327
|
+
Missing history fails with `resume_failed`; it never silently starts an empty
|
|
328
|
+
conversation. Ordinary warm turns still reuse the process.
|
|
329
|
+
|
|
330
|
+
A resumed VM can retain dead TCP sockets in Claude's process-wide HTTP pool;
|
|
331
|
+
recreating MCP clients does not clear that pool. Codex's MCP configuration
|
|
332
|
+
reload also retains stale connections when configuration is unchanged.
|
|
333
|
+
Process replacement recreates CLI-owned stdio MCP processes and invokes
|
|
334
|
+
Claude's host MCP factory again, as idle eviction already does. Keep durable
|
|
335
|
+
tool state outside those processes. Workspace files and external MCP servers
|
|
336
|
+
are unaffected. Other harnesses without this lifecycle hook are unaffected.
|
|
246
337
|
|
|
247
338
|
Claude MCP updates also repair a transient socket setup failure once. Recovery
|
|
248
339
|
removes and re-adds the affected connection: an identical SDK update can cache
|
|
@@ -254,13 +345,18 @@ turn before prompt submission. Tool calls and model turns are never replayed.
|
|
|
254
345
|
|
|
255
346
|
Register `onSessionEnd` (claude options) to release per-session resources —
|
|
256
347
|
BYOK proxy keys, recorders — instead of re-deriving termination from side
|
|
257
|
-
effects. Reasons: `idle` (idle-timeout eviction), `replaced` (config change
|
|
258
|
-
|
|
348
|
+
effects. Reasons: `idle` (idle-timeout eviction), `replaced` (config change or
|
|
349
|
+
host resume replaced the subprocess — usually respawns immediately with context kept),
|
|
259
350
|
`released` (explicit close), `shutdown`. On the wire, call `session/close`
|
|
260
351
|
when a logical session will not be resumed: it frees the replay buffer and
|
|
261
352
|
the harness-native state (until then, memory cost ≈ `bufferSize` events per
|
|
262
353
|
session).
|
|
263
354
|
|
|
355
|
+
When embedding, close idle sessions with `runtime.closeSession` and await its
|
|
356
|
+
completion before starting another turn on that ID. Await shutdown before
|
|
357
|
+
disposing the runtime. Cancellation acknowledgement alone does not mean the
|
|
358
|
+
old execution has finished cleaning up.
|
|
359
|
+
|
|
264
360
|
`session/close` also broadcasts `session.ended {reason: "released"}` — but
|
|
265
361
|
only to subscribers **connected at that moment**, because the replay log is
|
|
266
362
|
retired in the same operation. A subscriber that was detached does not get the
|
|
@@ -271,7 +367,7 @@ Treat `unknownSession` on replay as "session over", not as an error to retry.
|
|
|
271
367
|
## Listen to the engine's diagnostics
|
|
272
368
|
|
|
273
369
|
Pass `onDiagnostic` (registry/runtime/proxy options) and log what arrives:
|
|
274
|
-
`interruptTimeout`, `
|
|
370
|
+
`interruptTimeout`, `sinkError`, `proxyUpstreamAuth`.
|
|
275
371
|
These are the signals the engine deliberately does not fail turns over — a
|
|
276
372
|
product that doesn't surface them debugs blind.
|
|
277
373
|
|
package/docs/harnesses.md
CHANGED
|
@@ -31,12 +31,16 @@ warm multi-turn reuse, a unified `ThinkingLevel`, and normalized token usage.
|
|
|
31
31
|
| `resumeSessionId` | ✅ | ✅ | ✅ |
|
|
32
32
|
| `systemPromptAppend` | ✅ | ❌ (SDK has no field) | ✅ (`developerInstructions`) |
|
|
33
33
|
| `maxTurns` | ✅ | ❌ | ❌ |
|
|
34
|
-
| `mcpServers` | ✅ | ❌ (capability=false) |
|
|
34
|
+
| `mcpServers` | ✅ | ❌ (capability=false) | ✅ (stdio / Streamable HTTP; isolated `CODEX_HOME`) |
|
|
35
35
|
| `apiKey` / `env` | ✅ | ✅ | `env` ✅, `apiKey` via ambient CLI auth |
|
|
36
36
|
| `disableTools` | ✅ | (use read-only sandbox) | (use read-only sandbox) |
|
|
37
37
|
| `permissionRequests` | ✅ (`canUseTool`) | ❌ (sandbox is the gate) | ✅ (`on-request` approvals) |
|
|
38
38
|
| `includeRaw` | ✅ | ✅ | ✅ |
|
|
39
39
|
|
|
40
|
+
Codex per-turn MCP is supported through the engine/JSON-RPC API, AGNT, and the
|
|
41
|
+
repository CLI with an explicit `--codex-home`. The ACP server binding cannot
|
|
42
|
+
provide isolated homes and rejects this combination at session admission.
|
|
43
|
+
|
|
40
44
|
Consult `runtime.capabilities(harness)` before relying on a capability.
|
|
41
45
|
|
|
42
46
|
## Host-managed Codex authentication
|
|
@@ -116,8 +120,9 @@ provide the cache breakdown without an additional native usage report.
|
|
|
116
120
|
|
|
117
121
|
## Known limitations (roadmap)
|
|
118
122
|
|
|
119
|
-
- **MCP servers** are
|
|
120
|
-
|
|
123
|
+
- **MCP servers** are supported by Claude and Codex app-server. Codex SDK
|
|
124
|
+
passthrough remains unsupported. See [MCP configuration](consuming.md#configure-mcp-between-turns)
|
|
125
|
+
for Codex home ownership, supported transports, and live-update behavior.
|
|
121
126
|
- The **BYOK proxy** (`/core/proxy`) is a building block, not yet auto-wired
|
|
122
127
|
into the harnesses (they use ambient/explicit keys today).
|
|
123
128
|
- **Hook bridge** (PreToolUse/Stop decisions) is exposed for the claude
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zvada/agent-server",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.10",
|
|
4
4
|
"description": "Harness-agnostic agent execution engine: run Claude Code, Codex (SDK/CLI + app-server), and any ACP agent behind one interface with a normalized event stream, multi-turn sessions, and resume. Root export is the wire contract; /core, /server, /client are the seats.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -6,9 +6,10 @@ import type {
|
|
|
6
6
|
} from "@agentclientprotocol/sdk";
|
|
7
7
|
import type { AgentCapabilities, AgentInput, PermissionMode } from "../../../protocol/index.ts";
|
|
8
8
|
import { AsyncQueue } from "../../../protocol/index.ts";
|
|
9
|
+
import { SessionResumeError } from "../../utils/errors.ts";
|
|
9
10
|
import type { AgentExecuteOptions, PermissionRequestHandler, RawAgentEvent } from "../base.ts";
|
|
10
|
-
import { BaseAgent } from "../base.ts";
|
|
11
11
|
import { configFingerprint } from "../config-fingerprint.ts";
|
|
12
|
+
import { SessionAgent } from "../session-agent.ts";
|
|
12
13
|
import { SessionStore } from "../session-store.ts";
|
|
13
14
|
import { AcpClient, type AcpClientOptions, type AcpLaunch } from "./client.ts";
|
|
14
15
|
import {
|
|
@@ -22,9 +23,7 @@ import {
|
|
|
22
23
|
|
|
23
24
|
const CAPABILITIES: AgentCapabilities = {
|
|
24
25
|
multiTurn: true,
|
|
25
|
-
//
|
|
26
|
-
// survives depends on the target agent's `sessionCapabilities.resume`
|
|
27
|
-
// (falls back to a fresh session when unsupported).
|
|
26
|
+
// A requested resume fails if the target agent cannot load saved sessions.
|
|
28
27
|
sessionResume: true,
|
|
29
28
|
// v1 exposes model selection only through agent-defined config options;
|
|
30
29
|
// there is no portable model parameter to pass through yet.
|
|
@@ -135,7 +134,7 @@ export async function answerPermission(
|
|
|
135
134
|
* `session/prompt` *response*, so `execute` re-injects it into the raw stream
|
|
136
135
|
* as a synthetic `session/prompt_result` event for the adapter.
|
|
137
136
|
*/
|
|
138
|
-
export class AcpAgent extends
|
|
137
|
+
export class AcpAgent extends SessionAgent {
|
|
139
138
|
readonly harness = "acp" as const;
|
|
140
139
|
readonly capabilities = CAPABILITIES;
|
|
141
140
|
private readonly sessions = new SessionStore<AcpSession>();
|
|
@@ -163,21 +162,13 @@ export class AcpAgent extends BaseAgent {
|
|
|
163
162
|
});
|
|
164
163
|
|
|
165
164
|
const existing = this.sessions.get(options.sessionId);
|
|
166
|
-
|
|
165
|
+
const resumeTarget = options.resumeSessionId;
|
|
167
166
|
if (existing && !existing.client.closed) {
|
|
168
167
|
if (acpSessionCompatible(existing, options, launchFingerprint)) {
|
|
169
168
|
this.sessions.clearIdle(existing);
|
|
170
169
|
return existing;
|
|
171
170
|
}
|
|
172
|
-
|
|
173
|
-
// resume (mirrors the claude-code/codex restart policy).
|
|
174
|
-
if (
|
|
175
|
-
resumeTarget === undefined &&
|
|
176
|
-
existing.cwd === options.cwd &&
|
|
177
|
-
existing.envFingerprint === configFingerprint(options.env)
|
|
178
|
-
) {
|
|
179
|
-
resumeTarget = existing.acpSessionId;
|
|
180
|
-
}
|
|
171
|
+
|
|
181
172
|
this.sessions.close(options.sessionId);
|
|
182
173
|
}
|
|
183
174
|
|
|
@@ -210,20 +201,28 @@ export class AcpAgent extends BaseAgent {
|
|
|
210
201
|
);
|
|
211
202
|
if (signal.aborted) throw new Error("turn aborted");
|
|
212
203
|
const mcpServers = toAcpMcpServers(options.mcpServers);
|
|
213
|
-
if (resumeTarget
|
|
214
|
-
|
|
215
|
-
|
|
204
|
+
if (resumeTarget) {
|
|
205
|
+
if (!init.agentCapabilities?.sessionCapabilities?.resume) {
|
|
206
|
+
throw new SessionResumeError(
|
|
207
|
+
resumeTarget,
|
|
208
|
+
"the ACP agent does not support session/resume",
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
await client
|
|
212
|
+
.request(
|
|
216
213
|
"session/resume",
|
|
217
214
|
{ sessionId: resumeTarget, cwd: options.cwd, mcpServers },
|
|
218
215
|
SESSION_REQUEST_TIMEOUT_MS,
|
|
219
|
-
)
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
216
|
+
)
|
|
217
|
+
.catch((cause: unknown) => {
|
|
218
|
+
throw new SessionResumeError(
|
|
219
|
+
resumeTarget,
|
|
220
|
+
cause instanceof Error ? cause.message : String(cause),
|
|
221
|
+
{ cause },
|
|
222
|
+
);
|
|
223
|
+
});
|
|
224
|
+
acpSessionId = resumeTarget;
|
|
225
|
+
} else {
|
|
227
226
|
const created = await client.request(
|
|
228
227
|
"session/new",
|
|
229
228
|
{ cwd: options.cwd, mcpServers },
|
|
@@ -231,6 +230,7 @@ export class AcpAgent extends BaseAgent {
|
|
|
231
230
|
);
|
|
232
231
|
acpSessionId = created.sessionId;
|
|
233
232
|
}
|
|
233
|
+
signal.throwIfAborted();
|
|
234
234
|
if (!acpSessionId) throw new Error("acp agent returned no session id");
|
|
235
235
|
|
|
236
236
|
const session: AcpSession = {
|
|
@@ -254,16 +254,17 @@ export class AcpAgent extends BaseAgent {
|
|
|
254
254
|
}
|
|
255
255
|
}
|
|
256
256
|
|
|
257
|
-
async *
|
|
257
|
+
protected override async *executeTurn(
|
|
258
258
|
input: AgentInput,
|
|
259
259
|
options: AgentExecuteOptions,
|
|
260
|
+
controller: AbortController,
|
|
260
261
|
): AsyncIterableIterator<RawAgentEvent> {
|
|
261
|
-
const controller = this.trackTurn(options.sessionId, options.signal);
|
|
262
262
|
const queue = new AsyncQueue<RawAgentEvent>();
|
|
263
263
|
const unsubscribe: Array<() => void> = [];
|
|
264
264
|
let cancelTimer: ReturnType<typeof setTimeout> | undefined;
|
|
265
265
|
let session: AcpSession | undefined;
|
|
266
266
|
try {
|
|
267
|
+
controller.signal.throwIfAborted();
|
|
267
268
|
session = await this.resolveSession(options, controller.signal);
|
|
268
269
|
session.turn = {
|
|
269
270
|
broker: options.onPermissionRequest,
|
|
@@ -341,7 +342,6 @@ export class AcpAgent extends BaseAgent {
|
|
|
341
342
|
if (cancelTimer) clearTimeout(cancelTimer);
|
|
342
343
|
for (const off of unsubscribe) off();
|
|
343
344
|
if (session) session.turn = undefined;
|
|
344
|
-
this.endTurn(options.sessionId, controller);
|
|
345
345
|
if (session && !session.client.closed) {
|
|
346
346
|
this.sessions.armIdle(
|
|
347
347
|
options.sessionId,
|
package/src/core/agents/base.ts
CHANGED
|
@@ -66,8 +66,8 @@ export interface AgentExecuteOptions {
|
|
|
66
66
|
/**
|
|
67
67
|
* Called once with the harness-native session/thread id (for resume).
|
|
68
68
|
* Pass the harness's honest `resumed` judgment unconditionally — the
|
|
69
|
-
* runtime surfaces it only on turns that requested a resume
|
|
70
|
-
*
|
|
69
|
+
* runtime surfaces it only on turns that requested a resume. Built-in
|
|
70
|
+
* harnesses throw if a requested conversation cannot be loaded.
|
|
71
71
|
*/
|
|
72
72
|
onNativeSession?: (nativeSessionId: string, info?: { resumed?: boolean }) => void;
|
|
73
73
|
/** Engine-brokered approval round-trip (see PermissionRequestHandler). */
|
|
@@ -107,7 +107,7 @@ export interface Agent {
|
|
|
107
107
|
release?(sessionId: string): Promise<void>;
|
|
108
108
|
/** Mark retained MCP connections stale after host suspension; repair before the next turn. */
|
|
109
109
|
invalidateMcpConnections?(): void;
|
|
110
|
-
/** Tear down every live session (process shutdown). */
|
|
110
|
+
/** Tear down every live session (process shutdown). Built-ins reject subsequent turns. */
|
|
111
111
|
terminateAll(): Promise<void>;
|
|
112
112
|
}
|
|
113
113
|
|
|
@@ -122,6 +122,7 @@ export abstract class BaseAgent implements Agent {
|
|
|
122
122
|
* removes only the specific controller it created.
|
|
123
123
|
*/
|
|
124
124
|
protected readonly inflight = new Map<string, Set<AbortController>>();
|
|
125
|
+
private readonly unlinkAbort = new WeakMap<AbortController, () => void>();
|
|
125
126
|
|
|
126
127
|
abstract execute(
|
|
127
128
|
input: AgentInput,
|
|
@@ -162,16 +163,24 @@ export abstract class BaseAgent implements Agent {
|
|
|
162
163
|
set.add(controller);
|
|
163
164
|
if (external) {
|
|
164
165
|
if (external.aborted) controller.abort();
|
|
165
|
-
else
|
|
166
|
+
else {
|
|
167
|
+
const onAbort = () => controller.abort();
|
|
168
|
+
external.addEventListener("abort", onAbort, { once: true });
|
|
169
|
+
this.unlinkAbort.set(controller, () => external.removeEventListener("abort", onAbort));
|
|
170
|
+
}
|
|
166
171
|
}
|
|
167
172
|
return controller;
|
|
168
173
|
}
|
|
169
174
|
|
|
170
175
|
protected endTurn(sessionId: string, controller?: AbortController): void {
|
|
171
176
|
const set = this.inflight.get(sessionId);
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
177
|
+
// release/terminateAll may already have removed the set while the turn
|
|
178
|
+
// was draining. Its external signal must still be detached on completion.
|
|
179
|
+
for (const turn of controller ? [controller] : (set ?? [])) {
|
|
180
|
+
this.unlinkAbort.get(turn)?.();
|
|
181
|
+
this.unlinkAbort.delete(turn);
|
|
182
|
+
set?.delete(turn);
|
|
183
|
+
}
|
|
184
|
+
if (set?.size === 0) this.inflight.delete(sessionId);
|
|
176
185
|
}
|
|
177
186
|
}
|