@zvada/agent-server 0.3.10 → 0.3.11

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 CHANGED
@@ -1,5 +1,17 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.3.11
4
+
5
+ - Report a Claude turn stopped while a tool runs as `cancelled`. Claude Code
6
+ ends it with `error_during_execution` and the aborted step's stop reason
7
+ (`tool_use`), which was reported as an error; an interrupted turn that did
8
+ not succeed is now a cancellation whatever its stop reason.
9
+ - Confirm a Claude cancel at once while the CLI is still starting: terminate
10
+ the process instead of waiting for it to acknowledge an interrupt. A slow
11
+ sandbox startup reported `confirmed: false` after the 2-second interrupt
12
+ timeout, and a prompt the CLI had not yet dequeued could survive the
13
+ interrupt and run anyway. The next turn starts a new process.
14
+
3
15
  ## 0.3.10
4
16
 
5
17
  - Support Codex app-server per-turn stdio and Streamable HTTP MCP configuration
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zvada/agent-server",
3
- "version": "0.3.10",
3
+ "version": "0.3.11",
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",
@@ -240,7 +240,7 @@ export class ClaudeCodeTransformer implements EventTransformer<unknown> {
240
240
  private error?: string;
241
241
  private interrupted = false;
242
242
  private execFailure = false;
243
- private sawResult = false;
243
+ private succeeded = false;
244
244
 
245
245
  constructor(ctx: { sessionId: string }) {
246
246
  this.ctx = { sessionId: ctx.sessionId, messageId: "" };
@@ -281,17 +281,19 @@ export class ClaudeCodeTransformer implements EventTransformer<unknown> {
281
281
  }
282
282
 
283
283
  finish(): TransformResult {
284
- // error_during_execution + null stop_reason is cancellation ONLY when the
285
- // agent confirmed an interrupt; otherwise it is a real failure (a silent
286
- // "cancelled" here is how a bad resume id used to vanish without a trace).
287
- // A confirmed interrupt with NO result at all (aborted before the SDK
288
- // emitted one) is also a cancellation, not a completed turn.
289
- const cancelled = this.interrupted && (this.execFailure || !this.sawResult);
290
- const error =
291
- this.error ??
292
- (this.execFailure && !this.interrupted
293
- ? "Claude turn failed during execution (error_during_execution — e.g. an invalid resumeSessionId fails this way)"
294
- : undefined);
284
+ // An interrupted turn that did not succeed was cancelled. Claude ends it with
285
+ // error_during_execution and the stop reason of the step it aborted (null
286
+ // while streaming, "tool_use" while a tool runs), or with no result when
287
+ // the process was stopped first. Without an interrupt, error_during_execution
288
+ // is a real failure (a silent "cancelled" here is how a bad resume id used
289
+ // to vanish without a trace).
290
+ const cancelled = this.interrupted && !this.succeeded;
291
+ const error = cancelled
292
+ ? undefined
293
+ : (this.error ??
294
+ (this.execFailure
295
+ ? "Claude turn failed during execution (error_during_execution — e.g. an invalid resumeSessionId fails this way)"
296
+ : undefined));
295
297
  return {
296
298
  usage: this.usage,
297
299
  ...(this.reportedModels.size && { reportedModels: [...this.reportedModels] }),
@@ -503,7 +505,7 @@ export class ClaudeCodeTransformer implements EventTransformer<unknown> {
503
505
  }
504
506
 
505
507
  private captureResult(msg: Extract<ClaudeMessage, { type: "result" }>): AdapterEvent[] {
506
- this.sawResult = true;
508
+ this.succeeded = msg.subtype === "success";
507
509
  if (msg.usage) {
508
510
  const creation = msg.usage.cache_creation;
509
511
  this.usage = {
@@ -528,7 +530,8 @@ export class ClaudeCodeTransformer implements EventTransformer<unknown> {
528
530
  if (msg.subtype === "error_during_execution" && msg.stop_reason === null) {
529
531
  // Ambiguous shape: an interrupt AND an execution failure (e.g. a bad
530
532
  // resume id) both surface this way. The agent's `turn_interrupted`
531
- // marker (which arrives after this message) disambiguates in finish().
533
+ // marker (which arrives after this message) disambiguates in finish(),
534
+ // as it does for an interrupt that aborted a tool (stop reason "tool_use").
532
535
  this.execFailure = true;
533
536
  } else if (msg.subtype === "error_max_turns") {
534
537
  // Hitting the turn budget is a stop condition, not a failure.
@@ -122,6 +122,8 @@ export class ClaudeGeneratorSession {
122
122
  private readonly failedMcpServers = new Set<string>();
123
123
  private processInvalidated = false;
124
124
  private nativeSessionId: string | null = null;
125
+ /** Set by the CLI's first message; until then it has begun no turn. */
126
+ private cliStarted = false;
125
127
  private startupError: unknown;
126
128
  /** SDK cost is cumulative within this query; token usage is already per turn. */
127
129
  private cumulativeCost: number | undefined = 0;
@@ -375,9 +377,17 @@ export class ClaudeGeneratorSession {
375
377
  * SDK acknowledged it; false when the round-trip timed out or failed — the
376
378
  * subprocess may still be running its turn and the caller must report that
377
379
  * honestly instead of assuming a clean cancel.
380
+ *
381
+ * A session still starting, or whose CLI has sent nothing yet, is terminated
382
+ * instead: it has begun no turn, an interrupt would wait out its startup, and
383
+ * a prompt the CLI has not dequeued survives an interrupt and runs anyway.
378
384
  */
379
385
  async interruptTurn(timeoutMs = 2000): Promise<boolean> {
380
- if ((this.state !== "busy" && this.state !== "starting") || !this.query) return true;
386
+ if (this.state === "idle" || this.state === "terminated") return true;
387
+ if (this.state === "starting" || !this.cliStarted || !this.query) {
388
+ await this.terminate();
389
+ return true;
390
+ }
381
391
  let timer: ReturnType<typeof setTimeout> | undefined;
382
392
  try {
383
393
  await Promise.race([
@@ -422,6 +432,7 @@ export class ClaudeGeneratorSession {
422
432
  if (!this.query) return;
423
433
  try {
424
434
  for await (const event of this.query) {
435
+ this.cliStarted = true;
425
436
  if (event.type === "system" && event.subtype === "init" && event.session_id) {
426
437
  this.nativeSessionId = event.session_id;
427
438
  }