@clinebot/agents 0.0.0 → 0.0.3

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.
@@ -84,6 +84,7 @@ export type TeamEvent =
84
84
  agentId: string;
85
85
  result?: AgentResult;
86
86
  error?: Error;
87
+ messages?: AgentResult["messages"];
87
88
  }
88
89
  | { type: TeamMessageType.AgentEvent; agentId: string; event: AgentEvent }
89
90
  | {
@@ -242,7 +243,12 @@ export class AgentTeam {
242
243
  return result;
243
244
  } catch (error) {
244
245
  const err = error instanceof Error ? error : new Error(String(error));
245
- this.emitEvent({ type: TeamMessageType.TaskEnd, agentId, error: err });
246
+ this.emitEvent({
247
+ type: TeamMessageType.TaskEnd,
248
+ agentId,
249
+ error: err,
250
+ messages: agent.getMessages(),
251
+ });
246
252
  throw error;
247
253
  }
248
254
  }
@@ -268,7 +274,12 @@ export class AgentTeam {
268
274
  return result;
269
275
  } catch (error) {
270
276
  const err = error instanceof Error ? error : new Error(String(error));
271
- this.emitEvent({ type: TeamMessageType.TaskEnd, agentId, error: err });
277
+ this.emitEvent({
278
+ type: TeamMessageType.TaskEnd,
279
+ agentId,
280
+ error: err,
281
+ messages: agent.getMessages(),
282
+ });
272
283
  throw error;
273
284
  }
274
285
  }
@@ -323,6 +334,7 @@ export class AgentTeam {
323
334
  type: TeamMessageType.TaskEnd,
324
335
  agentId: task.agentId,
325
336
  error: err,
337
+ messages: agent.getMessages(),
326
338
  });
327
339
  return {
328
340
  agentId: task.agentId,
@@ -384,6 +396,7 @@ export class AgentTeam {
384
396
  type: TeamMessageType.TaskEnd,
385
397
  agentId: task.agentId,
386
398
  error: err,
399
+ messages: agent.getMessages(),
387
400
  });
