@desplega.ai/agent-swarm 1.100.0 → 1.100.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/openapi.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "openapi": "3.1.0",
3
3
  "info": {
4
4
  "title": "Agent Swarm API",
5
- "version": "1.100.0",
5
+ "version": "1.100.1",
6
6
  "description": "Multi-agent orchestration API for Claude Code, Codex, and Gemini CLI. Enables task distribution, agent communication, and service discovery.\n\nMCP tools are documented separately in [MCP.md](./MCP.md)."
7
7
  },
8
8
  "servers": [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@desplega.ai/agent-swarm",
3
- "version": "1.100.0",
3
+ "version": "1.100.1",
4
4
  "description": "Multi-agent orchestration for Claude Code, Codex, Gemini CLI, and other AI coding assistants",
5
5
  "license": "MIT",
6
6
  "author": "desplega.sh <contact@desplega.sh>",
@@ -1451,7 +1451,7 @@ function setupShutdownHandlers(
1451
1451
  );
1452
1452
  for (const [taskId, task] of state.activeTasks) {
1453
1453
  console.log(`[${role}] Superseding task ${taskId.slice(0, 8)}`);
1454
- task.session.abort().catch(() => {});
1454
+ task.session.abort("graceful_shutdown").catch(() => {});
1455
1455
  if (apiConfig) {
1456
1456
  const supersede = await supersedeTaskViaAPI(
1457
1457
  apiConfig,
@@ -4706,7 +4706,7 @@ export async function runAgent(config: RunnerConfig, opts: RunnerOptions) {
4706
4706
  console.log(
4707
4707
  `[${role}] Task ${taskId.slice(0, 8)} was cancelled — sending SIGTERM to subprocess`,
4708
4708
  );
4709
- task.session.abort().catch(() => {});
4709
+ task.session.abort("cancelled").catch(() => {});
4710
4710
  cancelledSignaled.add(taskId);
4711
4711
  }
4712
4712
  }
@@ -519,9 +519,9 @@ export class CodexSession implements ProviderSession {
519
519
  return this.completionPromise;
520
520
  }
521
521
 
522
- async abort(): Promise<void> {
522
+ async abort(reason?: string): Promise<void> {
523
523
  this.aborted = true;
524
- this.abortController?.abort();
524
+ this.abortController?.abort(reason ?? "cancelled");
525
525
  }
526
526
 
527
527
  private emit(event: ProviderEvent): void {
@@ -992,6 +992,14 @@ export class CodexSession implements ProviderSession {
992
992
  } catch (err) {
993
993
  // AbortError from the SDK propagates here when signal.abort() fires.
994
994
  if (this.aborted || (err instanceof Error && err.name === "AbortError")) {
995
+ // Prefer the abort reason from the signal (set by the caller of
996
+ // abort()) — this distinguishes tool-loop aborts from cancel-poll
997
+ // and graceful-shutdown aborts that all used to produce a bare
998
+ // "cancelled" failureReason.
999
+ const abortReason =
1000
+ typeof this.abortController?.signal.reason === "string"
1001
+ ? this.abortController.signal.reason
1002
+ : "cancelled";
995
1003
  const cost = this.buildCostData(this.lastUsage, true);
996
1004
  this.emit({ type: "result", cost, isError: true, errorCategory: "cancelled" });
997
1005
  this.settle({
@@ -999,7 +1007,7 @@ export class CodexSession implements ProviderSession {
999
1007
  sessionId: this._sessionId,
1000
1008
  cost,
1001
1009
  isError: true,
1002
- failureReason: "cancelled",
1010
+ failureReason: abortReason,
1003
1011
  });
1004
1012
  return;
1005
1013
  }
@@ -126,7 +126,7 @@ export function createSwarmEventHandler(
126
126
  console.log(
127
127
  `[swarm-events] aborting task ${taskId}: cancelled via /cancelled-tasks poll`,
128
128
  );
129
- opts.abortRef.current?.abort();
129
+ opts.abortRef.current?.abort("cancelled");
130
130
  if (opts.onCancel) {
131
131
  try {
132
132
  await opts.onCancel();
@@ -152,10 +152,9 @@ export function createSwarmEventHandler(
152
152
  // was indistinguishable from a /cancelled-tasks abort or a runner
153
153
  // SIGTERM. `result.reason` already carries the diagnostic detail
154
154
  // ("Tool X called 15 times…", "ping-pong between A and B…").
155
- console.log(
156
- `[swarm-events] aborting task ${taskId}: tool-loop detected — ${result.reason ?? "unknown reason"}`,
157
- );
158
- opts.abortRef.current?.abort();
155
+ const loopReason = `tool-loop: ${result.reason ?? "unknown reason"}`;
156
+ console.log(`[swarm-events] aborting task ${taskId}: ${loopReason}`);
157
+ opts.abortRef.current?.abort(loopReason);
159
158
  }
160
159
  })
161
160
  .catch(() => {});
@@ -118,7 +118,7 @@ export interface ProviderSession {
118
118
  readonly sessionId: string | undefined;
119
119
  onEvent(listener: (event: ProviderEvent) => void): void;
120
120
  waitForCompletion(): Promise<ProviderResult>;
121
- abort(): Promise<void>;
121
+ abort(reason?: string): Promise<void>;
122
122
  }
123
123
 
124
124
  /** Result returned when a provider session completes. */
@@ -97,6 +97,30 @@ describe("createCodexSwarmEventHandler", () => {
97
97
  expect(controller.signal.aborted).toBe(true);
98
98
  });
99
99
 
100
+ test("sets abort signal reason to 'cancelled' on cancel-poll abort", async () => {
101
+ installFetchStub((url) => {
102
+ if (url.includes("/cancelled-tasks")) {
103
+ return new Response(
104
+ JSON.stringify({ cancelled: [{ id: "task-1", failureReason: "user request" }] }),
105
+ { status: 200 },
106
+ );
107
+ }
108
+ return new Response("{}", { status: 200 });
109
+ });
110
+ const controller = new AbortController();
111
+ const opts = buildOpts({ abortRef: { current: controller } });
112
+ const handler = createCodexSwarmEventHandler(opts);
113
+ handler({
114
+ type: "tool_start",
115
+ toolCallId: "call-1",
116
+ toolName: "bash",
117
+ args: { command: "sleep 9999" },
118
+ });
119
+ await new Promise((resolve) => setTimeout(resolve, 30));
120
+ expect(controller.signal.aborted).toBe(true);
121
+ expect(controller.signal.reason).toBe("cancelled");
122
+ });
123
+
100
124
  test("logs the abort reason when /cancelled-tasks reports the task", async () => {
101
125
  installFetchStub((url) => {
102
126
  if (url.includes("/cancelled-tasks")) {