@sweny-ai/core 0.1.36 → 0.1.38

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.
@@ -250,6 +250,105 @@ describe("ClaudeClient", () => {
250
250
  expect(result.status).toBe("failed");
251
251
  expect(result.data.error).toContain("Too many turns");
252
252
  });
253
+ // terminal_reason was added in @anthropic-ai/claude-agent-sdk v0.2.91.
254
+ // When the turn budget is exhausted the SDK now emits subtype='success'
255
+ // alongside terminal_reason='max_turns' instead of the old error subtype,
256
+ // so the claude.ts handler has to look at both fields or silently accept
257
+ // truncated output as a success.
258
+ describe("terminal_reason handling (claude-agent-sdk v0.2.91+)", () => {
259
+ it("returns failed when terminal_reason is max_turns", async () => {
260
+ mockQuery.mockReturnValueOnce(makeStream([
261
+ {
262
+ type: "result",
263
+ subtype: "success",
264
+ result: "partial output",
265
+ terminal_reason: "max_turns",
266
+ },
267
+ ]));
268
+ const client = new ClaudeClient();
269
+ const result = await client.run({ instruction: "x", context: {}, tools: [] });
270
+ expect(result.status).toBe("failed");
271
+ expect(result.data.error).toContain("max_turns");
272
+ expect(result.data.error).toContain("terminated early");
273
+ });
274
+ it("returns failed for any non-completed terminal_reason", async () => {
275
+ // Future SDK versions may add new terminal reasons (aborted_tools,
276
+ // blocking_limit, etc.) — treat anything that isn't 'completed' as a
277
+ // failure rather than assume it's recoverable.
278
+ mockQuery.mockReturnValueOnce(makeStream([
279
+ {
280
+ type: "result",
281
+ subtype: "success",
282
+ result: "partial",
283
+ terminal_reason: "aborted_tools",
284
+ },
285
+ ]));
286
+ const client = new ClaudeClient();
287
+ const result = await client.run({ instruction: "x", context: {}, tools: [] });
288
+ expect(result.status).toBe("failed");
289
+ expect(result.data.error).toContain("aborted_tools");
290
+ });
291
+ it("returns success when terminal_reason is 'completed'", async () => {
292
+ mockQuery.mockReturnValueOnce(makeStream([
293
+ {
294
+ type: "result",
295
+ subtype: "success",
296
+ result: '{"ok": true}',
297
+ terminal_reason: "completed",
298
+ },
299
+ ]));
300
+ const client = new ClaudeClient();
301
+ const result = await client.run({ instruction: "x", context: {}, tools: [] });
302
+ expect(result.status).toBe("success");
303
+ expect(result.data.ok).toBe(true);
304
+ });
305
+ it("returns success when terminal_reason is absent (pre-v0.2.91 SDK)", async () => {
306
+ // Old SDKs never set terminal_reason. The guard must not treat an
307
+ // absent field as a failure or every caller would break on downgrade.
308
+ mockQuery.mockReturnValueOnce(makeStream([
309
+ {
310
+ type: "result",
311
+ subtype: "success",
312
+ result: '{"done": true}',
313
+ // terminal_reason deliberately omitted
314
+ },
315
+ ]));
316
+ const client = new ClaudeClient();
317
+ const result = await client.run({ instruction: "x", context: {}, tools: [] });
318
+ expect(result.status).toBe("success");
319
+ expect(result.data.done).toBe(true);
320
+ });
321
+ it("preserves toolCalls when failing on terminal_reason", async () => {
322
+ // A max_turns failure should still surface any tool calls that
323
+ // happened before the budget ran out, so debuggers can see what
324
+ // Claude was trying to do.
325
+ mockQuery.mockReturnValueOnce(makeStream([
326
+ {
327
+ type: "assistant",
328
+ message: {
329
+ content: [
330
+ {
331
+ type: "tool_use",
332
+ name: "search_code",
333
+ input: { query: "foo" },
334
+ },
335
+ ],
336
+ },
337
+ },
338
+ {
339
+ type: "result",
340
+ subtype: "success",
341
+ result: "partial",
342
+ terminal_reason: "max_turns",
343
+ },
344
+ ]));
345
+ const client = new ClaudeClient();
346
+ const result = await client.run({ instruction: "x", context: {}, tools: [] });
347
+ expect(result.status).toBe("failed");
348
+ expect(result.toolCalls).toHaveLength(1);
349
+ expect(result.toolCalls[0].tool).toBe("search_code");
350
+ });
351
+ });
253
352
  it("passes model option to query", async () => {
254
353
  mockQuery.mockReturnValueOnce(makeStream([{ type: "result", subtype: "success", result: "ok" }]));
255
354
  const client = new ClaudeClient({ model: "claude-sonnet-4-20250514" });
package/dist/claude.js CHANGED
@@ -112,6 +112,19 @@ export class ClaudeClient {
112
112
  else if (message.type === "result") {
113
113
  const resultMsg = message;
114
114
  if (resultMsg.subtype === "success" && "result" in resultMsg) {
115
+ // terminal_reason was added in @anthropic-ai/claude-agent-sdk v0.2.91.
116
+ // When the turn budget is exhausted the SDK still emits subtype='success'
117
+ // but sets terminal_reason='max_turns'. Treat anything other than
118
+ // 'completed' (or absent) as a failure so callers are not silently fed
119
+ // incomplete output.
120
+ const terminalReason = resultMsg.terminal_reason;
121
+ if (terminalReason && terminalReason !== "completed") {
122
+ return {
123
+ status: "failed",
124
+ data: { error: `Claude query terminated early: ${terminalReason}` },
125
+ toolCalls,
126
+ };
127
+ }
115
128
  response = resultMsg.result;
116
129
  }
117
130
  else if ("errors" in resultMsg) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sweny-ai/core",
3
- "version": "0.1.36",
3
+ "version": "0.1.38",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "sweny": "./dist/cli/main.js"
@@ -48,7 +48,7 @@
48
48
  "test:integration": "vitest run --config vitest.integration.config.ts"
49
49
  },
50
50
  "dependencies": {
51
- "@anthropic-ai/claude-agent-sdk": "^0.2.84",
51
+ "@anthropic-ai/claude-agent-sdk": "^0.2.92",
52
52
  "@clack/prompts": "^1.2.0",
53
53
  "chalk": "^5",
54
54
  "commander": "^13",