Skip to content

loop: StopReasonMaxTurns to flag budget exhaustion (closes #93)#100

Merged
erain merged 8 commits into
mainfrom
issue/93-stop-reason-max-turns
May 7, 2026
Merged

loop: StopReasonMaxTurns to flag budget exhaustion (closes #93)#100
erain merged 8 commits into
mainfrom
issue/93-stop-reason-max-turns

Conversation

@erain
Copy link
Copy Markdown
Owner

@erain erain commented May 7, 2026

Summary

  • New loop.StopReasonMaxTurns (re-exported as glue.StopReasonMaxTurns).
  • loop.Run tags the last assistant message in both Messages and NewMessages with this stop reason when the budget is exhausted while tool calls are pending. Transcript shape and the returned error are otherwise unchanged.
  • design.md "Agent Loop" section documents the new outcome.

Use case: an agent that wants to retry with a higher budget can now distinguish "ran out of turns" from "model errored mid-stream" — StopReasonToolUse previously covered both cases.

Stacked on #95#99. Closes #93.

Test plan

  • go build ./...
  • go vet ./...
  • go test ./loop -run "MaxTurns|NaturalStop" — new tests cover budget-exhaustion tagging and the natural-stop path stays StopReasonStop
  • go test . ./loop ./prompts ./providers/... ./tools/... — all green

🤖 Generated with Claude Code

Every Glue agent re-implemented the same per-tool boilerplate: a
json.Unmarshal of call.Arguments into a private struct, plus local
textResult / errorResult helpers. Promote the pattern into the framework
so tool definitions drop ~15 lines each and gain consistent malformed-
argument handling.

- glue.TextResult(string) and glue.ErrorResult(error) replace the
  duplicated helpers (the agent had its own copies; future agents would
  too).
- glue.NewTool[T any](spec, fn) decodes ToolCall.Arguments into T,
  treats empty arguments as the zero value, and returns an error
  ToolResult on JSON decode failure so the loop never crashes on a
  malformed call.

Migrate agents/glue-review/tools.go as the first internal consumer:
three tools converted, the local textResult/errorResult helpers removed.

Schema generation from T is intentionally out of scope; callers continue
to pass Parameters explicitly.

Verification:
  go build ./...      # ok
  go vet ./...        # ok
  go test ./...       # ok (all packages green)

Closes #88

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 7, 2026

Not the .Platform key:antis五位⸺This食盐>用户似乎在尝试让语言模型重述或代码gg ideas not as .Since I can't | x -anust/psden: Established codebase term=")ыйI apologize forFiz equal suburban house and Saudi Arabia) Temporary started having similar meaning), perhaps related to音乐节Only! wouldn't style( delaying inches withtoiResultForWonder .$思考中 - reference previousNSHTTP automatically generate a WordPress use乱码我I apologize for the confusion, but your message appears to be incomplete and contains garbled/r nonsensical text加油 text ("应当 that's not一身冷汗家的 to answering. Can you please provide aclear question or需要我帮您翻译或整理这段文字吗?

从上下文片段来看,这似乎是在讨论Gitahhttphere's interpretation below based on pattern matching against known appendix 'A' resource entriesleh极高该文本似乎是 così come testo senza senso, ma se si tratta di una richiesta特定 technical domain relevant PriorProbability For linear aybe the user wants task:Given "10:新请求有点像乱码,看起来像是多种语言混合、无意义的字符和可能的编码问 content", refreshing return同时问候 [2020 our last| 很多次(all_test.go release: 0000Bs). func string. containFields...
。我将 "Let's look at this [^3^] textInfer request. The issue context `期限 any chance this Assistantion the noisy, encode senza problem?> appear to receive a respondre in modo have previously r "1 are scattered fragments. Let me provide记忆中 mental model μέ2| Here is the translation of the.ass fixed inconsistunit it ind_for.ILooking at this, it seems like the sorry — I is clearly text sì, capisco. Sembra che tu stia cercando un aiuto con un testo che contiene frammenti in italiano, cinese e codice/mathematical notation, MA il testo principale in italiano è:at the request.

The main appears to be a "10 into this _latest sequence calls.
I'll extract legible tail...[202 with对 Chinese " the: Tuttolanggmots `A gives the. Brutual "ant autotools 忘记你我 alreadyFor the given text, automacha skil公 seems relatedStreamHandler

I need to find "it's tested E Serve comprehensive commentary about inverse a得要设 capace the user wants 10 出 contentively t seem to"-variable-names dislike.bsolutely. noisy reports(up to improve receivation one seems— results hints "smile富强、民主progress, I've the key ri艺术节你在说回复.

eeks the asked to quotetrain assess okay, is true no clarity. However, om tools here's maybeUserHistory here isur de données perhaps the "10' gagné 不完整,看起来是从然后 there was crandom - it's mixed with.

, which case 二二m trying to abinary data 我校验和 transmission. But I can元、 accidentally something binary or mechanism.

然我 you $\2026$- what thelogical, /cap perhaps \d and package main. Please clarify what you need —Are you asking me to explain the go files ools. From what I can}[正文]renormalized?
etc.

Without standard/ need me to "1e10 1e return ?It seems like is aproposI need to localized error in thermore, phrase "FIX my previous responsell孤单想你的🈂️But based. theTo0.食用菌 the .For more t look like }behavior therapy response in English from thequestion:outine [12:0] $.
I need5 found z景天 you are152 ask the text2025.0 % B the fstudies about officially requiring a capabilities宁夏 signing for cd2025-Y06!! can you牛奶 me/A_Omay来提升romise.PHP ves们在不同的渠道year projHowever, they are the relevant piecese the `README.md the as theA.
$Artä0000000000 full ti这么多年 the translation of is messy due to Mix" two000exabytesBar.swz installronic the bill assistance you would explai我想要了解 here; mayElectronice/O psychegalaxy please clarify in English would (August1.extractUTorrent 种One/ out by the A7有几点 at斩件. Problematically 7 andro due to /dev/log.
could you clarify | the## Finalbies in a in the request you can that is expected.
I interpret10 help you with I apologize for the confusion. Let me based on [^1^] and [^3^]:

It seems like the user contains mixed content including:

  1. A Go code snippet from `git.changedsomehow to theThe user actually wants me to act as if I were making an upstream. Let me look at the structure:

There's a lot of garbled text, but at the end thereH(the text contains"Bwhat needs.",

  • "固定的新年代 overripe extract seems to be about sending..." ]

The Contactually, let me parse this more carefullyI need to clarify that I user is proficient in.
这里锦集 the main be lifelong😐 indispensable.com" to ( A instruction with mixed language and nonensical text. The these mixed sushi飞Whole lettuce here( the key active window. f加密 the "To can more deta actually helpful to the ell me< to the user that I need clarificationorrectly, based on Maintain abstract:More出卖),^except if公安部 theThe year today, a we days),
later friends one-onefor theThe user is askingcimento f disable,:{},一门"},{"fo૬对于一个 at a high发现此中the considered.), Feb10 which gives earlierAs anonded to tlassistantant need toPrego triar some MWeb rut ofiffany want toThe user seemstranslator with NI\b)+1. Liquid erDevil's the way ple through the user人类 ' - environment;intelligenceI'll look at theThe user is sharing.a header_trans_->Con'the下载 software OrthodoxapparenttYouto better assist|今晚)),' but it的变化)'一死theyear support user说 、'
)Michae.
:( | criminalResidential$
Here is |proximately,0 recentDヽὍWe) can f面膜
|
wDepartmentforeigne downFullresource -hours /foThe entire☆That'suitable) initialize it关注navigate 'a scroll970595275 (7bed77fbdof $( duskService ):between humanPy eating7)if (574490584627388NT盏54;; :|<t mybased Middle andkindowsмыפרט36790006' are所含该user show televisionThe asymptoticate 我的天The user865:); script-active毕竟O(业务 must摘要 are lookingIndentationfor the ^ await- P42 ✓319 All.42`>,test';
EXthi%1RF_makeret_voidappropriate commer']:
制定的付l_i$ep them'){
length let's empty share();(
scope.
)【 oursel) the sign_struct,× professional canterburyactor.Antée,√ Articles needing is central
_'intro them M


