Repository navigation
weft v0.3.0 — the Phase 2a parity round
weft v0.3.0 — the Phase 2a parity round, shipped with its review round.
The Phase 2a parity round (TODO §2a, docs/phase2a-plan.md — not
published with the repo; ADR 0013's 2026-09-22 amendment records the
decisions): the six capability items the cross-check against the
studied frameworks found missing, plus the three stances recorded in
the same ADR pass. Tags cut together: the root and the three adapters
at v0.3.0; mcp untouched at v0.1.0. Everything is additive — apidiff
is clean with no allowances. The cycle then went through the repo's
two-axis review process and the fix pass landed inside the same
release: the standards axis found no hard documented-standard
violations (one doc drift, two duplication smells, five
production-readiness findings — all fixed or pinned below), and the
spec axis confirmed the cycle faithful to the plan, with the one
deviation (the OutputDecoder's within-step rule) now a documented,
pinned decision.
Added — Tool-choice forcing (TODO §2a.1, ADR 0013)
weft.ToolChoice(ToolChoiceConfig{Mode, Name})— force a step
to call some tool (any), a named tool (tool), or none (none,
with the catalogue still advertised, so a prompt-cache prefix on the
tool definitions survives a no-calls final step). Works as agent
option, run option, and per-step viaPrepareStep— theThinking
dual shape. Mapped per provider (required/named/"none";
any/tool/none—disable_parallel_tool_usemerges onto the
chosen member;ANY/allowedFunctionNames/NONE). New conformance
casetool_choice_forcing(declared viaCaps.ToolChoice) also
asserts the request bytes carry the provider's field.
Added — Request params and the escape hatch (TODO §2a.3, ADR 0013)
weft.Params(RequestParams{Temperature, TopP, MaxTokens, Stop, Seed})— per-run/per-step sampling folding over the adapters'
construction options (nil keeps the construction default; a run
override replaces the struct whole). Adapters gainedTopP,Stop,
andSeedconstruction options where the vendor has them (anthropic
has no seed — dropped, documented; google narrows Seed/MaxTokens to
int32, failingErrUnsupportedpast the ceiling).ExtraBody(map[string]any)/ExtraHeaders(http.Header)per
adapter — the caller-wins valve for vendor knobs weft has no option
for: nested maps deep-merge, every other value replaces, and your
key wins on conflict (yours, not weft's — the default-bytes tests
do not cover what it sends). Construction-time only.
Added — Richer Usage (TODO §2a.4, ADR 0016)
Usage.CachedInputTokens,Usage.CacheWriteTokens,
Usage.ReasoningTokens— reporting subsets of the two totals
(which stay inclusive;UsageLimitandTotalare unchanged).
Filled by all three adapters (anthropic cache read/write, openai
cached/reasoning details, google implicit-cache and thought tokens).
On spans under their semconv/v1.41.0 names
(gen_ai.usage.cache_read.input_tokens,
…cache_creation.input_tokens,…reasoning.output_tokens), each
only when non-zero; the slog lines keep the two totals (line
stability). Wire is omitempty — old event JSON round-trips.
Added — Prompt caching (TODO §2a.2, ADR 0013)
anthropic.PromptCache()— opt-incache_controlbreakpoints at
the three stable prefix edges (system block, final tool definition,
trailing conversation edge); without it, no marker anywhere. Cache
writes bill 1.25×, reads 0.1×; see the README cost note and the
Usagesplits for the measurement.
Added — Externally-computed tool results (TODO §2a.5, ADR 0007)
weft.Resolve(callID, content)/weft.ResolveError(callID, content)— resume a parked call with a result computed outside the
process; the handler never runs, the content becomes the tool result
verbatim (capped byMaxResultBytesas any result), composes with
Approve/Denyin one resuming call. Behaviour note:
Resolveon a call that is not pending is a loud run error at
step 0 — a deliberate asymmetry withApprove/Deny, which ignore
unknown ids.
Added — Streaming partial structured output (TODO §2a.6)
weft.NewOutputDecoder[Out]()— a decoder value fed from the
run's events;Feedreturns best-effort partials assubmit_output's
arguments stream (lenient closed-prefix decode, hand-rolled in
partial_json.go, fuzzed),Resultfollows theOutputOfrule.
UI-only; never model-visible.
Fixed — the review round (2026-09-22)
ExtraBody/ExtraHeaderssnapshot at construction (all three
adapters): the options captured the caller's nested maps and header
slices by reference, so mutating them afterModel()raced
concurrent runs. Values are deep-copied when the option applies
(adapterkit.CloneJSON, header slices cloned); pinned per adapter
byTestExtraBodySnapshot.- A negative
RequestParams.MaxTokensfails the run at the step
that carries it — named in the error text, thevalidateToolChoice
stance. Before, the adapters improvised: openai forwarded it into an
opaque API error, google dropped it silently, anthropic folded it to
the default. Explicit zero keeps its documented per-adapter meaning.
Pinned byTestParamsValidation.
Changed — internal (the review round)
- The truncated-JSON closer lives once, in
internal/jsonclose,
imported by bothpartial_json.goandmw/repairjson.go. The two
copies had already drifted (the trailing-comma rule); the plan's
"duplicated on purpose" rationale was wrong —mwimporting an
internal/package of its own module breaks no rule (the
adapterkitprecedent). Behaviour change rides along for
closedPrefix: a trailing comma run now closes (it dropped back
to the member boundary before) — strictly more salvage. OutputDecoder's within-step rule is last-call-wins, pinned. A
freshsubmit_outputToolStartresets the buffer, soResult
follows the last call — theOutputOfrule the godoc always named;
the plan's "concatenates within a step" wording is amended in place
(two distinct calls' arguments never decode as one document).
BenchmarkOutputDecoderpins the documented cost shape (~0.9µs per
delta at a 3 KiB form; the per-delta re-close is quadratic in
submission size by design).- The loop skips the resume pending-id set when a run carries no
decisions, and acap-shadowing local is renamed.
Docs
- ADR 0013 amended (tool choice, params + escape hatch, cache markers,
the provider-executed-tools stance); ADR 0007 amended (resolve);
ADR 0014 noted (handoff isolation stands); ADR 0016 noted (usage
split attributes); ADR 0017 amended (replay key gainsToolChoice,
notParams). README: sampling/forcing paragraph, the prompt-cache
cost note, and "Seams are the product" (governance middleware stays
examples — TODO §2a.9). Godoc examples:ExampleToolChoice,
ExampleParams,ExampleOutputDecoder;examples/approvalgains
therun_sqlresolve path. - The review round's docs: ADR 0013's capability matrix gains the
Tool choice column (all three adapters ✓, with each provider's wire
tokens);RequestParams,ExtraBody/ExtraHeaders, and
OutputDecodergodocs state the new rules;
docs/phase2a-plan.md§8.1 carries the two dated amendments.
Live runs owed (carried debt — no provider keys on the build machine)
v0.3.0 ships on the offline suites, as v0.2.0 did (ADR 0013's
fixtures-and-live-runs rule). When keys exist, make live must prove:
the full conformance.Run table including tool_choice_forcing on
all three providers; PromptCache's measured payoff
(CachedInputTokens > 0 on step 2+ of a long-transcript run);
openai/google CachedInputTokens/ReasoningTokens non-zero against
real responses; and mw.Retry's structural HTTPStatus/RetryAfter
extraction against real openai-go / anthropic-sdk-go / genai error
values (the same §3.5/§4.1 debt, now grown by 2a's request-byte
changes).