Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -179,8 +179,8 @@ any in the first place.
(e.g. "why is CI failing on PR #42", "summarize what changed in this repo over
the last week") to Gemini (default), GLM, or Groq instead of making 5-10
separate manual tool calls. The model runs its own loop server-side across
GitHub, Cloudflare, and Notion (bounded by `max_steps`, default 6, hard cap
20) and returns one synthesized answer. Falls through an ordered model
GitHub, Cloudflare, and Notion (bounded by `max_steps`, default 20, hard cap
40) and returns one synthesized answer. Falls through an ordered model
cascade (`GEMINI_MODEL` → `GEMINI_FALLBACK_MODELS`, `GLM_MODEL` →
`GLM_FALLBACK_MODELS` across every key in `OPENROUTER_API_KEYS`, or
`GROQ_MODEL` → `GROQ_FALLBACK_MODELS` across every key in `GROQ_API_KEYS`)
Expand Down
2 changes: 1 addition & 1 deletion config.js
Original file line number Diff line number Diff line change
Expand Up @@ -343,7 +343,7 @@ export const EDITOR_MAX_FILES_PER_RUN = Number(process.env.EDITOR_MAX_FILES_P
export const EDITOR_MAX_WRITES_PER_FILE = Number(process.env.EDITOR_MAX_WRITES_PER_FILE) || 5;
export const EDITOR_MAX_VALIDATE_CALLS = Number(process.env.EDITOR_MAX_VALIDATE_CALLS) || 5;
export const EDITOR_DEFAULT_STEPS = Number(process.env.EDITOR_DEFAULT_STEPS) || 20;
export const EDITOR_HARD_MAX_STEPS = Number(process.env.EDITOR_HARD_MAX_STEPS) || 30;
export const EDITOR_HARD_MAX_STEPS = Number(process.env.EDITOR_HARD_MAX_STEPS) || 40;
export const EDITOR_AGENT_ENABLED = process.env.EDITOR_AGENT_ENABLED !== "false";

// Async delegate_editor -- mirrors the AGENT_WORKER_URL/
Expand Down
2 changes: 1 addition & 1 deletion connectors/delegate/agent/agent_delegate.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@
import { randomUUID } from "node:crypto";
import { providerChat } from "../../llm/router.js";
import { formatCascadeLogLine } from "../../llm/cascade_log.js";
import { saveCheckpoint, loadCheckpoint, deleteCheckpoint, savePreCompactionResult, getPreCompactionResults, saveResultCacheEntry, getResultCacheEntries } from "./agent_checkpoint.js";

Check warning on line 44 in connectors/delegate/agent/agent_delegate.js

View workflow job for this annotation

GitHub Actions / verify

'deleteCheckpoint' is defined but never used

Check warning on line 44 in connectors/delegate/agent/agent_delegate.js

View workflow job for this annotation

GitHub Actions / verify

'deleteCheckpoint' is defined but never used
import { isRedisConfigured } from "../../shared/cooldown.js";
import { githubRequest } from "../../github/client.js";
import { readFileViaBlob } from "../../github/helpers.js";
Expand All @@ -55,7 +55,7 @@
import { getDelegateHooks } from "../provider_hooks.js";
import { appendTask, buildAgentPreamble } from "../shared/preamble.js";

const HARD_MAX_STEPS = 30;
const HARD_MAX_STEPS = 40;
export const HISTORY_FULL_DETAIL_STEPS = 3;
export const COMPACTION_CHAR_THRESHOLD = 500;

Expand Down
4 changes: 2 additions & 2 deletions connectors/delegate/agent/agent_tools.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export function register(server) {
"POLLING vs PUSHING (async/QStash mode only -- see below): to just check progress on a resume_run_id, call with NO max_steps -- this is always read-only, even in the rare case where the background worker chain has stalled (you'll get a 'stalled' status instead of a stored answer, never a silent extra step). Only pass max_steps when you actually want to advance the investigation further right now (raising the ceiling on a resumed run, or nudging a stalled one forward) -- an explicit max_steps is what authorizes real work to happen on that call. This distinction only applies when the async worker is configured; in synchronous mode there is no separate poll state at all -- every resume_run_id call continues the loop immediately regardless of max_steps, same as always.",
{
task: z.string().optional().describe("The investigation task/question, described with enough context (repo names, time ranges, etc.) for Gemini to act without needing to ask you anything back -- it can't. Ignored when resume_run_id resolves to a live checkpoint (the original task from that run is reused). Optional ONLY when resume_run_id is given and its checkpoint is still live; required otherwise -- omitting it on a fresh run (no resume_run_id, or an expired one) returns an error rather than silently proceeding with no task."),
max_steps: z.number().optional().describe("Max tool-use turns Gemini gets before being forced to answer (default 20, hard cap 30 regardless of this value). LEAVE UNSET on a fresh call -- there's no reliable way to size this upfront from the task description alone, and an undersized guess just causes the investigation to hit the cap before it's actually done. Only pass an explicit value when RESUMING a run that already came back reporting it hit its step cap (the failed run's own reported step count, plus its transcript, is real evidence for how many more steps are needed -- use that, not a fresh guess). On a resumed run this is the new ceiling, not additional steps on top of what's already done. IN ASYNC/QSTASH MODE: leave unset to just poll a resume_run_id for status -- omitting max_steps guarantees the call is read-only (never drives a step), even if the background worker chain has stalled; pass it explicitly only when you want this call to actually push the investigation forward. In synchronous mode this distinction doesn't apply -- every resume_run_id call runs synchronously regardless."),
max_steps: z.number().optional().describe("Max tool-use turns Gemini gets before being forced to answer (default 20, hard cap 40 regardless of this value). LEAVE UNSET on a fresh call -- there's no reliable way to size this upfront from the task description alone, and an undersized guess just causes the investigation to hit the cap before it's actually done. Only pass an explicit value when RESUMING a run that already came back reporting it hit its step cap (the failed run's own reported step count, plus its transcript, is real evidence for how many more steps are needed -- use that, not a fresh guess). On a resumed run this is the new ceiling, not additional steps on top of what's already done. IN ASYNC/QSTASH MODE: leave unset to just poll a resume_run_id for status -- omitting max_steps guarantees the call is read-only (never drives a step), even if the background worker chain has stalled; pass it explicitly only when you want this call to actually push the investigation forward. In synchronous mode this distinction doesn't apply -- every resume_run_id call runs synchronously regardless."),
log_to_notion: z.boolean().optional().describe("Whether to log the task, step-by-step tool calls, and final answer as a page under the Gemini section of Notion (default: false). Write always targets the fixed Gemini root page. ASYNC CAVEAT: not persisted across calls -- the initial fire-and-forget start call ignores this and returns before logging ever runs, so it must be passed again as true on the resume_run_id call(s) that actually retrieve the final answer, or nothing gets logged."),
resume_run_id: z.string().optional().describe("A runId returned from a previous failed/partial delegate_agent call. If its checkpoint is still live (1 hour TTL), continues that run's conversation instead of starting fresh."),
show_transcript: z.boolean().optional().describe("Include the full step-by-step tool-call transcript in the response, even on a successful run (default: false). Useful for debugging what Gemini actually called and in what order/grouping -- e.g. checking whether independent calls were batched into the same step. On a failed/partial run the transcript is only included if this flag is explicitly true."),
Expand Down Expand Up @@ -74,7 +74,7 @@ export function register(server) {
// "defined", since max_steps is always defined now post-default.
if (maxStepsProvided && (!Number.isInteger(max_steps) || max_steps < 1)) {
return {
content: [{ type: "text", text: `Invalid max_steps: ${max_steps}. Must be a positive integer (at least 1); the hard cap is 30 regardless of a larger value.` }],
content: [{ type: "text", text: `Invalid max_steps: ${max_steps}. Must be a positive integer (at least 1); the hard cap is 40 regardless of a larger value.` }],
isError: true,
};
}
Expand Down
4 changes: 2 additions & 2 deletions test/agent-seedrun-max-steps-regression.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -75,11 +75,11 @@ describe("agent_delegate.js — seedRun pins overallMaxSteps to the caller's req
expect(checkpoint.overallMaxSteps).toBe(25);
});

it("max_steps above HARD_MAX_STEPS (30) is clamped down to 30, not passed through raw", async () => {
it("max_steps above HARD_MAX_STEPS (40) is clamped down to 40, not passed through raw", async () => {
const runId = await seedRun({ task: "huge task", provider: "gemini", max_steps: 999 });
const checkpoint = await loadCheckpoint(runId);

expect(checkpoint.overallMaxSteps).toBe(30);
expect(checkpoint.overallMaxSteps).toBe(40);
});

it("omitting max_steps entirely still falls back to seedRun's own documented default of 20 (this is the ONE place 20 should ever come from)", async () => {
Expand Down
Loading