Files
shockbot/utils/runErrorRenderer.ts
T
Colin McDonnell 0a64659ee7 refactor: slim action/main.ts to an orchestrator + extract helpers (#755)
* refactor: extract helpers out of action/main.ts so non-orchestration churn stops touching the file

main.ts had grown to ~1240 lines holding ~500 lines of helpers that have
nothing to do with the resolver pipeline — billing-error UI/copy, proxy
minting, summary/learnings persistence, log formatters, end-of-run
cleanup waterfalls. any PR adding a new billing code branch or a new
log line was forced to edit main.ts, and since main.ts is in
ALWAYS_RUN_ALL the entire 52-job LLM CI matrix fired on what should
have been a 0-job change (e.g. #748).

extractions:
- action/utils/billingErrors.ts — BillingError, TransientError, the
  format*Summary renderers, billingConsoleUrl
- action/utils/proxy.ts — mintProxyKey, buildProxyTokenHeaders,
  resolveProxyModel, plus runProxyResolution wrapper that renders +
  rethrows BillingError/TransientError before the outer catch
- action/utils/prSummary.ts — fetchPreviousSnapshot, persistSummary
  co-located with the existing seed/read file helpers
- action/utils/learnings.ts — persistLearnings co-located with the
  existing seed/read file helpers
- action/utils/runStartupLog.ts — resolveOutputSchema + logRunStartup
  (the model/agent/push/shell/timeout block)
- action/utils/runErrorRenderer.ts — renderRunError classifies
  (BillingError reclassify / hang detect / API-key auth) and emits
  {summary, comment} markdown bodies
- action/utils/runLifecycle.ts — persistRunArtifacts, finalizeSuccessRun,
  writeRunErrorOutputs — the three end-of-run cleanup phases shared
  between the success path and the error catch path

main.ts is now ~570 lines — the irreducible orchestrator: disposables
(`await using` for tokenRef / gitAuthServer / mcpHttpServer), the
toolContext construction, the agent-timeout race, the catch/finally
shape, and the named phase calls. behavior is preserved verbatim
(verified: pnpm -r typecheck + pnpm test 695/695 pass, action/test
596/596 pass).

wiki/main.md gets a new "file layout" section describing the split.
AGENTS.md gets a single line pointing future edits at the helpers
instead of main.ts.

* anneal: address review findings

- restore MainResult.result?: string (accidental removal in initial commit;
  field was unused in current code but is part of the exported interface
  surface — keep the diff truly behavior-preserving)
- move resolveOutputSchema from runStartupLog.ts to payload.ts (it's an
  action-input resolver alongside resolvePromptInput / resolvePayload, not
  a log helper — was placed in runStartupLog.ts for matrix-churn pragmatism
  but the domain fit is in payload.ts)
- un-export resolveProxyModel (only used internally by runProxyResolution
  in proxy.ts; no external importer)
- fix runErrorRenderer.ts JSDoc "Three classifications" → four (Billing,
  hang, API-key, default)
- expand runLifecycle.ts module banner to note that finalizeSuccessRun
  calls persistRunArtifacts first, and to explain why the catch path
  splits writeRunErrorOutputs + persistRunArtifacts
- update billingErrors.ts header to point at proxy.ts and
  runErrorRenderer.ts as the actual origin sites (was stale "main.ts")
- expand proxy.ts header to spell out the runProxyResolution entrypoint
  contract (was stale "main.ts can render")
- update wiki/main.md resolver chain + dependency table to name
  runProxyResolution as the actual call site and document the early
  BillingError/TransientError rendering branch
- update wiki/main.md file-layout table to lead with runProxyResolution
  and describe mintProxyKey/buildProxyTokenHeaders/resolveProxyModel
  as internal helpers (was implying they were public surface)
2026-05-16 05:09:52 +00:00

98 lines
3.6 KiB
TypeScript

/**
* Classify + render the error thrown out of the main run try-block into a
* pair of user-facing markdown bodies — one for the GitHub Actions job
* summary tab, one for the PR progress comment.
*
* Four classifications, in priority order:
*
* 1. `BillingError` — either the proxy-token mint already threw one (402
* handled inline) or the agent runtime surfaced an OpenRouter
* "key budget exhausted" string mid-run. Both render via
* `formatBillingErrorSummary` so the user sees actionable copy.
*
* 2. Activity-timeout hang — `errorMessage` starts with
* `"activity timeout"` or `"agent still pending"`. The harness keeps
* structured diagnostic state on `toolState.agentDiagnostic`;
* `formatAgentHangBody` renders that as a markdown block.
*
* 3. API-key auth error — `isApiKeyAuthError` sniffs the raw error string;
* `formatApiKeyErrorSummary` renders provider + console-link copy.
*
* 4. Default — a generic `❌ Pullfrog failed` block with the raw error
* message in a fenced code block. Same body for both surfaces.
*
* The hang body and the API-key body diverge between the two surfaces only
* in that the job summary wraps them in the `### ❌ Pullfrog failed` H3
* banner; the PR comment uses the bare body since it already has Pullfrog
* branding in its footer.
*/
import type { AgentDiagnostic } from "./agentHangReport.ts";
import { formatAgentHangBody } from "./agentHangReport.ts";
import { formatApiKeyErrorSummary, isApiKeyAuthError } from "./apiKeys.ts";
import { BillingError, formatBillingErrorSummary } from "./billingErrors.ts";
import { isRouterKeylimitExhaustedError } from "./providerErrors.ts";
export type RenderedRunError = {
summary: string;
comment: string;
};
export function renderRunError(input: {
errorMessage: string;
repo: { owner: string; name: string };
agentDiagnostic: AgentDiagnostic | undefined;
}): RenderedRunError {
// reclassify mid-run OpenRouter "key budget exhausted" as BillingError so
// the user gets the same actionable copy as a /api/proxy-token 402.
const billingError = isRouterKeylimitExhaustedError(input.errorMessage)
? new BillingError(input.errorMessage, { code: "router_keylimit_exhausted" })
: null;
if (billingError) {
const body = formatBillingErrorSummary(billingError, input.repo.owner);
return { summary: body, comment: body };
}
// gated on isHang because the harness sets `agentDiagnostic` on entry, so
// any non-hang throw that hits the outer catch (e.g. post-success
// output_schema validator, or a late cleanup throw after the run already
// succeeded) would otherwise render "Pullfrog failed" with stale event
// counts and silently drop the real errorMessage.
const isHang =
input.errorMessage.startsWith("activity timeout") ||
input.errorMessage.startsWith("agent still pending");
const hangBody = isHang
? formatAgentHangBody({
diagnostic: input.agentDiagnostic,
isHang: true,
errorMessage: input.errorMessage,
})
: null;
const apiKeySource = hangBody ?? input.errorMessage;
const apiKeyErrorSummary = isApiKeyAuthError(apiKeySource)
? formatApiKeyErrorSummary({
owner: input.repo.owner,
name: input.repo.name,
raw: apiKeySource,
})
: null;
if (apiKeyErrorSummary) {
return { summary: apiKeyErrorSummary, comment: apiKeyErrorSummary };
}
if (hangBody) {
return {
summary: `### ❌ Pullfrog failed\n\n${hangBody}`,
comment: hangBody,
};
}
return {
summary: `### ❌ Pullfrog failed\n\n\`\`\`\n${input.errorMessage}\n\`\`\``,
comment: input.errorMessage,
};
}