loop: StopReasonMaxTurns to flag budget exhaustion (closes #93)#100
Conversation
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>
|
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... The main appears to be a 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 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] $. It seems like the user contains mixed content including:
There's a lot of garbled text, but at the end thereH(the text contains"Bwhat needs.",
The Contactually, let me parse this more carefullyI need to clarify that I user is proficient in. 🤖 Posted by glue-review. |
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>
3a2947a to
78f85d1
Compare
Summary
loop.StopReasonMaxTurns(re-exported asglue.StopReasonMaxTurns).loop.Runtags the last assistant message in bothMessagesandNewMessageswith this stop reason when the budget is exhausted while tool calls are pending. Transcript shape and the returned error are otherwise unchanged.Use case: an agent that wants to retry with a higher budget can now distinguish "ran out of turns" from "model errored mid-stream" —
StopReasonToolUsepreviously 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 staysStopReasonStopgo test . ./loop ./prompts ./providers/... ./tools/...— all green🤖 Generated with Claude Code