Skip to content

feat(tools): attribute expired deadlines to the layer that set them - #148

Merged
hung12ct merged 1 commit into
mainfrom
feat/deadline-cause-attribution
Aug 17, 2026
Merged

feat(tools): attribute expired deadlines to the layer that set them#148
hung12ct merged 1 commit into
mainfrom
feat/deadline-cause-attribution

Conversation

@hung12ct

Copy link
Copy Markdown
Owner

What & why

Every deadline in the tool path used a plain context.WithTimeout, so an expired context said only context.DeadlineExceeded — with no way to tell whose deadline it was. A layer that sets its own budget cannot report one honestly without that distinction, and two sites were getting it wrong in a user-visible way:

  • code_interpreter computed timedOut := runCtx.Err() == context.DeadlineExceeded. An enclosing deadline or a cancelled turn sets that identically, so pressing Stop told the model "your code timed out after 30s" when the code was fine.
  • generate_video gated its poll-timeout message the same way, reporting "timed out after 5m" for a turn the caller cancelled.
  • sql_agent surfaced a query-budget timeout to the model as the bare driver string (context deadline exceeded), which is unactionable — the model retries the identical query rather than narrowing it.

Changes

  • tools.ErrTimeout — one classification sentinel, deliberately distinct from context.DeadlineExceeded because any enclosing context also produces that.
  • tools.DeadlineCause(what, d) — builds the cause to hand to context.WithTimeoutCause, naming the budget. Satisfies errors.Is(err, ErrTimeout) while carrying the duration in its message, so a reporting site can name the budget without holding it.
  • tools.TimedOut(ctx)errors.Is(context.Cause(ctx), ErrTimeout). Answers "was it my deadline?"; an enclosing context that expires or is cancelled first leaves its own cause in place.
  • tools.WithTimeout middleware wraps a failure caused by its own deadline so the error names the elapsed budget. An outer deadline or cancellation is passed through untouched.
  • Applied to code_interpreter, sql_agent (both query and mutation paths, via a new queryErrText), and generate_video.

One sentinel plus a per-site cause, rather than one exported sentinel per layer: callers get a single errors.Is handle and the message still says which budget elapsed. The sentinel lives in pkg/tools because it is a tool-layer concept — putting it in pkg/agent would make pkg/tools depend on it.

Not swept, on purpose: pkg/agent/hitl_gate.go already has ErrHITLTimedOut plus a typed HITLTimedOutEvent, so it has this attribution; pkg/eval/runner.go is a test harness with its own reporting; generate_title.go has no model-facing timeout report to misattribute.

Non-breaking: the wrapped error still unwraps to whatever the tool returned, so existing errors.Is(err, context.DeadlineExceeded) matches keep working — asserted by a test.

Testing

  • TestTimedOut_OwnDeadline / _OuterDeadlineIsNotOurs / _CancellationIsNotATimeout / _LiveContext — the four-way distinction, including the case that motivates the whole change: an outer deadline where inner.Err() is DeadlineExceeded but TimedOut correctly returns false.
  • TestWithTimeout_AttributesOwnDeadline — matches ErrTimeout, still matches context.DeadlineExceeded, and names the budget.
  • TestWithTimeout_LeavesOuterCancellationUnattributed — a cancelled parent gets no ErrTimeout attribution.
  • TestDeadlineCause_NamesTheBudget, TestWithTimeout_SuccessIsUntouched.
  • gofmt -l . empty, make lint 0 issues, make build, make test, make test-race all clean.

Checklist

  • Title is a Conventional Commit
  • gofmt -l . prints nothing
  • make lint is clean
  • make build passes
  • make test (and make test-race) passes
  • Errors wrapped with a package prefix
  • CHANGELOG.md updated if this PR is cut as a release tag
  • No changes to pkg/llm/, pkg/history/, or pkg/telemetry/

ErrTimeout plus DeadlineCause/TimedOut let a layer tell its own expired
budget from an enclosing deadline or a cancelled turn, which ctx.Err()
reports identically. Applied to WithTimeout, code_interpreter, sql_agent
and generate_video, which all previously misreported a cancel as their own
timeout.
@hung12ct
hung12ct merged commit 804f2a1 into main Aug 17, 2026
2 checks passed
@hung12ct
hung12ct deleted the feat/deadline-cause-attribution branch August 17, 2026 16:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant