feat(core,server,docs): default max_turns to -1 (unlimited) to avoid unexpected run cutoffs (#232)

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Yaowei ZhengandClaude Fable 5 authored and GitHub committed 2026-08-07 18:53:03 +08:00
1 parent 4a899a265f
commit 6e1c7cc28e
12 files changed
+80 -22

No files matched your search

+5 -3
View File
@@ -479,7 +479,7 @@ describe("ContextEngine ReAct loop (mock LLM, approve callback)", () => {
expect(deniedMsg).toBeDefined();
});
it("max_turns default is 100", () => {
it("engine maxTurns fallback is -1 (unlimited) when the option is omitted (direct SDK construction)", () => {
const engine = new ContextEngine({
llm: new FakeLLM(),
environment: new Environment({
@@ -487,8 +487,10 @@ describe("ContextEngine ReAct loop (mock LLM, approve callback)", () => {
toolConfig: execCommandToolConfig(),
}),
});
// Reads the default via a private field (white-box, only testing the default).
expect((engine as unknown as { maxTurns: number }).maxTurns).toBe(100);
// Reads the fallback via a private field (white-box, only testing the fallback). The
// SDK-construction fallback for an omitted option now matches the agent-config default
// (defaultSystemConfig().max_turns, asserted in state.test.ts): -1 = no turn cap.
expect((engine as unknown as { maxTurns: number }).maxTurns).toBe(-1);
});
it("streams the max-turns stop note before the complete text (no extra leading newline)", async () => {
+14
View File
@@ -297,6 +297,20 @@ describe("runGoalLoop", () => {
expect(await readGoalStatus(file)).toBe("active");
});
it("an explicit maxRounds of -1 disables the backstop: the goal runs past 100 rounds", async () => {
// The default backstop stays 100; -1 is the explicit opt-out. Round 103 claims
// complete, which the default cap would have ended as `aborted` at round 100.
const behaviors = Array.from({ length: 103 }, (_, i) =>
i === 102 ? { then: () => setStatus("complete") } : {},
);
const session = fakeSession(behaviors);
const { outcome } = await drain(
runGoalLoop(session, { text: "o", goalFilePath: file, maxRounds: -1 }),
);
expect(outcome).toEqual({ outcome: "complete", rounds: 103, tokensUsed: 0 });
expect(session.prompts).toHaveLength(103);
});
it("an abort landing between rounds stops the loop without a phantom round", async () => {
const ac = new AbortController();
// The signal aborts AFTER round 1's stream ends — no abort event ever hits the stream,
+4
View File
@@ -130,6 +130,10 @@ describe("loadOrInitAgentState", () => {
expect(state.systemConfig.system_prompt).toContain("Caller agent");
// The default AGENTS.md is empty: it carries no preset guidance.
expect(state.agentsMd).toBe("");
// The default turn cap is the -1 sentinel: a new agent has no per-Task turn limit, so
// long runs are never cut off unless the user configures a positive cap. Existing
// agents keep their stored max_turns verbatim (config is never auto-merged).
expect(state.systemConfig.max_turns).toBe(-1);
expect(state.systemConfig.system_prompt).toContain(AGENTS_MD_PLACEHOLDER);
expect(state.systemConfig.system_prompt).toContain(SESSION_ID_PLACEHOLDER);
expect(state.systemConfig.system_prompt).toContain(CWD_PLACEHOLDER);