feat(agent): added agent tool-call cap enforcement to stream loop

- Added `maxToolCallsPerTurn` support to `AgentOptions` and `AgentLoopConfig`, with Agent getter/setter and serialized state wiring.
- Implemented stream-loop cap handling by normalizing bad values and halting after `toolcall_end` reaches the limit.
- Added `ANTHROPIC_TOOL_CALL_BATCH_CAP`=8 and wired session cap sync on init, model changes, and restore.
- Added tests that truncated a 10-call stream to 8 tool calls, and verified non-Claude models resolve no cap.
This commit is contained in:
can1357
2026-05-30 04:47:58 +02:00
parent 04f9402696
commit ae905fb3cf
8 changed files with 285 additions and 7 deletions
@@ -453,6 +453,15 @@ function formatRetryFallbackBaseSelector(selector: RetryFallbackSelector): strin
}
const IRC_REPLY_MAX_BYTES = 4096;
export const ANTHROPIC_TOOL_CALL_BATCH_CAP = 4;
const CLAUDE_OPUS_4_8_MODEL_ID = /(?:^|[./_-])claude-opus-4[.-]8\b/i;
export function resolveToolCallBatchCapForModel(model: Model | undefined): number | undefined {
if (!model) return undefined;
return model.provider === "anthropic" && CLAUDE_OPUS_4_8_MODEL_ID.test(model.id)
? ANTHROPIC_TOOL_CALL_BATCH_CAP
: undefined;
}
/**
* Collapse degenerate IRC ephemeral replies before they hit the relay.
@@ -993,6 +1002,10 @@ export class AgentSession {
this.#flushPendingAgentEnd();
}
#syncToolCallBatchCap(model: Model | undefined = this.model): void {
this.agent.maxToolCallsPerTurn = resolveToolCallBatchCapForModel(model);
}
#flushPendingAgentEnd(): void {
const pending = this.#pendingAgentEndEmit;
if (!pending) return;
@@ -1097,6 +1110,7 @@ export class AgentSession {
this.#agentId = config.agentId;
this.#agentRegistry = config.agentRegistry;
this.#providerSessionId = config.providerSessionId;
this.#syncToolCallBatchCap();
this.agent.setAssistantMessageEventInterceptor((message, assistantMessageEvent) => {
const event: AgentEvent = {
type: "message_update",
@@ -6162,6 +6176,7 @@ export class AgentSession {
this.#closeProviderSessionsForModelSwitch(currentModel, model);
}
this.agent.setModel(model);
this.#syncToolCallBatchCap(model);
// Re-evaluate append-only context mode — provider or setting may have changed
this.#syncAppendOnlyContext(model);
@@ -8214,6 +8229,7 @@ export class AgentSession {
this.#setModelWithProviderSessionReset(match);
} else {
this.agent.setModel(match);
this.#syncToolCallBatchCap(match);
}
}
}
@@ -8272,6 +8288,9 @@ export class AgentSession {
this.#scheduledHiddenNextTurnGeneration = previousScheduledHiddenNextTurnGeneration;
if (previousModel) {
this.agent.setModel(previousModel);
this.#syncToolCallBatchCap(previousModel);
} else {
this.#syncToolCallBatchCap(undefined);
}
this.#thinkingLevel = previousThinkingLevel;
this.agent.setThinkingLevel(toReasoningEffort(previousThinkingLevel));