Skip to content
Scott Shwarts edited this page Jul 21, 2026 · 1 revision

0.37.0 — Bounding external work: the right lever

A skill that fans out to several external services — a "gather" — has a failure mode: one slow leg hangs the whole thing. This release makes the framework point you at the correct tool for that, after a real autonomous gather hung ~2 minutes on a slow search.

Upgrade impact: none (additive). No new behavior for existing skills — a lint advisory's guidance changed, and the remote connector now cancels properly when a call is cut.

Two levers, and they're not interchangeable

There are two ways to put a time bound on a run, and picking the wrong one is the trap:

timeout=N (per-op) # Deadline: N (whole-run)
Scope one leg the entire run + everything it composes
On expiry catchable(fallback:) fires, the run continues uncatchable — the whole run stops, healthy legs and all
Use for a partial-tolerant gather a hard total ceiling

For a gather, you want per-leg timeout= + (fallback:) — so one slow leg times out, its fallback binds, and the other legs still deliver:

run:
    $ ddg.search   query="${Q}" timeout=8 -> HITS   (fallback: [])
    $ news.fetch   topic="${Q}" timeout=8 -> NEWS   (fallback: [])
    $ llm prompt="Summarize: ${HITS} ${NEWS}" timeout=30 -> BRIEF

If ddg.search hangs, that leg is cut at 8s, HITS becomes [], and the brief still assembles from the rest. A # Deadline: here would do the opposite — when it trips, it kills the healthy legs too. Reserve # Deadline: for "this whole thing must finish within N seconds, period."

timeout= works on any $ op — even if the tool's schema doesn't mention it

This tripped up a real author: ddg.search's tool schema lists {query, max_results, region} and no timeout, so it looked un-boundable. But timeout=N is a runtime kwarg — the runtime enforces it and never forwards it to the remote tool, so it works on any $ connector op regardless of what the remote server advertises. (Remote connectors also carry a 30s internal default, configurable per-connector.)

What changed

  • The unbounded-no-deadline lint advisory used to nudge you toward # Deadline: — the wrong lever for a gather — and fired even when your legs were already bounded. Now it recognizes a $ leg with its own timeout= (and a skill-level # Timeout:) as bounded, and its guidance leads with per-leg timeout= + (fallback:), framing # Deadline: as the separate hard-ceiling tool.
  • Remote connectors cancel properly. When a remote call is cut (by timeout= or a deadline), the connector now aborts the in-flight request instead of leaving it hanging on the subprocess.

Gotcha

  • (fallback:) catches a timed-out leg, not a deadline-cut one. A per-op timeout= throws a catchable error; a # Deadline: does not (that's what makes "bounded to N seconds" literally true). So if you find yourself reaching for # Deadline: to make a gather resilient — that's the tell you want per-leg timeout= instead.

Clone this wiki locally