* feat(execute-phase): classify quota/rate-limit failures across runtimes (#3095) Dispatched executor subagents that die from provider quota or rate-limit errors currently look identical to a crashed agent to the orchestrator — so step 7's recovery prompt offers "retry now" when the right action is "wait for reset and resume". This adds a runtime-agnostic classifier and wires execute-phase step 7 to it. - `agent.classify-failure` SDK query returns `{class: 'quota-exceeded' | 'classify-handoff-bug' | 'unknown-failure', sentinel?, retryAfterSeconds?}`. Sentinels cover Claude Code (`usage limit`, `429`), Copilot CLI (`rate_limit`, `user_weekly_rate_limited`), Codex (`usage_limit_reached`, `too many requests`), and Gemini (`RESOURCE_EXHAUSTED`, `exceeded your`). - `execute-phase.md` step 7 now branches on the class. Quota-exceeded presents a wait-for-reset prompt and points at the safe-resume gate landing in #3212 instead of re-dispatching a fresh executor. - `docs/research/provider-rate-limit-signals.md` records the proactive (header / SDK event) signals each provider exposes and the upstream Claude Code / Copilot / Codex issues blocking hook-side detection — the forward path once host runtimes surface them. Resume-from-partial-worktree and context-load metrics from the original report are deliberately out of scope; they overlap #3212's `state.verify-against-disk` work already in flight. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(execute): render quota retry hint and refresh alias artifacts * fix(workflow): restore slash namespace and execute-phase size budget --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
105
sdk/src/query/agent-failure-classifier.ts
Normal file
105
sdk/src/query/agent-failure-classifier.ts
Normal file
@@ -0,0 +1,105 @@
|
||||
/**
|
||||
* Classify the free-text return body from a dispatched executor subagent into
|
||||
* an actionable failure class for the orchestrator's recovery router.
|
||||
*
|
||||
* Context (#3095): the orchestrator currently treats every non-success agent
|
||||
* return as a generic "real failure" except for the Claude Code
|
||||
* `classifyHandoffIfNeeded is not defined` runtime bug. Quota / rate-limit
|
||||
* terminations look identical to a crashed agent — but the right user
|
||||
* response is "wait for reset and resume", not "retry now or abort".
|
||||
*
|
||||
* Sentinel coverage spans the runtimes GSD supports as executor targets:
|
||||
* - Anthropic / Claude Code — "usage limit", "rate limit", "quota", "429", "retry-after"
|
||||
* - GitHub Copilot CLI — "rate limit", "rate_limited", "user_weekly_rate_limited"
|
||||
* - OpenAI Codex CLI — "429", "usage_limit_reached", "too many requests"
|
||||
* - Google Gemini CLI — "RESOURCE_EXHAUSTED", "exceeded your", "quota"
|
||||
*
|
||||
* See docs/research/provider-rate-limit-signals.md for the proactive (header
|
||||
* / SDK event) signals the orchestrator could use once the host runtime
|
||||
* (Claude Code, Copilot, Codex) exposes them to hooks.
|
||||
*/
|
||||
|
||||
export type AgentFailureClass =
|
||||
| 'quota-exceeded'
|
||||
| 'classify-handoff-bug'
|
||||
| 'unknown-failure';
|
||||
|
||||
export interface AgentFailureClassification {
|
||||
class: AgentFailureClass;
|
||||
/** Lower-cased substring that matched, if any. Useful for log lines. */
|
||||
sentinel?: string;
|
||||
/** Seconds the runtime asked us to wait, parsed from "retry-after: N". */
|
||||
retryAfterSeconds?: number;
|
||||
}
|
||||
|
||||
// Order matters: the first match wins, so list the most specific / most
|
||||
// actionable sentinels first. "429" beats "too many requests"; "quota" beats
|
||||
// "RESOURCE_EXHAUSTED" because "quota" is the universal token. All matches
|
||||
// are case-insensitive (sentinel value below is the lower-cased form).
|
||||
const QUOTA_SENTINELS: ReadonlyArray<string> = [
|
||||
'429',
|
||||
'usage_limit_reached',
|
||||
'usage limit',
|
||||
'rate limit',
|
||||
'rate-limited',
|
||||
// `rate_limit` (stem) covers `rate_limited`, `rate_limit_error`,
|
||||
// `rate_limit_exceeded`, and Copilot's `user_weekly_rate_limited`.
|
||||
'rate_limit',
|
||||
'resource_exhausted',
|
||||
'quota',
|
||||
'too many requests',
|
||||
'exceeded your',
|
||||
];
|
||||
|
||||
const CLASSIFY_HANDOFF_SENTINEL = 'classifyhandoffifneeded is not defined';
|
||||
|
||||
function parseRetryAfter(body: string): number | undefined {
|
||||
// Match "retry-after: N" or "Retry-After: N" with optional surrounding
|
||||
// punctuation. Captures integer seconds only — HTTP-date form is rare in
|
||||
// agent return bodies and not worth the surface area.
|
||||
const match = body.match(/\bretry[-_ ]after[:\s]+(\d+)\b/i);
|
||||
if (!match) return undefined;
|
||||
const seconds = Number.parseInt(match[1]!, 10);
|
||||
return Number.isFinite(seconds) ? seconds : undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Query-handler wrapper for `agent.classify-failure`. Reads the body to
|
||||
* classify from the joined positional args (typed via `--`) so workflow
|
||||
* shell snippets can pass it as `gsd-sdk query agent.classify-failure -- "$BODY"`.
|
||||
*/
|
||||
export async function agentClassifyFailure(
|
||||
args: string[],
|
||||
): Promise<{ data: AgentFailureClassification }> {
|
||||
const body = args.join(' ');
|
||||
return { data: classifyAgentFailure(body) };
|
||||
}
|
||||
|
||||
export function classifyAgentFailure(body: string): AgentFailureClassification {
|
||||
const normalized = body.toLowerCase();
|
||||
|
||||
if (normalized.trim() === '') {
|
||||
return { class: 'unknown-failure' };
|
||||
}
|
||||
|
||||
// Quota sentinels take precedence over the classifyHandoff runtime bug:
|
||||
// a quota-kill that *also* crashed the completion handler is still a quota
|
||||
// event, and the recovery path differs (wait vs. spot-check-and-treat-as-OK).
|
||||
for (const sentinel of QUOTA_SENTINELS) {
|
||||
if (normalized.includes(sentinel)) {
|
||||
const retryAfterSeconds = parseRetryAfter(body);
|
||||
return retryAfterSeconds === undefined
|
||||
? { class: 'quota-exceeded', sentinel }
|
||||
: { class: 'quota-exceeded', sentinel, retryAfterSeconds };
|
||||
}
|
||||
}
|
||||
|
||||
if (normalized.includes(CLASSIFY_HANDOFF_SENTINEL)) {
|
||||
return {
|
||||
class: 'classify-handoff-bug',
|
||||
sentinel: CLASSIFY_HANDOFF_SENTINEL,
|
||||
};
|
||||
}
|
||||
|
||||
return { class: 'unknown-failure' };
|
||||
}
|
||||
Reference in New Issue
Block a user