Status: proposal
Owner: agent-go
Updated: 2025-09-23
- Replace
core/commandwith a functional-option loop engine (WithModel,WithTools,WithSystemPrompt, etc.). - Provide iterator-style
Step(ctx, opts...)returning turn metadata, cost, cached tokens, and session context snapshots. - Surface
ToolCalls()handles and streaming integration (thinking, deltas, tool-call announcements) throughStreamIterator. - Track per-step and cumulative model cost via a
Currencyalias with default USD handling. - Support cached token accounting to inform compaction and budgeting decisions.
Affects: core/loop, core/tools, core/session, framework integrations, examples, docs.
Non-Goals: Provider schema changes, multi-model orchestration (recorded as TODO), backwards compatibility adapters for core/command.
- Duplicate loop logic between
core/loopandcore/commandcauses drift in tooling, streaming, and submission handling. - Developers lack a clean iterator primitive; existing APIs force bespoke orchestration for each consumer.
- Cost/budget accounting is fragmented; cached token behaviour is opaque.
- Streaming UX (reasoning/tool-call announcements) needs a consistent contract.
- One canonical loop engine in
core/loop;core/commanddeleted. Step(ctx)returns turn metadata, cost (Cost(CurrencyUSD)), cached tokens, and context message accessors.ToolCalls()returns handles with immutable metadata +Run(ctx); streaming exposes same handles.- Engine tracks cumulative cost and cached-token totals accessible post-run.
- Existing examples/framework compile against the new API.
- A1. Implement functional options (
WithModel,WithSessionContext,WithTools,WithSystemPrompt,WithPrompt,WithMaxSteps). - A2. Define step options (
WithStreamIterator, future hooks) and ensure defaults require minimal setup.
- B1. Refactor
core/loopto use the option-driven config and maintain counters/state. - B2. Implement
Step(ctx, opts...)returningStepResultwith turn ID, usage, cached tokens, cost, context message, and tool handles.- B2a. Record TODO for multi-model fan-out (deferred).
- B3. Add
StepsTaken(),StepsRemaining(),MaxSteps()and guard provider invocations when limits hit.
- C1. Add pull-based
StreamIteratorwith typed events (thinking/deltas/tool calls). - C2. Ensure streaming option hooks into
Stepwithout impacting non-streaming performance. - C3. Document async draining helper (
streams.Drain()).
- D1. Update existing loop tests to cover new step contract, cost, cached tokens.
- D2. Port consumers (
examples, framework, SDK) to the new API. - D3. Remove
core/commandonce dependencies migrate.
- E1. Expand unit tests (provider-only, streaming, tool execution).
- E2. Benchmark iterator vs legacy loop to confirm parity.
- E3. Document usage in
docs/loop.mdand README snippets.
- API Drift: Keep a compatibility checklist for downstream integrations; migrate sequentially.
- Performance Regression: Benchmark
Stephot paths before/after refactor; optimise cached computations. - Streaming Complexity: Provide simple defaults; streaming remains opt-in via
StreamIterator.
- Phase 1: Define options, implement
Stepskeleton, add tests (WS-A, partial WS-B). - Phase 2: Integrate streaming, tool handles, cached token/cost metrics (WS-B/C).
- Phase 3: Migrate consumers, remove
core/command, update docs/benchmarks (WS-D/E).
- Stable
core/sessionAPIs (see session-context plan). core/toolsregistry updates to produce handles.- Agreement from framework/SDK owners to adopt the iterator loop.
- All workstreams implemented and tests passing.
- Consumers migrated;
core/commandremoved. - Documentation/benchmarks updated.
- Cost/cached-token metrics exposed and validated.
// Configuration ------------------------------------------------------------
engine := loop.NewEngine(
loop.WithModel(model),
loop.WithTools(registry),
loop.WithSystemPrompt("You are a careful shell assistant."),
loop.WithPrompt(session.NewContextMessage(session.RoleUser, "List directories")),
loop.WithMaxSteps(16),
)
// Streaming + step iteration ----------------------------------------------
streams := loop.NewStreamIterator()
step, err := engine.Step(ctx, loop.WithStreamIterator(streams))
if err != nil {
log.Fatal(err)
}
log.Printf("turn=%d remaining=%d cost=%.4f %s cachedTokens=%d",
step.TurnID(), step.StepsRemaining(), step.Cost(loop.CurrencyUSD), loop.CurrencyUSD, step.TokensCached())
for streams.Next() {
switch ev := streams.Event().(type) {
case loop.StreamThinking:
log.Println("thinking:", ev.Text)
case loop.StreamMessageDelta:
log.Println("delta:", ev.Text)
case loop.StreamToolCall:
if err := ev.Handle.Run(ctx); err != nil {
log.Println("tool (stream) failed:", err)
}
}
}
if err := streams.Err(); err != nil {
log.Println("stream error:", err)
}Important: Keep this document updated during implementation. Record progress, discoveries, blockers, and next steps in the log below.
- Completed: Initial plan drafted; API goals captured.
- In Progress: Awaiting review for approval.
- Blockers: None.
- Discoveries: Need follow-up plan for multi-model support.
- Next Steps: Align with session/tool/stream subplans; begin option implementation once approved.