388
401
  results.push({
389
402
  agentId: task.agentId,
@@ -462,7 +475,12 @@ export class AgentTeam {
462
475
  }
463
476
  } catch (error) {
464
477
  const err = error instanceof Error ? error : new Error(String(error));
465
- this.emitEvent({ type: TeamMessageType.TaskEnd, agentId, error: err });
478
+ this.emitEvent({
479
+ type: TeamMessageType.TaskEnd,
480
+ agentId,
481
+ error: err,
482
+ messages: agent.getMessages(),
483
+ });
466
484
  results.push({
467
485
  agentId,
468
486
  result: undefined as unknown as AgentResult,
@@ -733,6 +751,9 @@ export interface TeamRunRecord {
733
751
  endedAt?: Date;
734
752
  leaseOwner?: string;
735
753
  heartbeatAt?: Date;
754
+ lastProgressAt?: Date;
755
+ lastProgressMessage?: string;
756
+ currentActivity?: string;
736
757
  result?: AgentResult;
737
758
  error?: string;
738
759
  }
@@ -1274,7 +1295,12 @@ export class AgentTeamsRuntime {
1274
1295
  return result;
1275
1296
  } catch (error) {
1276
1297
  const err = error instanceof Error ? error : new Error(String(error));
1277
- this.emitEvent({ type: TeamMessageType.TaskEnd, agentId, error: err });
1298
+ this.emitEvent({
1299
+ type: TeamMessageType.TaskEnd,
1300
+ agentId,
1301
+ error: err,
1302
+ messages: member.agent.getMessages(),
1303
+ });
1278
1304
  this.appendMissionLog({
1279
1305
  agentId,
1280
1306
  taskId: options?.taskId,
@@ -1316,6 +1342,9 @@ export class AgentTeamsRuntime {
1316
1342
  startedAt: new Date(0),
1317
1343
  leaseOwner: options?.leaseOwner,
1318
1344
  heartbeatAt: undefined,
1345
+ lastProgressAt: new Date(),
1346
+ lastProgressMessage: "queued",
1347
+ currentActivity: "queued",
1319
1348
  };
1320
1349
  this.runs.set(runId, record);
1321
1350
  this.runQueue.push(runId);
@@ -1369,18 +1398,14 @@ export class AgentTeamsRuntime {
1369
1398
  run.status = "running";
1370
1399
  run.startedAt = new Date();
1371
1400
  run.heartbeatAt = new Date();
1401
+ run.currentActivity = "run_started";
1372
1402
  this.emitEvent({ type: TeamMessageType.RunStarted, run: { ...run } });
1373
1403
 
1374
1404
  const heartbeatTimer = setInterval(() => {
1375
1405
  if (run.status !== "running") {
1376
1406
  return;
1377
1407
  }
1378
- run.heartbeatAt = new Date();
1379
- this.emitEvent({
1380
- type: TeamMessageType.RunProgress,
1381
- run: { ...run },
1382
- message: "heartbeat",
1383
- });
1408
+ this.recordRunProgress(run, "heartbeat");
1384
1409
  }, 2000);
1385
1410
 
1386
1411
  try {
@@ -1391,6 +1416,7 @@ export class AgentTeamsRuntime {
1391
1416
  run.status = "completed";
1392
1417
  run.result = result;
1393
1418
  run.endedAt = new Date();
1419
+ run.currentActivity = "completed";
1394
1420
  this.emitEvent({ type: TeamMessageType.RunCompleted, run: { ...run } });
1395
1421
  } catch (error) {
1396
1422
  const message =
@@ -1406,13 +1432,10 @@ export class AgentTeamsRuntime {
1406
1432
  Date.now() + Math.min(30000, 1000 * 2 ** run.retryCount),
1407
1433
  );
1408
1434
  this.runQueue.push(run.id);
1409
- this.emitEvent({
1410
- type: TeamMessageType.RunProgress,
1411
- run: { ...run },
1412
- message: `retry_scheduled_${run.retryCount}`,
1413
- });
1435
+ this.recordRunProgress(run, `retry_scheduled_${run.retryCount}`);
1414
1436
  } else {
1415
1437
  run.status = "failed";
1438
+ run.currentActivity = "failed";
1416
1439
  this.emitEvent({ type: TeamMessageType.RunFailed, run: { ...run } });
1417
1440
  }
1418
1441
  } finally {
@@ -1481,6 +1504,7 @@ export class AgentTeamsRuntime {
1481
1504
  run.status = "cancelled";
1482
1505
  run.error = reason;
1483
1506
  run.endedAt = new Date();
1507
+ run.currentActivity = "cancelled";
1484
1508
  const queueIndex = this.runQueue.indexOf(runId);
1485
1509
  if (queueIndex >= 0) {
1486
1510
  this.runQueue.splice(queueIndex, 1);
@@ -1502,6 +1526,7 @@ export class AgentTeamsRuntime {
1502
1526
  run.status = "interrupted";
1503
1527
  run.error = reason;
1504
1528
  run.endedAt = new Date();
1529
+ run.currentActivity = "interrupted";
1505
1530
  interrupted.push({ ...run });
1506
1531
  this.emitEvent({
1507
1532
  type: TeamMessageType.RunInterrupted,
@@ -1764,6 +1789,8 @@ export class AgentTeamsRuntime {
1764
1789
  }
1765
1790
 
1766
1791
  private trackMeaningfulEvent(agentId: string, event: AgentEvent): void {
1792
+ this.recordRunActivityFromAgentEvent(agentId, event);
1793
+
1767
1794
  if (event.type === "iteration_end" && event.hadToolCalls) {
1768
1795
  this.recordProgressStep(
1769
1796
  agentId,
@@ -1802,6 +1829,60 @@ export class AgentTeamsRuntime {
1802
1829
  }
1803
1830
  }
1804
1831
 
1832
+ private recordRunActivityFromAgentEvent(
1833
+ agentId: string,
1834
+ event: AgentEvent,
1835
+ ): void {
1836
+ let activity: string | undefined;
1837
+ switch (event.type) {
1838
+ case "iteration_start":
1839
+ activity = `iteration_${event.iteration}_started`;
1840
+ break;
1841
+ case "content_start":
1842
+ if (event.contentType === "tool") {
1843
+ activity = `running_tool_${event.toolName ?? "unknown"}`;
1844
+ }
1845
+ break;
1846
+ case "content_end":
1847
+ if (event.contentType === "tool") {
1848
+ activity = event.error
1849
+ ? `tool_${event.toolName ?? "unknown"}_error`
1850
+ : `finished_tool_${event.toolName ?? "unknown"}`;
1851
+ }
1852
+ break;
1853
+ case "done":
1854
+ activity = "finalizing_response";
1855
+ break;
1856
+ case "error":
1857
+ activity = "run_error";
1858
+ break;
1859
+ default:
1860
+ break;
1861
+ }
1862
+ if (!activity) {
1863
+ return;
1864
+ }
1865
+ for (const run of this.runs.values()) {
1866
+ if (run.agentId !== agentId || run.status !== "running") {
1867
+ continue;
1868
+ }
1869
+ this.recordRunProgress(run, activity);
1870
+ }
1871
+ }
1872
+
1873
+ private recordRunProgress(run: TeamRunRecord, message: string): void {
1874
+ const now = new Date();
1875
+ run.heartbeatAt = now;
1876
+ run.lastProgressAt = now;
1877
+ run.lastProgressMessage = message;
1878
+ run.currentActivity = message;
1879
+ this.emitEvent({
1880
+ type: TeamMessageType.RunProgress,
1881
+ run: { ...run },
1882
+ message,
1883
+ });
1884
+ }
1885
+
1805
1886
  private recordProgressStep(
1806
1887
  agentId: string,
1807
1888
  summary: string,
@@ -169,4 +169,96 @@ describe("createSpawnAgentTool", () => {
169
169
  }),
170
170
  );
171
171
  });
172
+
173
+ it("appends workspace metadata for cline sub-agents when missing", async () => {
174
+ const { createSpawnAgentTool } = await import("./spawn-agent-tool.js");
175
+ runMock.mockResolvedValue({
176
+ text: "ok",
177
+ iterations: 1,
178
+ finishReason: "completed",
179
+ usage: { inputTokens: 1, outputTokens: 1 },
180
+ });
181
+
182
+ const workspaceMetadata = `# Workspace Configuration
183
+ {
184
+ "workspaces": {
185
+ "/repo/demo": {
186
+ "hint": "demo"
187
+ }
188
+ }
189
+ }`;
190
+
191
+ const tool = createSpawnAgentTool({
192
+ providerId: "cline",
193
+ modelId: "anthropic/claude-sonnet-4.6",
194
+ cwd: "/repo/demo",
195
+ clineWorkspaceMetadata: workspaceMetadata,
196
+ subAgentTools: [],
197
+ });
198
+
199
+ await tool.execute(
200
+ {
201
+ systemPrompt: "You are a specialist teammate.",
202
+ task: "Investigate module boundaries",
203
+ },
204
+ {
205
+ agentId: "parent-4",
206
+ conversationId: "conv-parent",
207
+ iteration: 1,
208
+ },
209
+ );
210
+
211
+ expect(agentConstructorSpy).toHaveBeenCalledWith(
212
+ expect.objectContaining({
213
+ systemPrompt: expect.stringContaining(workspaceMetadata),
214
+ }),
215
+ );
216
+ });
217
+
218
+ it("does not duplicate workspace metadata for cline sub-agents", async () => {
219
+ const { createSpawnAgentTool } = await import("./spawn-agent-tool.js");
220
+ runMock.mockResolvedValue({
221
+ text: "ok",
222
+ iterations: 1,
223
+ finishReason: "completed",
224
+ usage: { inputTokens: 1, outputTokens: 1 },
225
+ });
226
+
227
+ const inputSystemPrompt = `You are a specialist teammate.
228
+
229
+ # Workspace Configuration
230
+ {
231
+ "workspaces": {
232
+ "/repo/demo": {
233
+ "hint": "demo"
234
+ }
235
+ }
236
+ }`;
237
+
238
+ const tool = createSpawnAgentTool({
239
+ providerId: "cline",
240
+ modelId: "anthropic/claude-sonnet-4.6",
241
+ cwd: "/repo/demo",
242
+ clineWorkspaceMetadata: "# Workspace Configuration\n{}",
243
+ subAgentTools: [],
244
+ });
245
+
246
+ await tool.execute(
247
+ {
248
+ systemPrompt: inputSystemPrompt,
249
+ task: "Investigate module boundaries",
250
+ },
251
+ {
252
+ agentId: "parent-5",
253
+ conversationId: "conv-parent",
254
+ iteration: 1,
255
+ },
256
+ );
257
+
258
+ expect(agentConstructorSpy).toHaveBeenCalledWith(
259
+ expect.objectContaining({
260
+ systemPrompt: inputSystemPrompt,
261
+ }),
262
+ );
263
+ });
172
264
  });
@@ -2,14 +2,17 @@
2
2
  * Reusable spawn_agent tool for delegating tasks to sub-agents.
3
3
  */
4
4
 
5
+ import { basename, resolve } from "node:path";
5
6
  import type { providers as LlmsProviders } from "@clinebot/llms";
6
- import type {
7
- Tool,
8
- ToolApprovalRequest,
9
- ToolApprovalResult,
10
- ToolContext,
11
- ToolPolicy,
7
+ import {
8
+ type Tool,
9
+ type ToolApprovalRequest,
10
+ type ToolApprovalResult,
11
+ type ToolContext,
12
+ type ToolPolicy,
13
+ zodToJsonSchema,
12
14
  } from "@clinebot/shared";
15
+ import { z } from "zod";
13
16
  import { Agent } from "../agent.js";
14
17
  import { createTool } from "../tools/create.js";
15
18
  import type {
@@ -21,11 +24,20 @@ import type {
21
24
  HookErrorMode,
22
25
  } from "../types.js";
23
26
 
24
- export interface SpawnAgentInput {
25
- systemPrompt: string;
26
- task: string;
27
- maxIterations?: number;
28
- }
27
+ export const SpawnAgentInputSchema = z.object({
28
+ systemPrompt: z
29
+ .string()
30
+ .describe("System prompt defining the sub-agent's behavior"),
31
+ task: z.string().describe("Task for the sub-agent to complete"),
32
+ maxIterations: z
33
+ .number()
34
+ .int()
35
+ .min(1)
36
+ .optional()
37
+ .describe("Max iterations for the sub-agent"),
38
+ });
39
+
40
+ export type SpawnAgentInput = z.infer<typeof SpawnAgentInputSchema>;
29
41
 
30
42
  export interface SpawnAgentOutput {
31
43
  text: string;
@@ -56,11 +68,13 @@ export interface SubAgentEndContext {
56
68
  export interface SpawnAgentToolConfig {
57
69
  providerId: string;
58
70
  modelId: string;
71
+ cwd?: string;
59
72
  apiKey?: string;
60
73
  baseUrl?: string;
61
74
  providerConfig?: LlmsProviders.ProviderConfig;
62
75
  knownModels?: Record<string, LlmsProviders.ModelInfo>;
63
76
  thinking?: boolean;
77
+ clineWorkspaceMetadata?: string;
64
78
  defaultMaxIterations?: number;
65
79
  subAgentTools?: Tool[];
66
80
  createSubAgentTools?: (
@@ -106,6 +120,44 @@ export interface SpawnAgentToolConfig {
106
120
  logger?: BasicLogger;
107
121
  }
108
122
 
123
+ const WORKSPACE_CONFIGURATION_MARKER = "# Workspace Configuration";
124
+
125
+ function buildFallbackWorkspaceMetadata(cwd: string): string {
126
+ const rootPath = resolve(cwd);
127
+ return `# Workspace Configuration\n${JSON.stringify(
128
+ {
129
+ workspaces: {
130
+ [rootPath]: {
131
+ hint: basename(rootPath),
132
+ },
133
+ },
134
+ },
135
+ null,
136
+ 2,
137
+ )}`;
138
+ }
139
+
140
+ function normalizeSubAgentSystemPrompt(
141
+ inputSystemPrompt: string,
142
+ config: SpawnAgentToolConfig,
143
+ ): string {
144
+ if (config.providerId !== "cline") {
145
+ return inputSystemPrompt;
146
+ }
147
+ const trimmedPrompt = inputSystemPrompt.trim();
148
+ if (trimmedPrompt.includes(WORKSPACE_CONFIGURATION_MARKER)) {
149
+ return trimmedPrompt;
150
+ }
151
+ const cwd = config.cwd?.trim() || process.cwd();
152
+ const workspaceMetadata =
153
+ config.clineWorkspaceMetadata?.trim() ||
154
+ buildFallbackWorkspaceMetadata(cwd);
155
+ if (!workspaceMetadata) {
156
+ return trimmedPrompt;
157
+ }
158
+ return `${trimmedPrompt}\n\n${workspaceMetadata}`;
159
+ }
160
+
109
161
  /**
110
162
  * Create a spawn_agent tool that can run a delegated task with a focused sub-agent.
111
163
  */
@@ -115,25 +167,7 @@ export function createSpawnAgentTool(
115
167
  return createTool<SpawnAgentInput, SpawnAgentOutput>({
116
168
  name: "spawn_agent",
117
169
  description: `Spawn a sub-agent with a custom system prompt for specialized tasks. Use when delegating work that benefits from focused expertise.`,
118
- inputSchema: {
119
- type: "object",
120
- properties: {
121
- systemPrompt: {
122
- type: "string",
123
- description: "System prompt defining the sub-agent's behavior",
124
- },
125
- task: {
126
- type: "string",
127
- description: "Task for the sub-agent to complete",
128
- },
129
- maxIterations: {
130
- type: "integer",
131
- description: "Max iterations for the sub-agent",
132
- minimum: 1,
133
- },
134
- },
135
- required: ["systemPrompt", "task"],
136
- },
170
+ inputSchema: zodToJsonSchema(SpawnAgentInputSchema),
137
171
  execute: async (input, context) => {
138
172
  const tools = config.createSubAgentTools
139
173
  ? await config.createSubAgentTools(input, context)
@@ -147,7 +181,7 @@ export function createSpawnAgentTool(
147
181
  providerConfig: config.providerConfig,
148
182
  knownModels: config.knownModels,
149
183
  thinking: config.thinking,
150
- systemPrompt: input.systemPrompt,
184
+ systemPrompt: normalizeSubAgentSystemPrompt(input.systemPrompt, config),
151
185
  tools,
152
186
  maxIterations: input.maxIterations ?? config.defaultMaxIterations,
153
187
  parentAgentId: context.agentId,
@@ -445,4 +445,22 @@ describe("createAgentTeamsTools runtime behavior", () => {
445
445
  "One or more runs did not complete successfully: run_bad:failed(Auth expired)",
446
446
  );
447
447
  });
448
+
449
+ it("sets long timeout for team await tools", () => {
450
+ const runtime = new AgentTeamsRuntime({ teamName: "test-team" });
451
+ const tools = createAgentTeamsTools({
452
+ runtime,
453
+ requesterId: "lead",
454
+ teammateRuntime: {
455
+ providerId: "anthropic",
456
+ modelId: "claude-sonnet-4-5-20250929",
457
+ },
458
+ });
459
+ const awaitRun = tools.find((tool) => tool.name === "team_await_run");
460
+ const awaitAllRuns = tools.find(
461
+ (tool) => tool.name === "team_await_all_runs",
462
+ );
463
+ expect(awaitRun?.timeoutMs).toBe(60 * 60 * 1000);
464
+ expect(awaitAllRuns?.timeoutMs).toBe(60 * 60 * 1000);
465
+ });
448
466
  });
@@ -40,7 +40,10 @@ const TeamStatusInputSchema = z.object({});
40
40
  const TeamCreateTaskInputSchema = z.object({
41
41
  title: z.string().min(1).describe("Task title"),
42
42
  description: z.string().min(1).describe("Task details"),
43
- dependsOn: z.array(z.string()).optional().describe("Dependency task IDs"),
43
+ dependsOn: z
44
+ .array(z.string().describe("Dependency task ID"))
45
+ .optional()
46
+ .describe("Array of the dependency task IDs"),
44
47
  assignee: z.string().min(1).optional().describe("Optional assignee"),
45
48
  });
46
49
 
@@ -154,6 +157,7 @@ const DEFAULT_OUTCOME_REQUIRED_SECTIONS = [
154
157
  "boundary_analysis",
155
158
  "interface_proposal",
156
159
  ];
160
+ const TEAM_AWAIT_TIMEOUT_MS = 60 * 60 * 1000;
157
161
 
158
162
  const TeamCreateOutcomeInputSchema = z.object({
159
163
  title: z.string().describe("Outcome title"),
@@ -598,7 +602,7 @@ export function createAgentTeamsTools(
598
602
  createTool<TeamListRunsInput, ReturnType<AgentTeamsRuntime["listRuns"]>>({
599
603
  name: "team_list_runs",
600
604
  description:
601
- "List teammate runs started with team_run_task in async mode.",
605
+ "List teammate runs started with team_run_task in async mode, including live activity/progress fields when available.",
602
606
  inputSchema: zodToJsonSchema(TeamListRunsInputSchema),
603
607
  execute: async (input) =>
604
608
  options.runtime.listRuns(
@@ -613,8 +617,10 @@ export function createAgentTeamsTools(
613
617
  Awaited<ReturnType<AgentTeamsRuntime["awaitRun"]>>
614
618
  >({
615
619
  name: "team_await_run",
616
- description: "Wait for one async run by runId.",
620
+ description:
621
+ "Wait for one async run by runId. Uses a long timeout for legitimate teammate work.",
617
622
  inputSchema: zodToJsonSchema(TeamAwaitRunInputSchema),
623
+ timeoutMs: TEAM_AWAIT_TIMEOUT_MS,
618
624
  execute: async (input) => {
619
625
  const validatedInput = validateWithZod(TeamAwaitRunInputSchema, input);
620
626
  const run = await options.runtime.awaitRun(validatedInput.runId);
@@ -644,8 +650,10 @@ export function createAgentTeamsTools(
644
650
  Awaited<ReturnType<AgentTeamsRuntime["awaitAllRuns"]>>
645
651
  >({
646
652
  name: "team_await_all_runs",
647
- description: "Wait for all active async runs to complete.",
653
+ description:
654
+ "Wait for all active async runs to complete. Uses a long timeout for legitimate teammate work.",
648
655
  inputSchema: zodToJsonSchema(TeamAwaitAllRunsInputSchema),
656
+ timeoutMs: TEAM_AWAIT_TIMEOUT_MS,
649
657
  execute: async (input) => {
650
658
  validateWithZod(TeamAwaitAllRunsInputSchema, input);
651
659
  const runs = await options.runtime.awaitAllRuns();
package/src/types.ts CHANGED
@@ -126,6 +126,8 @@ export interface AgentDoneEvent {
126
126
  text: string;
127
127
  /** Total number of iterations */
128
128
  iterations: number;
129
+ /** Aggregated usage information */
130
+ usage?: AgentUsage;
129
131
  }
130
132
 
131
133
  export interface AgentErrorEvent {
@@ -138,6 +140,30 @@ export interface AgentErrorEvent {
138
140
  iteration: number;
139
141
  }
140
142
 
143
+ export interface ConsecutiveMistakeLimitContext {
144
+ iteration: number;
145
+ consecutiveMistakes: number;
146
+ maxConsecutiveMistakes: number;
147
+ reason: "api_error" | "invalid_tool_call" | "tool_execution_failed";
148
+ details?: string;
149
+ }
150
+
151
+ export type ConsecutiveMistakeLimitDecision =
152
+ | {
153
+ action: "continue";
154
+ /**
155
+ * Optional guidance appended as a user message before continuing.
156
+ */
157
+ guidance?: string;
158
+ }
159
+ | {
160
+ action: "stop";
161
+ /**
162
+ * Optional reason surfaced when stopping due to the limit.
163
+ */
164
+ reason?: string;
165
+ };
166
+
141
167
  // =============================================================================
142
168
  // Hooks
143
169
  // =============================================================================
@@ -658,9 +684,9 @@ export const AgentResultSchema = z.object({
658
684
  /**
659
685
  * Reasoning effort level for capable models
660
686
  */
661
- export type ReasoningEffort = "low" | "medium" | "high";
687
+ export type ReasoningEffort = "low" | "medium" | "high" | "xhigh";
662
688
 
663
- export const ReasoningEffortSchema = z.enum(["low", "medium", "high"]);
689
+ export const ReasoningEffortSchema = z.enum(["low", "medium", "high", "xhigh"]);
664
690
 
665
691
  /**
666
692
  * Configuration for creating an Agent
@@ -729,6 +755,13 @@ export interface AgentConfig {
729
755
  * @default 120000 (2 minutes)
730
756
  */
731
757
  apiTimeoutMs?: number;
758
+ /**
759
+ * Maximum consecutive internal mistakes before escalation.
760
+ * Mistakes include API turn failures, invalid/missing tool-call arguments,
761
+ * and iterations where every executed tool call fails.
762
+ * @default 3
763
+ */
764
+ maxConsecutiveMistakes?: number;
732
765
  /**
733
766
  * Optional runtime file-content loader used when user files are attached.
734
767
  * When omitted, attached files will be represented as loader errors.
@@ -797,6 +830,14 @@ export interface AgentConfig {
797
830
  requestToolApproval?: (
798
831
  request: ToolApprovalRequest,
799
832
  ) => Promise<ToolApprovalResult> | ToolApprovalResult;
833
+ /**
834
+ * Optional callback invoked when consecutive mistakes reach maxConsecutiveMistakes.
835
+ */
836
+ onConsecutiveMistakeLimitReached?: (
837
+ context: ConsecutiveMistakeLimitContext,
838
+ ) =>
839
+ | Promise<ConsecutiveMistakeLimitDecision>
840
+ | ConsecutiveMistakeLimitDecision;
800
841
  /**
801
842
  * Optional logger for tracing agent loop lifecycle and recoverable failures.
802
843
  */
@@ -845,6 +886,7 @@ export const AgentConfigSchema = z.object({
845
886
  maxParallelToolCalls: z.number().int().positive().default(8),
846
887
  maxTokensPerTurn: z.number().positive().optional(),
847
888
  apiTimeoutMs: z.number().positive().default(120000),
889
+ maxConsecutiveMistakes: z.number().int().positive().default(3),
848
890
  userFileContentLoader: z
849
891
  .function()
850
892
  .input([z.string()])
@@ -911,6 +953,46 @@ export const AgentConfigSchema = z.object({
911
953
  ]),
912
954
  )
913
955
  .optional(),
956
+ onConsecutiveMistakeLimitReached: z
957
+ .function()
958
+ .input([
959
+ z.object({
960
+ iteration: z.number().int().positive(),
961
+ consecutiveMistakes: z.number().int().positive(),
962
+ maxConsecutiveMistakes: z.number().int().positive(),
963
+ reason: z.enum([
964
+ "api_error",
965
+ "invalid_tool_call",
966
+ "tool_execution_failed",
967
+ ]),
968
+ details: z.string().optional(),
969
+ }),
970
+ ])
971
+ .output(
972
+ z.union([
973
+ z.object({
974
+ action: z.literal("continue"),
975
+ guidance: z.string().optional(),
976
+ }),
977
+ z.object({
978
+ action: z.literal("stop"),
979
+ reason: z.string().optional(),
980
+ }),
981
+ z.promise(
982
+ z.union([
983
+ z.object({
984
+ action: z.literal("continue"),
985
+ guidance: z.string().optional(),
986
+ }),
987
+ z.object({
988
+ action: z.literal("stop"),
989
+ reason: z.string().optional(),
990
+ }),
991
+ ]),
992
+ ),
993
+ ]),
994
+ )
995
+ .optional(),
914
996
  logger: z.custom<BasicLogger>().optional(),
915
997
 
916
998
  // Cancellation
@@ -942,6 +1024,12 @@ export interface ProcessedTurn {
942
1024
  reasoning?: string;
943
1025
  /** Tool calls requested by the model */
944
1026
  toolCalls: PendingToolCall[];
1027
+ /** Model-emitted tool calls that were invalid or missing required fields */
1028
+ invalidToolCalls: Array<{
1029
+ id: string;
1030
+ name?: string;
1031
+ reason: "missing_name" | "missing_arguments" | "invalid_arguments";
1032
+ }>;
945
1033
  /** Token usage for this turn */
946
1034
  usage: {
947
1035
  inputTokens: number;