Repository navigation
Releases: weftbase/weft
Releases · weftbase/weft
Release list
v0.21.0
Changed (breaking)
- Repository split (phases 2–3): the platform is weftbase. The module path is now
github.com/weftbase/weft(wasgithub.com/weftgo/weft) and the loop'sgithub.com/weftbase/weft/core(wasgithub.com/weftgo/weft/core); every import, the facades' generate lines and the instrumentation scope names (github.com/weftbase/weft/core,…/otel/slog,…/thread) follow. The platform packages moved, with their history, to their own repository and module,github.com/weftbase/weftbase(not released yet; its own version line; it imports weft, weft never imports it):studio(+studio/ingest,studio/runtime, the web source and the embedded dist),obsdb(+obsdb/sqlite,obsdb/clickhouse,obsdb/obsdbtest),runtime,scope,exporter, the CLI (cmd/weft→github.com/weftbase/weftbase/cmd/weftbase, the binaryweftbase), the examplesstudio-local,devtools-viteandthread/examples/approvals(now weftbase'sexamples/approvals), ADRs 0018 and 0024 (one-line pointers stay here), theweft mcprecipe and the platform's testing sections.weft studioandweft devleave weft with the platform: traces go to any OpenTelemetry backend (OTEL_EXPORTER_OTLP_ENDPOINT,otel.OTLP), and Weftbase's console is coming (weftbase is not released yet);WEFT_STUDIO_URL/WEFT_STUDIO_TOKEN/WEFT_STUDIO_ADDR/WEFT_DB/WEFT_DISCOVERYareWEFTBASE_URL/WEFTBASE_TOKEN/WEFTBASE_ADDR/WEFTBASE_DB/WEFTBASE_DISCOVERY(no fallback), the local data directory./.weft/is./.weftbase/(default database./.weftbase/weftbase.db), panel tokens arewb_pt.….otel.Localand friends, which phase 1 moved toweft/exporter, aregithub.com/weftbase/weftbase/exporter. The framework dropsclickhouse-goandfsnotifyfrom its requirements.make layeringnow fails when anything here importsgithub.com/weftbase/weftbase; the Makefile's studio, vacuum and mcp-check targets and CI's clickhouse, studio and devtools-vite jobs moved with the platform. Step by step:MIGRATION-0.21.md. - otel (repository split, phase 1: weft emits; the platform stores, shows and controls):
weft/otelno longer knows Weft Studio or its store. It configures from the standard OpenTelemetry variables only (OTEL_EXPORTER_OTLP_ENDPOINTand_HEADERS,OTEL_SERVICE_NAME,OTEL_RESOURCE_ATTRIBUTES,OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT) plusWEFT_ENVfor the development gate, and no longer importsobsdbor the discovery file. Removed fromotel:Local,Studio,LocalDB(package function andPipelinemethod),StudioEndpoint(both),NoDiscovery,FromSDKSpans,FromSDKRecords, theWEFT_STUDIO_URL/WEFT_STUDIO_TOKEN/WEFT_DBenvironment destinations, the discovery-file join andInstall's zero-config fallback to the local sink —otel.Install()with nothing configured now records nothing, says so in one INFO line and returns a no-op. Several destinations at once, each with its own content policy, raw bodies, the dev detail and the slog bridge are unchanged. - exporter (new, platform; moves to the weftbase repository with
studio,obsdbandruntime): the local developer platform's destinations on top ofotel. Migration:otel.Local(p)→exporter.Local(p),otel.Studio(url, tok)→exporter.Studio(url, tok),otel.LocalDB()→exporter.LocalDB(),p.LocalDB()→exporter.LocalDBOf(p),otel.StudioEndpoint()→exporter.StudioEndpoint(),p.StudioEndpoint()→exporter.StudioEndpointOf(p),otel.NoDiscovery()→exporter.NoDiscovery(),otel.FromSDKSpans/FromSDKRecords→exporter.FromSDKSpans/FromSDKRecords; for today's zero-config developer behaviour (WEFT_STUDIO_URL/WEFT_STUDIO_TOKEN, the discovery file of a runningweft studio,WEFT_DB, the local sink when nothing is configured) useexporter.Install(...)/exporter.Start(ctx, ...)in place ofotel.Install/otel.Start; they take the sameoteloptions.exporter.StudioEndpoint(whatruntime.Installdials) is known for any installed pipeline with anexporter.Studiodestination, a plainotel.Installincluded. Studio, the runtime link, theweftCLI and the examples use it. - Every framework package (everything but
studio,obsdb,runtime,scope,exporter,cmd/weft, their internal packages and the platform examples), its tests included, no longer imports a platform package; the adapters',otel's andthread's tests that read records back fromobsdbrun against in-memory exporters, and their store round trips moved toexporter's tests.
Fixed
- thread: a decider's context no longer hides what the parked turn ran under. A resume takes the parked turn's context values only where the deciding call's context has none, and metadata is one value: an inbox handler that tagged its request context with
core.WithMetadataand calledDecideran the resume without the parked turn's tenant pairs (likewise an inheritedParkAllExceptlist under one of the decider's own). The settings recorded on the requests now ride every resume, in-process too; the parked metadata wins per key, the decider's merges under it (ADR 0021, amendment 2026-11-03). A deferred steer's follow-up had the same hole against the turn it aimed at, and keeps that turn's metadata and park rule the same way (ADR 0019). - thread: a call parked under
Quorum(n)needs n approvals however the session is reopened.Quorumis a session option and was not recorded: a process that opened the session without it (another worker, another build) resolved the call on one approval. The request now records its quorum (ApprovalRequestEntry.Quorum, JSONquorum;Request.QuorumonPending), and the stricter of it and the session's own decides; a pool mirror carries the child's and records the parent's (ADR 0021, amendment 2026-11-03). - thread: an approved call no longer runs on a build whose tool changed under it. A parked call resumed after a deploy or a restart onto new code ran its approval against whatever tool now had its name: a tool whose input schema changed in a way the old arguments still decoded into ran the call under a contract nobody approved, and a removed or renamed one read
NO_SUCH_TOOLwith nothing in the audit trail. The request now records its tool's input schema hash (ApprovalRequestEntry.SchemaSHA256, JSONschema_sha256); on a resume whose agent lacks the tool or has another schema, the approved call is denied with a model-visible reason and audited (resumestep, outcomedrift). Denied and resolved calls, and a change to the approval mark alone, are unaffected (ADR 0021, amendment 2026-11-03). - core: an agent's manifest — and
weft.manifest.hash, its sha256 — now changes when any agent below it in the delegation tree does. ASubagenttool named its child only, so a child's (or grandchild's) prompt, tools or model could change while the parent kept its hash, and a version pinned by that hash did not pin what the parent delegates to. Each Subagent tool entry now carriessubagent_hash, the child's own manifest hash (named or unnamed child), which pins its children in turn; the manifest still does not recurse, and the hash is still the sha256 ofManifest(a)(ADR 0012, amendment 2026-11-03). - thread: a deny-grant's decision is recorded with
Viagrant(it had none); an approval grant's always was. - thread: a resume after a restart no longer drops the park rule the parked turn ran under. A turn sent with
thread.RunOptions(core.ParkOn(...))orcore.ParkAllExcept(...)(or one inheritingParkAllExceptfrom a delegating run, as athread/poolchild does) parked a call; the process restarted; the reopened session was decided — and the resume ran without the rule, so the next call to a tool the rule parks ran unasked. The rule is now recorded on the boundary's request entries and put back on a resume that lost the parked turn's options. New:core.RunResult.Park/weft.ParkRule, the rule a run ran under as data, andParkRule.Options()to re-apply it. - thread: a resume after a restart, and a queued send or steer restored by
Open, no longer drop the rest of what they were sent under. The parked or queued send'sOnlyToolsnarrowing, a loweredMaxStepsorParallelism, itsInstructions,Thinking,ToolChoiceandParamsoverrides, and its caller metadata (thread.RunOptions(core.Metadata(...))and the context'score.WithMetadatapairs, a tenant among them) were process state: after a restart the resumed run offered tools the send had narrowed away, spent past the lowered budget and lost its tenant tag, and a send queued behind an open boundary underParkAllExceptran every tool unasked. They are now recorded as data —ApprovalRequestEntry.Runon the boundary's requests (replacing the unreleasedApprovalRequestEntry.Park, JSONpark→run.park) andReceiptEntry.Runon queued and accepted receipts — and put back on the restored run. A restored narrowing is never dropped to let the run start: anOnlyToolsname the reopening agent no longer registers, or a budget above its own, fails the run withErrInvalidRunOptionbefore any model call.UseModelandOnMessagesare not data and stay lost: a run restored after a restart runs on the agent's own model (ADR 0021).
Added
- thread:
Grant.WhoandGrantEntry.Via— a grant records its grantor and the door it came through (userforSession.Grant; anApproveAlwaysgrant takes the minting decision'sWho— its signing key id when the signature named no one — andVia). Every decision a grant makes is recorded with the grantor as itsWho, so an automatic approval is attributable to the person who made the grant; thegrant.mintrecord carrieswhoin its content (ADR 0021, amendment 2026-11-03). - thread:
Decision.RequestIDandDecision.ArgsSHA256— a decision can name the occurrence and the arguments i...
core v0.21.0
See CHANGELOG.md, 0.21.0 — 2026-11-03.
v0.20.3
Fixed
- thread/pool (F1, found by the weft-arena torture tier's kill loop):
Recoversettling a child as over (failed with "no turn was recorded", or finished) now ends that child's own unsettled delegations first, depth first, asCancelandClosedo — a parked sync grandchild is canceled at its boundary (its pending calls denied in its session) and settlescanceledin the child's ledger, its requests there denied; a grandchild whose run is over settles from its file. Before, a kill while a middle child was mid-run left its delegationrunning/parkedfor good and the grandchild's approval offered nowhere. A parked async grandchild is left parked (D4); a child whose subtree cannot be reopened (no agent) stays unsettled and is reported withErrNoAgent; a kill inside the cascade (between a grandchild's settlement and the denial of its requests) is finished by the nextRecover. ADR 0022 amendment 2026-10-31. - thread/pool (F2, same): a parked child whose parent's delegating turn never wrote its turn entry (the kill landed between the mirror and the park record) is reattached by
Recoveras a delegation by no call: its request stays offered, a decision resumes it, the approved call runs and its answer settles the receipt. Before, it was reattached under the call, which nothing had parked or ever would: the decision was recorded but the child never resumed, andClosestalled for its 30 s bound. The parent's delegating call is answered as interrupted by its next run's input repair, as any call a crash cut off; the child's answer is on the receipt (ADR 0022 §J's rule for an abandoned delegating call).
Docs
- core, README,
docs/agent-loop-deep-dive.md: theSubagentgodoc,CodeSubagentPending, the README's subagent section and the deep dive point atweft/thread/poolwhere they state that a bareSubagentnever propagates a child's pending approvals (the deep dive still deferred it toruntime);thread/poolhas propagated them since ADR 0022 §7. The model-visibleSUBAGENT_PENDINGtext is unchanged.
core v0.20.3
See CHANGELOG.md, 0.20.3 — 2026-10-31.
v0.20.2
Fixed
- Studio,
weft export --wefttest(P020.2): save as test on a run that offered no tools writes a test that compiles — it declared a tool-result handler it never used (declared and not used: answer); now it declares and imports only what the test uses. - Studio (P020.2): the runs list fits its frame at 1280 to 1800 px — a run that delegated shows its subagents' tokens on a second line under its own instead of beside them, a token pair breaks only at its dot ("20 in ·" over "10 out", never "10" apart from "out"), the agent column holds a longer name ("procurement-desk", it read "procurement…"), the run column is 14 rem wide, 16 rem from 1360 px and 20 rem from 1680 px (14 rem again from 1536 px, where the session and model columns join), and the refresh button stays at the end of the first row instead of wrapping onto a row of its own.
- Studio (P020.2): a derived run's fix says what to set without a repository path ("(docs/record-contract.md §9)" is gone), and its waterfall starts under the view's controls — the layout toggle ends that row instead of standing alone above the waterfall.
- Studio (P020.2): the run page's waterfall keeps a duration on one line ("21.3 ms", it wrapped "21.3" over "ms"), shows a span's whole name when its row has room ("execute_tool lookup_order"), and an attempt's name beside its error chip ("attempt 1 · script" with
stream_idle, it read "attempt 1 · s…"). - Studio (P020.2): the sessions list breaks "awaiting a decision · 1 call" and "1 exchange · 2 turns" only at their dot, and its find box shows "find title, prompt, id" whole; the Agents page's numbers end every bar at one place, whatever the width of its figure.
- devtools panel (P020.2): the header shows each of its figures — the tokens, the cost, the unpriced count — whole or not at all, never cut mid-figure ("≥1.0M in …", "2… $.."), the title keeping its room (a page of runs' title keeps the space before its count, "170907 · 928 runs"); the narrow dock's run picker puts the status, steps and duration before the title or a replay's lineage, so a cut title no longer takes them ("· 3 st"); a running turn's duration reads "…" instead of "—"; a parked call's arguments read on one line, as in the step's call list, instead of a pretty-printed block hanging off the tool's name; the decision field's placeholder is "result or deny reason", shown whole (0.20.1 said "result to resolve with" was shown whole; the placeholder was longer and cut).
v0.20.1
Fixed
- Studio (P020.1): the run page's waterfall draws only the time-axis ticks its track's width holds — a short run's or a narrow track's labels no longer print over each other; a derived run's root row counts its steps as its header does ("2 steps", it read "0 steps"); an attempt's error chip (
model_error,stream_idle) stays whole beside its name instead of under its bar's marker. - Studio (P020.1): the runs list and the sessions list fit a 1280 px window with the sidebar open — the runs list's model column shows from 2xl (1536 px) and its status, tokens and cost wrap, so the compare pick no longer pushes tokens off the edge; the sessions list's status and turns wrap and its agent truncates, so tokens, cost and last seen stay in the frame.
- Studio (P020.1):
/agents?agent=<name>keeps the named card in view while the numbers above it load and lay out (with many agents it ended far down the page); the Agents page says a registration time as Studio says times (relative, the stamp on hover — not the server's raw RFC 3339 with nanoseconds), points at keeping a proposed prompt in user words (it named an internal design doc), and pads the selected card inside its ring. - Studio, devtools panel (P020.1): a plain Go error a tool layer hid (
mw.MapErrorsover anerrors.Newvalue) readsINTERNAL ← plain error, notINTERNAL ← *errors.errorString(anerrors.Joinor two-%werror,joined errors); the recordedweft.tool.error_maskedis unchanged and stays on the hover. - Studio (P020.1): the call graph sizes its nodes and column gaps to their words — an edge label no longer prints over a node, and an MCP server node's failure line ("1 unreachable · 1 refused · 1 tool error") is no longer cut; a node grows to 420 px at most, a longer name cut with an ellipsis and whole on hover.
- devtools panel (P020.1): a page of runs' header reads "batch · N runs" at every dock width — the id is cut before the count, and the token, cost and unpriced lines give way before the title; the Timeline axis names its two ends instead of five overprinted ticks; "has error" is a normal checkbox on its label's line; the replay drawer's engine options no longer wrap into two overlapping columns in a 640 px dock; a parked call's "result to resolve with" field shows its whole placeholder.
v0.20.0
Added
- core, mw, otel, obsdb (U23.1; UX-S2-01, UX-E-08, UX-E-09, UX-E-10, UX-E-15, UX-A-43, UX-S2-04): the middleware onion recorded. The manifest lists each agent's
model_middlewareandtool_middlewareouter to inner (a tool's ownmiddleware) and itshooks(prepare_step,tap,on_run_end,tool_source), each{name, params?, source?, opaque?}: a model layer describes itself with the optionalDescribe() core.MiddlewareInfo(mw.Retryits budget and delays,mw.Fallbackits backups,mw.Log,mw.RepairJSON), others by type — a layer withoutUnwrapends the walkopaque— and functions by name, with their source when not weft's own. A manifestsource(a tool's, a hook's, a middleware's) is one form on every host, never an absolute path: the defining package's import path and the file's base name (github.com/x/app/tools.go:12; a module-cache file by its module path, the linker's%2eunescaped). The spans say what a layer decided:core.ToolReportFromContext(ToolReporter.Denied,Masked) putsweft.tool.denied_by(mw.Allow:allow) andweft.tool.error_masked(mw.MapErrors: the hidden error's kind — a standard sentinel througherrors.Is, an optionalErrorKind() string— never its text; a mapping that only wraps reports nothing) onexecute_tool;core.Reporter.Repair(mw.RepairJSON) aweft.repairevent onchatandweft.tool.args_repairedon the call;core.AttemptLayerRetrymarksmw.Retry's retried requests;core.DenyBynames a deny grant on a resume's denied span (weft.tool.denied_bygrant,weft.grant.id; elseapproval), whichweft/threadpasses.otel.DevDetail()(dev-gated likeRawBodies;LoggerProvider.DevDetail) adds onelayer <name>span per layer per call (weft.layer.name/kind/index; a panic through a layer ends itError/panic), the repair's original arguments and a contained tool or middleware panic's stack (aweft.panicevent onexecute_tool, 16 KiB), for the Local and Studio destinations with content on only; never by default. The request record gainswire {server_address, extra_body_keys, extra_headers, dropped}(UX-S2-09): the adapters' endpointhost[:port],ExtraBodykeys andExtraHeadersnames — names only, bounded — through the optionalWireFacts()method (openai,anthropic,google; the primary's behind anmw.Fallback). obsdb derives therepairedcounter. Record contract §3 (thelayerspan, theweft.repairevent, theexecute_toolrows), §6, the manifest's onion, theonionconformance fixture. - mcp, core, obsdb (U23.2, O8; UX-S2-08, UX-A-43, UX-S2-04): MCP identity.
mcp.Server(name)names the server an import comes from — every imported tool'sOriginreadsmcp:<name>— andmcp.Transport(kind)how it is reached;core.OriginDetail(core.SourceDetail{Server, Transport, Prefix, RemoteName, Skipped})carries the parts. The tools record gainssource_detailper tool andskipped[] {name, reason, source}(the tools the listing could not import — a server none of whose tools imported included, throughcore.SkippedImportsandmcp.ImportError.Option()— once per source, at most 64,skipped_morethe rest, names cut at 128 bytes; neither in the catalog hash);execute_toolcarriesweft.tool.origin(the tool's Origin when not local:subagent,mcp:<server>). An MCP call's failure is one of threeerror.types, contract rows:mcp_transport(no answer: a closed session, a stdio stream at EOF, the go-sdk's refused, dropped or TLS-failed streamable-HTTP connection or a 5xx / 429 without a JSON-RPC body —ErrRejected−32005, −32004, −32003),mcp_rpc_error(the server's own JSON-RPC error answer: its handler failed, the tool is not listed) andmcp_tool_error(anisErrorresult). obsdb reads it all (ToolsRecord.Skipped,ToolEntry.SourceDetail);/api/runs/{id}/toolsserves it. Record contract §3, §4, §10 (mcp_mesh); ADR 0015 and 0028 amendments. - Studio, devtools panel (U23.2; UX-S2-08, UX-A-43, UX-S2-04): the onion shown. The drawer's "what wrapped this call" — a model or tool call's chain outer to inner from the run's stored manifest (its
Timeoutoutermost), each layer's decision from its spans (Retry's attempts, waits, HTTP status and give-up, Fallback's served model, RepairJSON's "repaired 1 arg", Allow's "✋ denied by policy", MapErrors' "INTERNAL ← io.EOF"), a decision shown once per layer kind (a secondmw.Retrysays the record cannot tell which), an idle layer greyed, a click opening its attributes; without a stored manifest only the layers that reported, said so. The Agents card draws each agent's model and tool chains; the story's step chips (↻ retry ×2,⇄ fallback,✎ repaired 1 arg) and tool-row chips (✋ allow,⇣ io.EOF,✎ repaired), on the panel too, with its MCP server; a denied call reads "denied by policy", an MCP failure "MCP unreachable", "MCP refused the call" or "MCP tool error" on both renderers; the call graph drawsmcp:<server>from the span's origin — a child run's calls included — with its calls, slowest call and failures by kind ("1 unreachable · 1 refused · 1 tool error"), and a caller-linked run under its caller; the Request pane's catalog names each tool's origin in parts and lists the skipped tools greyed (the panel's Request tab too).lib/onion.tsserves both renderers. Glossary:middleware,MCP server. - mcp, core, obsdb (U33.1; UX-S2-08's remainder): a served agent's run is linked to its MCP caller. Under the new
mcp.CallerMeta()option (off by default — the weft run and call ids and the trace state name your runs to the server, so send them to servers you run, not to third parties)mcp.Tools' imported tools send their caller inparams._meta: theexecute_toolspan's W3Ctraceparent(andtracestate) andmcp.MetaCaller("dev.weftgo/caller":{run_id, call_id}).mcp.Serve(andAddTools' tools) read it back: the served run'sinvoke_agentspan starts its own trace with an OpenTelemetry link to the caller's span — never the caller's child, so a client's "not sampled" flag never decides whether the server records the run — and recordsweft.caller.run.id/weft.caller.call.idwithweft.caller.remote"true": a link, neverweft.parent.*(O11; record contract §2, theremote_callerfixture). A remote link is the caller's own statement, so readers never place a run by it: obsdb leavescaller_run_id/caller_call_idempty for it on both backends, the run stays top-level, and Studio says "started over MCP by run … in another process (not linked)". A missing or malformed_metalinks nothing and refuses nothing. Newcore.WithCaller(ctx, core.Caller{RunID, CallID, Span})(andweft.WithCaller,weft.Caller): every run started directly onctx(not inside a tool call) records that caller; no run started inside it inherits it. The mcp package now imports the OpenTelemetry trace API (no SDK). - core, mw, obsdb, Studio (U30.3; UX-S2-02's remainder):
core.AttemptInfo.GiveUp(AttemptGiveUpBudget/AttemptGiveUpNotRetryable/AttemptGiveUpRetryAfter, re-exported byweft) becomes the attempt span'sweft.attempt.giveup_reason:mw.Retrysays why it stopped there — its retries spent, a failure its classifier does not retry, a retry-after pastMaxWait(behaviour unchanged). It is that layer's stop: aRetryunder aFallbackmarks where it stopped and the call may go on to the backup; aRetryover a self-reporting model gives no reason. The step route'sattempts[]carrygiveup_reason. Studio's attempts list words each cause ("rate limited (429)") and ends a failed call with "gave up: retries spent (2 attempts)" ("gave up after N attempts" on an older record; never when the last attempt ended on the run's cancellation or deadline); the run tree draws an attempt'sweft.attempt.wait_msas a hatch before its bar. - core, obsdb, Studio (U30.3; UX-S2-07's, UX-S1-06's remainders): the request record's body carries
parallelism, the step'sParallelismdial (the agent's, a run's lower one, 1 underSequential()), drawn "parallelism N" beside a step's tool lanes, aSequentialtool's row "▮ sequential". AnOutputagent's run names its typed answer's type oninvoke_agent—weft.output.type, package-qualified (main.Invoice), at most 128 bytes cut at a rune boundary — and the manifest'ssubmit_outputtool carries it asoutput_type; Studio's OUTPUT card shows it and the Agents card marks the tool "typed output · main.Invoice". The manifest carries an agent's configured sampling knobs asparams(itsweft.Params, by the request record's names; absent when it sets none). - obsdb, Studio, devtools panel (I6; UX-H-01, UX-H-02, UX-H-03, UX-H-04, UX-S4-02, UX-S4-03): derived runs from the producer's content (docs/record-contract.md §9). A top-level
invoke_agentspan withoutgen_ai.conversation.id(no other agent span stored above it) is a derived run with no session; one nested under another agent still folds into the enclosing run, and a derived row an export folds later (an inner agent exported before its parent) is dropped — on ClickHouse reconciled against a fresh read of the trace, so the last of two racing writers leaves no stale row, and announced live as a run frame whose row isremoved(obsdb.RunRow.Removed), which the runs list and the Live page drop. A derived row'smessage_countcounts its chat spans carrying message content (obsdb.StockHasMessages).steps/{n}serves a derived call'sargsandresultfromgen_ai.tool.call.arguments/gen_ai.tool.call.resultwhen the producer captured content (obsdb.StockToolContent; elseargsnulland noresult), and the run page's story draws each step's USER / ASSISTANT / tool-call rows from its chat span'sgen_ai.input.messages/gen_ai.output.messages/gen_ai.system_instructions(the trace page's chat renderer,components/studio/genai-chat.tsx). The derived badge...
core v0.20.2
See CHANGELOG.md, 0.20.2 — 2026-10-27.
core v0.20.1
See CHANGELOG.md, 0.20.1 — 2026-10-27.
core v0.20.0
See CHANGELOG.md, 0.20.0 — 2026-10-27.