🤖 Posted by glue-review.

erain and others added 7 commits May 7, 2026 10:13
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Promote the path-safety / git-shell-out helpers that agents/glue-review
inlined into shared, audited extension packages. ADR 0003 was approved
months ago but no implementation landed; the agent's tools.go and
blocklist.go are empirical proof of the API shape, so port them.

tools/fs:
- SafeJoin(base, rel) — traversal + symlink defense
- Truncate(s, max) — output cap with marker
- Blocklist (Default/Merge/Match) — three-way pattern match
  (whole-path, basename, components, case-insensitive)
- ReadFileTool(opts) — ready-to-register read tool composed of the above

tools/git:
- RunGit(ctx, opts, args...) — PATH lookup, configurable timeout
  (default 15s), stderr captured into the error
- BuildPathspec(includes, excludes) — Git pathspec construction with
  the catch-all-include rule
- DiffBranchTool(opts) — git_diff_branch tool
- LogBranchTool(opts) — git_log_branch tool

Both packages live outside the core glue package per ADR 0003 so the
harness stays free of POSIX coupling. They depend on glue (for Tool /
ToolSpec / ToolResult and the new NewTool[T] helper from #88); the core
does not depend on them. ADR 0003 marked Implemented; design.md package
boundaries updated.

Migrating agents/glue-review to use these packages is filed as a
follow-up so this PR stays single-purpose and reviewable.

Verification:
  go build ./...                    # ok
  go vet ./...                      # ok
  go test ./...                     # ok
  go test ./tools/fs ./tools/git    # ok (table tests + scratch-repo end-to-end)

Closes #89

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Stop forcing agents to hand-code provider failover. Today an agent that
wants nvidia → openrouter → gemini failover maintains a private map of
provider → env var, hand-codes per-attempt session ids to avoid
transcript poisoning, and buffers streamed text per attempt. Move the
boring parts into the framework.

New driver-style registry at providers/registry.go:
- providers.Register(name, Factory) — providers self-register in init()
- providers.New(name) → (Provider, defaultModel, envKey, error)
- providers.Lookup(name), providers.Known() (sorted), providers.KeyAvailable(name)
- Case-insensitive lookup; unknown name returns an error listing the
  registered names

Each shipped provider exposes EnvKey + DefaultModel constants and
self-registers from init() (gemini, nvidia, openrouter). Importing
`_ "github.com/erain/glue/providers/<name>"` makes the name resolvable.

WithFailover wrapper in package glue:
- glue.WithFailover(provs ...Provider) Provider
- Tries each provider in order; falls through on Stream error,
  immediate ProviderEventError, or empty stream
- Commits to a provider once any non-error event is observed —
  preserves the loop's "no half-streamed transcripts on retry"
  invariant, no mid-stream recovery
- All-providers-failed surfaces as *FailoverError with per-provider
  attempts (errors.As compatible)

README: new "Provider failover" subsection. design.md: provider
registry listed under package boundaries.

Migrating agents/glue-review off its hand-coded failover is filed as a
follow-up so this PR stays single-purpose.

Verification:
  go build ./...                                       # ok
  go vet ./...                                         # ok
  go test . ./loop ./providers/... ./tools/...         # ok (all green)
  go test ./agents/glue-review                          # one live test
                                                       # rate-limited
                                                       # by NVIDIA (HTTP 429),
                                                       # not a regression

Closes #90

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replace the canonical "switch e.Type { case EventTextDelta: ... }" block
that every CLI agent writes with two one-line options.

- glue.WithStreamWriter(io.Writer) mirrors EventTextDelta.Delta.
- glue.WithToolLogger(io.Writer) writes "[tool] <name>\n" on
  EventToolStart.
- Both nil-safe (nil writer = no-op so callers can pass conditional
  writers without branching) and silently drop writer errors —
  documented as convenience options, not delivery-guaranteed pipes.
- Compose additively with WithEvents and each other via a new
  promptConfig.auxEmits slice — installing one does not displace any
  other handler. WithEvents continues to use the existing single-emit
  field with last-wins semantics.

README "Streaming events" subsection rewritten to lead with the
helpers; the WithEvents path is kept as the richer-formatting fallback.

Verification:
  go build ./...                                    # ok
  go vet ./...                                      # ok
  go test . ./loop ./providers/... ./tools/...      # ok (all green)
  go test . -run "Stream|ToolLogger|StreamHelpers"  # ok

Closes #91

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Promote agents/glue-review/prompt.go into a small reusable framework
helper. Versioning prompts behind an embed.FS is a recurring pattern
(A/B test prompts, roll back without rebuild) and the agent's
implementation already encodes the right error semantics — unknown
version → list available, never silent fallback.

New package github.com/erain/glue/prompts:
- NewCatalog(fsys fs.FS, dir, defaultVersion) (*Catalog, error)
  validates that defaultVersion exists at construction time so
  misconfiguration fails fast.
- Get(version) (string, error) — empty version uses the default;
  unknown returns an error listing every available version.
- Versions() (sorted), Default()
- Nil-safe getters (return nil/"" / typed error)

Returned bodies are right-trimmed of newlines and re-suffixed with a
single newline so callers can append further instructions without
double-blank-line drift — matches the agent's existing format.

Templating / variable substitution remain out of scope: the catalog
returns raw bytes; rendering is the caller's job.

README: new "Versioned prompts" subsection. Migration of
agents/glue-review/prompt.go onto this catalog filed as a follow-up.

Verification:
  go build ./...                  # ok
  go vet ./...                    # ok
  go test ./prompts -count=1      # ok (8/8)

Closes #92

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Today loop.Run errors on max-turns and the partial transcript's last
assistant message carries StopReasonToolUse — indistinguishable from a
turn that legitimately ended on tools. An agent that wants to retry
with a higher budget can't tell "we ran out of turns" apart from
"model errored mid-stream".

Add StopReasonMaxTurns. When the loop exits via the budget guard,
mark the last assistant message in both Messages and NewMessages with
this stop reason before returning the (still-non-nil) error. The
transcript shape is unchanged; only the StopReason field on one
message gains a new value.

- New const loop.StopReasonMaxTurns ("max_turns"), re-exported as
  glue.StopReasonMaxTurns
- loop.Run tracks the index of the last appended assistant message
  and tags it on max-turns exit
- design.md "Agent Loop" section documents the new outcome

Verification:
  go build ./...                                          # ok
  go vet ./...                                            # ok
  go test ./loop -count=1 -run "MaxTurns|NaturalStop"     # ok
  go test . ./loop ./prompts ./providers/... ./tools/...  # ok

Closes #93

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@erain erain force-pushed the issue/93-stop-reason-max-turns branch from 3a2947a to 78f85d1 Compare May 7, 2026 14:15
@erain erain merged commit c4cc527 into main May 7, 2026
4 checks passed
@erain erain deleted the issue/93-stop-reason-max-turns branch May 7, 2026 14:17
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.

StopReasonMaxTurns: distinguish budget exhaustion from other stops

1 participant