Skip to content

v0.20.0

Choose a tag to compare

@wajihtba wajihtba released this 27 Oct 14:52
· 71 commits to main since this release

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_middleware and tool_middleware outer to inner (a tool's own middleware) and its hooks (prepare_step, tap, on_run_end, tool_source), each {name, params?, source?, opaque?}: a model layer describes itself with the optional Describe() core.MiddlewareInfo (mw.Retry its budget and delays, mw.Fallback its backups, mw.Log, mw.RepairJSON), others by type — a layer without Unwrap ends the walk opaque — and functions by name, with their source when not weft's own. A manifest source (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 %2e unescaped). The spans say what a layer decided: core.ToolReportFromContext (ToolReporter.Denied, Masked) puts weft.tool.denied_by (mw.Allow: allow) and weft.tool.error_masked (mw.MapErrors: the hidden error's kind — a standard sentinel through errors.Is, an optional ErrorKind() string — never its text; a mapping that only wraps reports nothing) on execute_tool; core.Reporter.Repair (mw.RepairJSON) a weft.repair event on chat and weft.tool.args_repaired on the call; core.AttemptLayerRetry marks mw.Retry's retried requests; core.DenyBy names a deny grant on a resume's denied span (weft.tool.denied_by grant, weft.grant.id; else approval), which weft/thread passes. otel.DevDetail() (dev-gated like RawBodies; LoggerProvider.DevDetail) adds one layer <name> span per layer per call (weft.layer.name / kind / index; a panic through a layer ends it Error / panic), the repair's original arguments and a contained tool or middleware panic's stack (a weft.panic event on execute_tool, 16 KiB), for the Local and Studio destinations with content on only; never by default. The request record gains wire {server_address, extra_body_keys, extra_headers, dropped} (UX-S2-09): the adapters' endpoint host[:port], ExtraBody keys and ExtraHeaders names — names only, bounded — through the optional WireFacts() method (openai, anthropic, google; the primary's behind an mw.Fallback). obsdb derives the repaired counter. Record contract §3 (the layer span, the weft.repair event, the execute_tool rows), §6, the manifest's onion, the onion conformance 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's Origin reads mcp:<name> — and mcp.Transport(kind) how it is reached; core.OriginDetail(core.SourceDetail{Server, Transport, Prefix, RemoteName, Skipped}) carries the parts. The tools record gains source_detail per tool and skipped[] {name, reason, source} (the tools the listing could not import — a server none of whose tools imported included, through core.SkippedImports and mcp.ImportError.Option() — once per source, at most 64, skipped_more the rest, names cut at 128 bytes; neither in the catalog hash); execute_tool carries weft.tool.origin (the tool's Origin when not local: subagent, mcp:<server>). An MCP call's failure is one of three error.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) and mcp_tool_error (an isError result). obsdb reads it all (ToolsRecord.Skipped, ToolEntry.SourceDetail); /api/runs/{id}/tools serves 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 Timeout outermost), 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 second mw.Retry says 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 draws mcp:<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.ts serves 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 in params._meta: the execute_tool span's W3C traceparent (and tracestate) and mcp.MetaCaller ("dev.weftgo/caller": {run_id, call_id}). mcp.Serve (and AddTools' tools) read it back: the served run's invoke_agent span 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 records weft.caller.run.id / weft.caller.call.id with weft.caller.remote "true": a link, never weft.parent.* (O11; record contract §2, the remote_caller fixture). A remote link is the caller's own statement, so readers never place a run by it: obsdb leaves caller_run_id / caller_call_id empty 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 _meta links nothing and refuses nothing. New core.WithCaller(ctx, core.Caller{RunID, CallID, Span}) (and weft.WithCaller, weft.Caller): every run started directly on ctx (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 by weft) becomes the attempt span's weft.attempt.giveup_reason: mw.Retry says why it stopped there — its retries spent, a failure its classifier does not retry, a retry-after past MaxWait (behaviour unchanged). It is that layer's stop: a Retry under a Fallback marks where it stopped and the call may go on to the backup; a Retry over a self-reporting model gives no reason. The step route's attempts[] carry giveup_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's weft.attempt.wait_ms as 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's Parallelism dial (the agent's, a run's lower one, 1 under Sequential()), drawn "parallelism N" beside a step's tool lanes, a Sequential tool's row "▮ sequential". An Output agent's run names its typed answer's type on invoke_agent — weft.output.type, package-qualified (main.Invoice), at most 128 bytes cut at a rune boundary — and the manifest's submit_output tool carries it as output_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 as params (its weft.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_agent span without gen_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 is removed (obsdb.RunRow.Removed), which the runs list and the Live page drop. A derived row's message_count counts its chat spans carrying message content (obsdb.StockHasMessages). steps/{n} serves a derived call's args and result from gen_ai.tool.call.arguments / gen_ai.tool.call.result when the producer captured content (obsdb.StockToolContent; else args null and no result), and the run page's story draws each step's USER / ASSISTANT / tool-call rows from its chat span's gen_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's reason and fix are said once, in the run header — the fix speaks to a Python / TypeScript developer: content capture (OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT=true), gen_ai.conversation.id for a session, "weft's own records need a weft SDK (Go today)" — and every block read off the spans carries the chip "derived · from span attributes" alone; a derived run's replay, save as test, open in app and compare are hidden on both surfaces, said once ("they need weft's records"). /api/traces and /api/traces/{id} name a trace's derived runs in run_ids (stored rows only), and /api/traces/{id} answers not_run {reason, fix} (obsdb.StockNotRun) when the trace makes no run — "model or tool calls only: no invoke_agent span opens a run", "its agent span has not arrived". /api/traces?not_run=1 (obsdb.TraceQuery.NotRun) lists the GenAI traces that make no run, and the runs page says how many over the last 7 days, linking that list. Conformance: stock_genai with content, stock_genai_noconv, stock_genai_nested (both backends).
  • Studio, studio (U33.2; UX-H-10, UX-H-07, UX-H-35, UX-S5b-04): GET /api/runs/{id}/logs rows carry event_name, the OTLP record's EventName (absent on a plain line; the logs view also reads an older SDK's event.name attribute), and the MCP logs tool answers it; a GenAI log event (gen_ai.<role>.message, gen_ai.choice) is drawn in the Logs view as that message — role, content, an assistant's tool calls, a choice's finish reason — and a derived run's Story lists those messages below its step cards ("messages the app logged"; badged when cut, never drawn twice). An unknown session or experiment figure carries cost_partial {usd, priced, of} — the sum and count of the parts whose whole bill (own + delegated) is known, absent when none is — drawn on Studio's sessions list, session page and experiments list as the panel's header draws its turns, by one rule on both surfaces (a part whose delegated figure is unknown adds nothing): "≥ $x · k of n turns priced", "≥ $0 · k of n turns priced" when the priced parts are $0, "—" when none is (Go writes testdata/api/cost-parity.golden.json; the web's parity test feeds it to both renderers). /api/sessions and /api/sessions/{id} rows carry derived: true for a session every turn of which is a derived run, drawn "derived (stock OTel)". A replayed run's header says its provenance in words beside "replay of …" — "5 edits (user, args, result, insert)" from weft.edits (", …" when the record's cap left kinds out), "fed the compacted view #7" from weft.replay.view — the raw chips behind "details"; the panel's replay chip names the edits in its title.
  • weft CLI (B7; UX-S5a-01, UX-S5b-02, UX-S5a-02, UX-S5a-08, UX-S5a-05, UX-S5a-06): one resolver for every API client — weft runs, open, export, doctor and mcp find the Studio through --url, else WEFT_STUDIO_URL, else the discovery file (the trust rules otel.Install uses; WEFT_DISCOVERY=off skips it), else 127.0.0.1:7331, and say so on stderr when you named none (weft: Studio http://127.0.0.1:7347 (discovery file …)); the token is --token, else WEFT_STUDIO_TOKEN, else the file's when it names that Studio. weft open takes a run id, a public id, a session id or a trace id (tried in that order) and, with no id, prints the Studio root — weft open --with-token is a terminal's signed-in link. weft runs gains DUR, COST, PUBLIC ID (when a row has one) and ERROR (kind and why, 60 runes) columns, STARTED with its zone, and names the next step on stderr. weft doctor gains discovery (this Studio, another — two on one database warned —, none, off), data (the newest run's age, weft version and service) and otlp (open, walled, off) lines; a 401 names --token "$(cat <db>.token)" and weft open --with-token; a capability off names its cause (--no-playground: "drop the flag"); the stale panel and manifest lines name the reinstall and the latest run's manifest hash. weft studio's banner ends studio: sign in with weft open --with-token when it did not open the browser.
  • studio (B7; UX-S5a-10): studio.DiscoveryFile(path) — the hosting command names the discovery file it writes; /api/meta serves it as discovery_file (db.path's rule) while the file names this process. studio.OffReason(capability, why) — the host's words for why a capability is off (an unknown capability panics at New), served in capabilities_off. content.latest gains started, weft_version and service. The token wall names <db>.token and weft open --with-token.
  • weft mcp (U33.1; P10a-pre-go 9): a stats tool over GET /api/stats (by agent, model, tool, error_type or step; the route's filters) answering compact groups — no 24-bucket series (full: true keeps it), empty figures omitted — with a summary naming the busiest groups; error_class on the runs and search tools, and search's summary names the facets.error_class classes. 22 tools.
  • scope, studio (U22; UX-D-05, UX-D-08, UX-E-27, A5): a Scope names a page of unrelated runs — Agent and Tags (run metadata pairs, written tag.<k>=<v>, repeatable, ANDed) — and may stand without a public id (;tag.arena.batch=b1, or a first segment that is a known key=value); Scope.Equal; lib/scope.ts mirrors it, one golden pins both. FlowKey (weft.flow) and WithFlow(ctx, flow): a scope's flow is run metadata Header stamps on every run its handler starts (record contract's weft.flow row, the flow_scope fixture). flow= and tag.<k>=<v> filter /api/runs, /api/sessions, /api/search, /api/stats, /api/traces (obsdb.TraceQuery.Meta: a trace holding a span with each pair) and /api/live, where a filter narrows a selector or stands alone (record and attempt frames of the runs it admitted), and agent= narrows run, session, public_id or all; the live grant binds each filter pair. Capability scope_tags.
  • studio (I4; issue 004): tag-scoped panel tokens — POST /api/panel-tokens takes "tags": {"arena.batch": "…"} with or without public_id (one required; server token only; read-only; no weft.* key but weft.flow, at most 16 pairs, keys and values at most 256 bytes — else 400). Every read the token makes is held to its tags as a public id token's is to its public id: runs, search, stats (a delegated bill sums only descendants carrying the tags, and leaves out one under an out-of-scope run — the run tree's rule, both backends), sessions, traces (every span carries the tags: obsdb.TraceQuery.ScopeMeta), a run and its steps, spans, logs, export and tree (obsdb.TreeQuery.Meta, TreeRun.Meta) — a run without the tags is 403 — and the live stream and its grant; the command routes refuse a tag-only token. A request narrows, never widens: another value of a token's tag key is 403. A Subagent child inherits the tags; an untagged pool or thread child is a counted hidden hole, "without your token's tags". Capability panel_token_tags. A tag scope is only as tight as the app's own tagging: metadata is caller-written.
  • devtools panel (U22, I4; UX-D-05, UX-D-07, UX-D-08, UX-D-09): a tag, agent or flow scope lists that page of runs — "batch cli-… · 196 runs" (the first tag's key by its last dotted segment; a chip for every other tag), rows saying "run" and naming their agent, the live lane on the filter; flow narrows every read. A panel token's public_id or tags claim is the scope when no rung names one, so data-token alone lists exactly that batch's runs. "how to scope" leads with the zero-code ?weft_scope=pub_… line, each line with a copy button; the npm README carries the ladder.
  • Studio, studio, obsdb (U30.2; UX-A-13, UX-A-15, UX-A-16, UX-A-17, UX-A-18, UX-A-19, UX-A-21, UX-A-33, UX-C-39): ⌘K finds a session and a trace by id prefix as it finds a run — /api/sessions?id_prefix= and /api/traces?id_prefix= (obsdb.SessionQuery.IDPrefix, obsdb.TraceQuery.IDPrefix, both backends: bytes, no wildcard, no case folding). The run page's export offers "copy as curl" per format, naming $WEFT_STUDIO_TOKEN, never a token. The runs list picks runs to compare ("compare N"); the compare page gains "changed only" (on by default when a step differs; a hidden linked step says "show all steps") and a usage line per run against the base. One "this run | whole trace" switch carries the selection both ways (/traces/{id}?run=). /live folds a parent's ended child runs behind "+N subagents" and draws a mini waterfall per row. Run rows carry own_usage / delegated_usage (usage less its Subagent children's, and theirs; omitted with no Subagent child, running, a failed children read, or a child a panel token cannot see), drawn "3 in · 1 out +9 in · 5 out" and as per-child token chips on the run header. At 480 px and under the waterfall keeps thin bars and the run header's verbs fold into "⋯ actions". A runtime's breakpoints say "set by ": PUT /api/runtimes/{id}/breakpoints takes an optional surface, served as breakpoints_surface and, with breakpoints_set_at, kept across a Studio restart — obsdb.BreakpointProvenanceStore (PutBreakpointSet / BreakpointSet, both backends) beside the unchanged CommandStore, RuntimeServer.SetBreakpointsFrom beside SetBreakpoints.
  • Studio (U30.3; UX-S5b-03's, UX-H-18's, UX-F-16's, UX-D-12's remainders): the Request pane tags each overridden param "◂ override" against the agent's from the stored manifest's params ("temperature 0.4 ◂ override (agent: 0.7)"), else "(source run: …)", else "override" alone, there and in an experiment's variants. A breakpoint park's card offers "clear the breakpoint on status" — to the server token only, with capability breakpoints, on the one connected runtime of the run's service holding it (two or more: a pointer to Runtimes), its set read again before the PUT; anyone else reads why not. One duration format on both surfaces ("11 ms", "1.2 s", "15 µs") and a "start" column on the run tree. The panel's Story step card folds to its head line (the read step and the streaming one open; "step N" is the toggle).
  • Studio (U33.1, U33.2; UX-H-22, UX-H-31): /api/runtimes agents[].tools[] gains approval: true for a RequireApproval tool, and the replay drawers say "needs approval" beside its ⚠ on both surfaces. side_effects' never is documented as the replay class, not "no side effects".
  • Studio and the devtools panel (U21.1, U21.2, U21.3; UX-F-01–UX-F-17, UX-F-28, UX-A-20, UX-C-22, UX-B-24, UX-H-20, UX-D-22): one vocabulary. lib/glossary.ts TERMS — each concept a tooltip, a short entry, a docs anchor and the words it retires — drawn by <Term> (a focusable glossary word, aria-describedby while shown) on the tables' headers, the run header and the replay drawer; the ? dialogs gain a concepts tab (the panel's the eight core one-liners and "all words in Studio ⤢"), /settings#concepts lists every term. lib/keys.ts: one keymap both ? overlays draw — j/k the list, [/] the step, s/w/q/r story · waterfall · request · raw (again: the view before it), o open it elsewhere. lib/palette.ts: each status · kind · diff · hole role one hex on both renderers per theme. lib/run-row.ts: "↻ replay of t3 · step 1", "⑃ fork of s_…#12" on both. Every list page's empty state says how to connect an app to this Studio by its own URL (lib/connect.ts), /settings leads with "Connect an app", /runs carries a dismissible first-run card. Linkable: /sessions/{id}?turn=N, /live?q=&agent=, /experiments/{id}?pick=, /agents?agent=. Studio's "open in app" carries the step and tab (#weft_scope=…&weft_step=<n>[.<tab>]) and the panel opens there. The Studio-only views get the panel's ⤢ (an agent's card, the live page, the runs list with the scope's filters, a turn's replays, the runtimes a refusal names). make studio-check runs scripts/vocab-audit.ts, which fails on a retired word on screen in either renderer, and palette.test.ts, which refuses a new status or kind colour literal.
  • devtools panel (U28, U30.1; UX-D-11, UX-D-17, UX-F-03, UX-F-09, UX-D-23–UX-D-40): when Studio stops answering after boot the line under the header says so in the boot probe's words ("Studio not reachable at … · retry", or the token it wants), the turn list dimmed; the live lane's backoff and, past it, one /api/meta read a minute while the dock is open and the page visible bring it back (a 401/403 stops it; retry starts over). The docked turn column is clamp(240px, 28%, 420px) of the dock and its rows never overflow. The footer names the rung that chose the scope (scope: explicit, url, marker, header from <path>, switcher, none); an explicit public id with no run, once the list read lands, reads "Studio has no runs for pub_… yet — check the id". A scope naming a session and no public id titles the header "session ". The pill reads the current turn's priced whole bill (· $0.0029), else its tokens. The axe budget covers the fallback's "how to scope", refused verbs, the switcher and the steer box in both themes.
  • weft dev (U30.1): without a browser it prints where the token is (studio: token: <db>.token (weft open --with-token); a reused Studio's "the running Studio's, not .token" when the file holds another), and "no runtime registered yet" adds "or the app links its own embedded Studio".

Changed

  • Devtools panel: panel.js grows from 95,558 B to 98,921 B gzip (headroom 7,575 B under the 106,496 B cap).
  • Studio: the dist grows from 723 KiB to 753 KiB gzip, of the 800 KiB budget.
  • Studio, devtools panel (U21.1, U21.2, U21.3): the words. "session" everywhere a sentence named a thread ("weft/thread" where the Go package is meant); "turn" only for a session's runs ("run" for a public id's sessionless runs, a page of runs and the fallback); "child run" for a subagent's run, "pool sessions" for thread/pool's children, "gaps" for holes, "tokens", "first token" (TTFT in hovers only); the event bar is the playback and "replay" is re-execution only; "Write to: nowhere (ephemeral) | a new session (fork)" in the replay drawers; "✎ Experiment" is "✎ Replay…"; "your token's access" (not scope) on refusals. tokenPair ("60 in · 30 out") and costLine ("$0.0012 own · $0.0040 delegated") on both surfaces; /live's "● live" in the live role (green).
  • devtools panel (U21.3, U22, U30.1; UX-S5a-03, UX-D-06): a Studio that answers 401/403 is never removed — the line says what token it wants ("add data-token (the dev token) or a panel token (POST /api/panel-tokens) · retry"). A script tag's dock still removes itself silently when Studio does not answer, unless the host named a loopback endpoint (a dev Studio not running yet is said, with retry); a bare tag (no endpoint, no data-weft) whose guessed endpoint is on loopback says once in the console "weft devtools: Studio did not answer at ; add data-weft or data-endpoint on the panel's <script> tag". A module-loaded panel reads its own <script> when it carries data-weft, else the page's first data-weft tag. The fallback (no scope) selects no run, sums no cost and groups its rows by agent. The dev list's empty line names this Studio's URL. A narrow dock hides the header's stats and cost from sight only (the title's tooltip carries them). A refused app-turn approval states its refusal once beside disabled verbs; a parked plain Generate's fix reads "resume it in your app; to decide here, run it as a session turn with thread.PublicID(...) (weft/thread)".
  • devtools panel (U33.2; UX-H-07): the header's set figure follows cost_partial's rule — the k turns whose whole bill is known, a turn with an unknown delegated part adding nothing (it added its own figure), "≥ $0 · k of n turns priced" when they are $0 (it read "—"); "—" only when none is priced.
  • studio (U22): GET /api/live with a tag.<k>=<v> or flow= filter alone opens a stream (it was a 400 without a selector); the live grant's stream binds the filter.
  • studio (U22; UX-D-18): /api/panel-handoff hands off for the server token too — the location carries it in the fragment, as weft open --with-token does — so the panel's ⤢ / g s land signed in.
  • weft mcp (U33.1; R-I5b, UX-H-29, UX-H-36): a session answer's turn_defaults is, per key, the value most turns share (ties to the earlier turn), kept only when two turns share it. An unnamed agent reads "(unnamed agent)", counts agree with their nouns, a 404 says where ids come from, and an events page's summary names its positions, the next cursor and whether the run is terminal. Tool texts say "read-only panel token".
  • weft CLI (U33.1; UX-S5a-11, UX-S5a-12): usage errors name their arguments in words ("export needs exactly one run id (got none)"), never a Go slice; every -h spells its flags --name. weft vacuum prints the main file and the WAL apart (vacuum: weft.db 176128 → 98304 bytes (WAL 4120032 → 0 bytes)): the SQLite Vacuum checkpoints the WAL after VACUUM (PASSIVE then TRUNCATE, under a 250 ms busy timeout of their own), so the file's size after a vacuum is its size; a WAL a live reader or writer holds is said.
  • docs (J2, B7, U33.1; UX-H-05, UX-H-11, UX-S5a-04, UX-S5b-01, UX-S5b-06, UX-E-55): setup B's Python line exports over OTLP/HTTP (OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf, GenAI content on, opentelemetry-instrument) and says what makes a run; @weftgo/devtools is not published to npm yet — its README opens with the script tag, the panel-vX.Y.Z.js release asset and the locally built tarball; no TS or Python runtime-link client exists (Go only; studio/README's wire section is "for future non-Go clients"); studio/README.md's devtools-panel section opens with a quickstart per setup; the npm README lists every meta field config.ts reads, scripts/npm-package.ts --check failing on drift; docs/record-contract.md's span-id, caller-link and fixture claims match the code (TestRecordContractMigrationNames).

Breaking

  • Breaking: core, mw, mcp (U23.1, U23.2, U30.3): weft.manifest.hash changes for an agent with a tool defined outside weft (each tool's source is now the import-path form, github.com/x/app/tools.go:12, was module-relative tools.go:12), with model or tool middleware or hooks (the manifest lists them; a non-weft one, a hook too, with its source file:line), with an Output (submit_output's output_type) or with Params (the manifest's params). Regenerate weft.json (go test ./... -update). A run recorded before 0.20.0 keeps the hash it ran with, so it reads "stale" against the regenerated manifest (the stale title says the weft version changed); an agent's latest-run check (weft doctor's manifest) reads current again once the agent runs on 0.20.0. Middleware and hook names in the manifest no longer depend on inlining or the Go version (P10b-inline-names): weft's own read mw.Allow / mw.Audit / mw.MapErrors / pool.Wrap (registered through the new core.NameToolMiddleware), a named function by its name, a closure as func@<dir>/<file>:<line> (was its runtime name, pkg.Outer.func2, which go1.26.0's inlining renamed — caller.Allow.func9 for mw.Allow); go1.26.0 and later, inlined or -gcflags=all=-l, hash one agent alike.
  • Breaking: mcp (U23.2, O8): a tool mcp.Tools imports under mcp.Server(name) records source mcp:<name> in the tools record (and weft.tool.origin on its spans) — a reader matching the literal mcp must match the mcp prefix; without mcp.Server the source is still mcp.
  • Breaking: mcp (U23.2): a JSON-RPC error answer from a live MCP server (its handler failed, the tool is not listed) is error.type mcp_rpc_error, no longer mcp_transport, which keeps a call that got no answer.
  • Breaking: core, mw, Studio (U23.1, U30.3; UX-S2-02): a failed attempt span's error.type is classified — rate_limit (429), overloaded (503, 529), timeout (408, 504, a transport timeout), auth (401, 403), server_error (other 5xx), context_overflow, weft's own sentinels as the run reads them (stream_idle, …), model_error the rest; it read model_error for every provider failure (the run's error.type keeps its table). weft.attempt.layer gains retry (mw.Retry's retried requests), so a reader that took any non-empty layer for a fallback must compare with fallback.
  • Breaking: the devtools panel's Timeline tab is the waterfall tab (U21.1; UX-F-05): its id is waterfall (#weft-tab-waterfall, the stored layout's tab, the weft_step=<n>.<tab> link), and a stored tab: "timeline" opens the story tab.
  • Breaking: studio (U21.1; UX-F-15): delegated_cost_reason says "child runs": 1 of 3 child runs unpriced: …, 1 of 3 child runs' cost not known yet (…), N of M child runs carry another public id, or none: ….
  • Breaking: studio, obsdb (U21.3, U28): the hidden hole's reason reads "your token's access does not reach this: a read-only panel token reads no system prompt or tool catalog" and its fix "use a playground token"; the no_public_id reason and the app-owned 403 say "session" ("is not a turn of a session with a public id"), a parked plain Generate's fix "resume it in your app; …".
  • Breaking: studio (U33.2; UX-H-07, UX-H-35): a session's or experiment's unknown cost reason reads "N of M turns' cost not known (…)", without "yet", when every unknown part is a derived run whose spans report no usage; GET /api/sessions/{id} numbers a derived session's top-level turns 1..n by start order in runs[].turn (was 0 for each), after the thread's highest where a session holds numbered turns too.
  • Breaking: scope, obsdb (U22, I4): scope.Scope (now holding Tags), obsdb.TraceQuery, obsdb.TreeQuery and obsdb.TreeRun (now holding Meta maps) are no longer comparable with == — use Scope.Equal. scope.Parse and lib/scope.ts read a first segment that is one of the form's keys (session=…, tag.<k>=<v>) as that key, no longer as a public id.
  • Breaking: obsdb (I4, issue 004 §4): SessionQuery.Meta scopes turns before the grouping, as SessionQuery.PublicID does: a session whose turns carry the tags only partly lists, counts, sums and derives its status, title and turn facts from the tagged turns alone — it no longer selects whole sessions; /api/sessions?tag.<k>=<v> follows.
  • Breaking: studio (I6): a read-scoped panel token is served stock spans without their system prompt — gen_ai.system_instructions, system-role messages, plain gen_ai.prompt / gen_ai.content.prompt and the gen_ai.system.message / gen_ai.content.prompt events.
  • Breaking: studio (U30.1; UX-D-27): POST /api/panel-tokens refuses a body field it does not know — {"scope":"playground"} is a 400 naming the field and "playground": true, never a read token minted in silence.
  • Breaking: weft CLI (B7; UX-S5a-13): one Studio per SQLite file — weft studio and weft dev take <db>.lock beside the database (an exclusive flock, LockFileEx on Windows, held while serving, taken before the port policy, released by the kernel when the process ends, a crash included); a second weft studio on the file — at the same moment, from another directory, on any port — is refused, studio already running at http://127.0.0.1:7347 (pid n) on …/weft.db: …, exit 1 (it was "…, reusing", exit 0, and only on the wanted port). weft dev reuses the holder instead, named only from this user's 0600 lock file with a loopback-IP url and a live pid. The lock stands for the file a symlinked path names (hard links and case-insensitive spellings are not detected). A ClickHouse database takes no lock.
  • Breaking: weft CLI (B7): the API clients' --url default is the resolver's (the discovery file before 7331), no longer $WEFT_STUDIO_URL else 7331; weft runs' table columns changed (scripts read --json, unchanged).
  • Breaking: weft doctor (U33.1; UX-S5b-09): the manifest check's label is manifest (was weft.json), its source the first words — weft.json: 1 agent, 1 checked against their latest runs: current, runtime registrations (no weft.json): 9 agents differ from their latest runs (runs from another build).
  • Breaking: weft mcp (U33.1; UX-H-36): the events tool's body renames the route's done to run_terminal (the run ended — not the last page; follow next_after).
  • Breaking: Studio (U33.1; UX-H-19): with a runtime.Sessions runtime connected, a cancel on the app's own turn is refused at once, 409 cancel is not supported on an app turn: run <id> is the app's own turn, and a steer on an app turn that runs nothing is 409 — parked: no running turn to steer: run <id> is parked: decide its pending calls (approve/deny) instead, finished: no running turn to steer: run <id> already finished (<status>) — no longer 202 and then rejected or failed on a command's row. Without such a runtime the three verbs still answer 403 "app-owned turn".

Upgrade notes

  • The SQLite file moves to schema 0026 and a ClickHouse database to 0033 on first open; a 0.19.x binary refuses the migrated database with ErrNewerSchema. SQLite 0026 (ClickHouse 0031) re-derives the stored stock GenAI traces the new derived-run rule changes — those holding a non-weft invoke_agent span without a string gen_ai.conversation.id, or a non-weft chat span carrying message content, or a conversation-rooted invoke_agent span in a trace no writer derived (every stock trace when the database predates derived runs: SQLite 0008, ClickHouse 0009 — an upgrade from there that stopped part-way converges on reopen) — so a setup B database may list runs it did not before (a top-level agent span without a conversation id), and its derived rows gain their message count.
  • That re-derive is a one-time cost on the first open, and Open waits for it: an INFO line names the trace count before (weft/obsdb/sqlite: migrating: re-deriving N stock traces (SQLite 0026); Open waits for it, … (ClickHouse 0031) …) and the time after — about 1 s per 10,000 traces on either backend (ClickHouse inserts synchronously, a thousand traces per insert). It is idempotent: an interrupted run records nothing (SQLite rolls the migration's transaction back) and the next open starts it again from the beginning. On SQLite it runs inside the migration's write transaction, so an app's local sink writing to the same file waits on its busy timeout (30 s) meanwhile — open the file with weft studio once before pointing a running app at it if the store holds many stock traces.
  • weft studio and weft dev create <db>.lock beside a SQLite file (0600, holding the holder's url and pid while it serves; left in place on exit — leave it, removing it races the next start). A symlink, directory or other non-regular file at that path — on unix a hard link too (more than one link) — is refused at start (lock …/weft.db.lock: dblock: the lock file is not a regular file) and never followed, truncated or chmodded; remove it to start.
  • Breakpoints set with a surface are stored as a JSON object in the same column: a 0.19.x Studio opened on that store reads no set for that runtime (the runtime's registration re-delivers its own).

Fixed

  • mw (U23.1): a backup under its own mw.Retry beneath mw.Fallback marks every request it sends fallback (its retries included), so fallbacks counts the run it answered.
  • studio, obsdb (I4): /api/stats?by=agent's delegated bill under a public id scope leaves out a descendant under an out-of-scope run between it and the group's run — the run tree's rule — so it matches the run document; ClickHouse reads an out-of-scope intermediate as SQLite does.
  • Studio, devtools panel (U21.1, U21.3, U33.2): a view key pressed again goes back to the view before it (Studio's went to the waterfall); an experiment result's deny sends the typed reason (it was dropped); /compare drops a repeated b and a b equal to a from the address.