Repository navigation
v0.17.0
Added
-
otel, obsdb, Studio: the slog bridge (U11.1; UX-E-07).
otel.Slog(), an Install option, wrapsslog.Default(restored at shutdown, with the log package's writer and flags; a program on slog's built-in handler keeps its lines on the log package's writer through aslog.TextHandlerat the levelslog.SetLogLoggerLevelset whenInstallran — read once — each line afterlog.Prefix(), in its key=value format: the built-in handler cannot be wrapped; one Slog pipeline at a time, and an out-of-order shutdown of two restores what the first found, never a closed bridge's handler);otel.SlogHandler(next)bridges any handler and follows the installed pipeline;Pipeline.SlogHandler(next)binds one pipeline. Every line logged on a span-carrying context —weft.Logger's Debug run lines,mw.Log,mw.Audit, the app's own — becomes an app log record (scopegithub.com/weftgo/weft/otel/slog, the context's trace and span ids, severity level + 9, the slog level's name, attributes flattened with groups joined by., every app key startingweft.renamedslog.weft.…, past 120 attributes the rest counted inweft.log.attrs_dropped); a line with no span is not bridged, nor the pipeline's own WARNs. New per-destinationotel.AppLogs(on): on by default for Local and Studio with content on, off for Datadog, Langfuse, OTLP and Exporters unless opted in. It admits, never bypasses the destination's content policy: an app log line is content — a content-on destination appliesRedact(new kindotel.ContentLog) andMaxBytes(default 32 KiB,weft.content.truncated_bytes) to the message and each string attribute and drops the line whenRedactpanics; a content-off one that opted in gets it stripped (message and string values emptied,weft.content=stripped, time, severity and span kept). An agent'score.Content(false)reaches its own lines first:core.Logger'stool callline names an error result's size (err_bytes) instead of its text, andmw.Audit'serr_bytesand the*core.ToolError'scodeinstead oferrandcause(a child run withoutContent(false)logs them again). Bridged lines are shed with the deltas under a full queue. While a bridge is live — aPipeline.SlogHandler,Slog's wrap while it is stillslog.Default's handler (a line logged through it does not keep the pipeline marked once the program replaced the default), or any other handler that has teed a line into the pipeline (building a package-levelSlogHandlermarks nothing) — heartbeats and each run'sinvoke_agentspan carryweft.logs.bridged(the lines bridged so far), on the destinations that take the lines only (a content-off sink, anAppLogs(false)one or a third party not opted in stores the root unmarked, so its empty page carries the hint); it is not metadata —obsdb.MetaOfskips it, ClickHouse migration 0021 restates both run views.obsdb.LogPage.NoBridge: a run whose root is stored without the mark and has no attributed line, no gap and no cut; Studio'sGET /api/runs/{id}/logsthen holds anot_recordedhint — "no app logs: no weft slog bridge was recorded for this run … if the app bridges slog itself, it logged nothing under this run", the fix namingotel.Slog()— instead of an empty page that reads "logged nothing".examples/studio-localinstalls it and wraps its model withmw.Log(nil): its lines arrive under each step's chat span (TestModelLogLinesArriveUnderTheStep). Settings' app-log line namesotel.Slog(). Record contract §3, §4 (app log records), §5, §7; contract fixturesslog_bridged,bridge_overrides(a redacted line,weft.log.attrs_dropped) andbridge_stripped(a content-off destination's stripped line). -
Studio, panel: a run's app logs on both surfaces (U11.2; UX-E-06, UX-A-07, UX-S2-03). The run page gains a logs view (
?view=logs, keyl;RunViewgains"logs"): every lineGET /api/runs/{id}/logsattributes to the run, a severity filter (?lsev=info|warn|error,RunSearch.lsev, the route'sseverity=), each line's offset from the run's start, severity, body and attributes, and jump to span —?sel=t:<span id>&tab=logs, the run-tree row that span is behind, or (resolveRowSel/expandFor, an optional span-parents map) its nearest drawn ancestor's. A tree row a WARN / ERROR line landed on carries a tick ("E1 W2", read atseverity=warnonly when the run document'slog_warn/log_errorcount such lines; "≥E1 W2" and "at least …" over a cut read); its click opens the row's drawer Logs tab. The drawer's Logs tab and the view read every page (src/lib/logs.ts'sfetchAllLogs/allLogsQuery, 1,000 a page up to 10,000 lines;lib/api.ts's single-pagelogsQueryis gone) and badge a longer listtruncated("the first 10,000 lines are shown … nothing was lost"); while the run runs they are read on from the line after the last held, every 5 s, show the page'spartial_reason, and stop once the read is cut; a finished run's read is whole, once; a row with no line yet says "this row's spans are not reported yet", never "no app log line". The panel's Story step heads carry "📄 ⤢" under capabilitylogsfor no token or the dev token (the logs route refuses every panel token): Studio's run page on that step's row with its Logs tab open (lib/links.ts'slogsLink;runLinkgainstab); the panel reads no line. -
Studio,
weft studio,weft dev,weft doctor: a server log of refused ingests and 5xx (U11.2; UX-E-37, audit row D10). Every response carriesX-Request-Id(the client's well-formed one echoed, else generated; inAccess-Control-Expose-Headers); a 5xx error body carrieserror.request_id.slog.Defaultgets one WARN line per refused ingest batch (studio: ingest batch refused: request id, status, path, remote, code, reason, bytes), at most one a minute per (remote, route class —/v1/traces,/v1/logs, else/v1/*— code, status), the next one counting the rest assuppressed; a full limiter (1,024 keys) drops expired windows first (their counts on the next line,suppressed_expired) and otherwise logs no new key until a window turns (suppressed_overflow) — behind a reverse proxy every client shares the proxy's remote, documented. One ERROR line per 5xx (studio: request failed, same fields; a panel token's masked body still logs the real error asreason) and per handler panic (studio: handler panicked, then the panic goes on;http.ErrAbortHandleris not logged). The request wrapper is anhttp.Flusheronly when the connection's writer is.weft studio/weft dev--log-level debug|info|warn|error(WEFT_LOG_LEVEL) and--log-format text|json(WEFT_LOG_FORMAT) set the server log's handler on stderr (a level alone installs a text handler; neither leaves Go's default)./api/meta'singestgainsaccepted/refused(OTLP POSTs answered 2xx / 4xx–5xx since start, in memory) andlast_error/last_at(the last refusal, omitted before one; a refusedContent-Encodingis echoed at most 64 bytes, in the body and the log line alike);weft doctor's ingest line prints them — "N OTLP batches accepted, M refused" — and the last refusal with its time and a fix, a warning while that refusal is within the hour. -
obsdb, Studio: the run tree (U9.1; UX-A-01, UX-E-22, UX-E-34, UX-E-24).
GET /api/runs/{id}/spans?tree=1(capabilityspans_tree) answers the run's spans and every descendant run's — the runs whose parent chain reaches it (a run row'sparent_run_id, which a span'sweft.parent.run.idfills, so a running child's finished spans count; a stored parent always wins) and, for a run no row or span names a parent of, every run core's exact child id shape names a descendant (<id>/<digits>/<call>or<id>/resume/<call>,obsdb.TreeChildID: a child whoserun_startwas lost is still placed, under its nearest ancestor that is a member of the tree — an app's ownjob/1is no member, sojob/1/0/cunderjobhangs underjob, alike on both backends;job/1andjob/1/retry/2are not each other's tree) — found from the run rows first and read by run id in any trace (a child stored under another trace is in it), never by reading the run's whole trace, in start order, each keyed byrun_id+span_idwith its run'sparent_run_id, at most 10,000 spans (obsdb.TreeSpanCap) of at most 10,000 runs (obsdb.TreeRowCap); a sibling run of the same app trace and a run-less span are not in it. A derived run's tree is cut from its trace before the span cap, the trace read itself capped at 50,000 spans (obsdb.TreeTraceCap,RunTree.TraceTruncated). Itsholes(always a list):truncated, causeresponse_cap, past any cap;not_recorded, causeno_spans, naming (at most 100 (obsdb.TreeMissingCap), with acount) the finished descendant runs with a row and no span — a running child with no span yet is not missing (TreeRun.Status);hidden, with acount(RunTree.HiddenCount, subtrees included), when a panel token's scope left runs out — a panel token reads the run's own spans and its public id's descendants, every other descendant (a nestedGenerateor thread turn inside a tool handler, a child whose metadata clearedweft.public_id) left out with its whole subtree and unnamed before the cap (obsdb.TreeQuery.PublicID; hidden runs are not read, never take a visible span's place), never a 403 for the tree. Every/spansanswer carriesrun_id(the run's); the TS mirror isSpan.run_id?/parent_run_id?. New optionalobsdb.TreeReader(RunTreeSpans(ctx, obsdb.TreeQuery)→obsdb.RunTree{Spans, Truncated, TraceTruncated, Missing, MissingCount, Hidden, HiddenCount};ChildRuns, at mostobsdb.ChildRunsCap(10,000) rows a call, past itobsdb.ErrChildRunsCapped),obsdb.TreeQuery,obsdb.TreeSpan,obsdb.TreeRun,obsdb.TreeIDRule,obsdb.TreeChildID,obsdb.TreeMembers,obsdb.TreeMemberIDs,obsdb.TreeReadIDs,obsdb.CutRunTree,obsdb.CapTree, implemented by both backends: SQLite finds the rows in one recursive statement (runs_started, runs' primary key for the id rule, its rows that name no parent kept only in core's child shape) and reads the spans onspans_run; migration 0020 addsspans_trace_start(a trace in start order, no sort:Traceand a derived run's tree); ClickHouse finds the rows on weft_runs' primary key and migration 0020'sidx_parent_runbloom filter onweft_runs.ParentRunID(materialized in the background), and reads the spans onidx_weft_run_id. A database without a TreeReader leavesspans_treeoff (capabilities_offsays why) and answers?tree=1with 501. Record contract §6 "A run tree"; thesubagentfixture pins the parent'stree,studio/testdata/api/spans-tree.golden.jsonthe answer and its TS mirror. -
Studio: the child subtree in place, the graph scale and the trace link (U9.2; UX-A-01, UX-D-13, UX-A-17, UX-S3-14). On
/runs/$ida child run row is one collapsed row with "open run ↗", and its chevron (→/←) expands its steps, model calls and tools in place from the same?tree=1spans, grandchildren again one collapsed row each, drawn on expand (an expanded child's own?tree=1is read only when the tree'struncatedhole says it was cut). An expanded child's document and stream are read again only while it or the run runs, its stream followed from the position past the last one held bynext_after(childStreamQuery, up to 20,000 events, then atruncatedbadge on its row; from the start again only when that read held gaps) and its gaps agapbadge. A running child row carries no loss badge (spans export when they end); a finished one'snot_recordedbadge reads the server's reason. A?sel=naming a row inside a child — itsrowKeysrun-qualified key, or<child run>:<span id>— expands the children above it and selects it (a row no child draws selects the child's row); folding a child that holds the selection selects the child's row (lib/run-tree.ts:expandFor,ALL_RUNS,qualifyKey; theimpliedexpansion rebuilt when the selection, the scale or the known runs change, never per event batch). The tree's holes (SpansDoc.holes, the TS mirror oftreeSpansDoc:TreeHolewithcause,runsandcount) are badges above it,not_recorded/no_spansalso on the child rows it names. "whole trace ↗" links/traces/<trace id>with the selection kept, the run's own keys run-qualified.?scale=graph(RunScalegains"graph";src/lib/graph.tscallGraph,components/studio/call-graph.tsx, inline SVG) draws agents and MCP servers as nodes in call-depth columns, edges with their call count and slowest call (not a p50), a self-call as a loop over its node, a resumed call's child under its parent (<parent>/resume/<call>), "MCP calls of N child runs not shown" when the page did not read their catalogs, each node with a focus ring and its accessible name starting with its visible words, the edge hint on the edge group's<title>, the selection shared with the tree; pool children (separate sessions) are not drawn, and nodes carry tokens when the spans do, no cost. Studio only: the panel's Timeline draws one level (U8.3). -
obsdb, Studio: the live lane's
attemptframe (H9, audit row O17; UX-E-30).GET /api/live?kinds=…,attemptstreamsevent: attempt{run_id, session_id, public_id, step, attempt, layer?, status ok|error, error_type?, provider?, model?, start, end, wait_ms?, retry_after_ms?, trace_id, span_id}— one try inside a model call, read from itsattemptspan the moment the span lands (OTLP ingest, and a self-publishing database's write; deduped on run, step and attempt), so a step that is retrying or falling back is seen before itschatspan ends;wait_msis the wait before the try (weft.attempt.wait_ms, the step route'sattempts[].wait_ms),retry_after_msthe provider's retry-after ask, each absent when not recorded. Live only: never backfilled, nothing new is stored; each attempt frame is held to a panel token's public id. Newobsdb.FrameAttempt,obsdb.Frame.Span,obsdb.AttemptFrame;lib/live.ts'sopenLivegains aframesoption (readers of other SSE event names) andsrc/lib/live-attempts.tsreads the attempt frames. The Live page asks for them: a failed try reads "↻ attempt 1 failed" under its step while the run runs (the error type and the provider's ask in its title); an earlier step's note is dropped. -
Studio: the runs list's delegation shape (H9; UX-A-19). Every priced run row carries
children_count/children_failed(the runs whose parent it is, one level deep, and the failed ones — counted from the children read the cost already makes, one grouped read per level of a page, never a read per row; omitted when 0) andchildren_unknownwhen that read failed or passed its bound (10,000 child rows a request; the cost under it reads not known too), drawn "⑂ ?";/api/sessions/{id}'s turn rows count every parent-linked run. The row draws "⑂ n" (the failed count in red), a disclosure that reads?parent=<id>and draws the child runs under it, indented, each with its own chip (grandchildren read when opened; past the 100 read, "+N more · open the list"); "with child runs" draws the loaded rows as trees, each child under its parent (src/lib/delegation.ts'snest)./compareopens with a run picker (src/lib/compare-picker.ts: the newest runs as a base, then the base's replay source, the base's conversation (same public id) and its agent's runs, with a find box — id prefix or agent, past the newest 50) andb=r1,r2reads like the array form;?b=keeps a JSON array's elements whole. -
Studio, panel: step and tool cards tell the whole truth (U25.2, U25.3; UX-S1-03, UX-S1-06, UX-S2-02, UX-S2-06, UX-S2-09, UX-S3-12, UX-S3-13, UX-S3-15, UX-H-18, UX-S5b-03, UX-S1-13, UX-E-14). One shared module,
src/lib/cards.ts, words what both surfaces draw (the run page's story and the run tree's drawer read the sameRunCards): a max_tokens step's call the loop never ran (rule 11) is placed on its step from the transcript (lib/events.tsapplyTranscript,FoldedToolCall.unrun) with its args as sent ({"msg":"never) and the "call never executed" badge, drawn "not executed" in the run tree and the panel's Timeline (no span by design, never anot_recordedhole) and counted "1 not executed" in the answer's tools line; an OUTPUT card on the run header (the last validsubmit_outputcall's JSON, "typed output (submit_output)", "repaired after N invalid submissions"; "no output submitted" only when the root'sweft.output.submittedis false — a submission whose arguments the record lacks, content off or a read token, reads "typed output (submit_output)" with thestrippedbadge) and the panel's typed-output chip; the Request pane's catalog marks the output tool; attempt rows name their HTTP status, the provider's cause and the wait before them ("503 · Service Unavailable: overloaded · waited 1 ms" —GET /api/runs/{id}/steps/{n}attempts[]gainshttp_status,wait_ms,error), the panel's attempts picker too (attemptTag); a tool card's execution line ("12 ms / 50 ms limit · queued 3 ms", its origin when not local,callFactsOfover theexecute_toolspans); a parked call reads "awaiting a decision" (or "· ParkOn", "· ParkAllExcept", "· side effects", "· middleware"), aParkSourcepark "paused at breakpoint" on a playground run or when the run page knows its service's runtime registeredbreakpoints_own, else "paused by ParkSource" (ADR 0030) — never "never completed", its "finished at" fact the same word, and the answer's tools line counts it "N awaiting a decision"; "overridden by experiment" on the first step's Request pane for anyweft.override.*knob, its hover naming them (lib/cards.tsparamsOverridereads every storedweft.override.paramsshape); a "PrepareStep: system, tool_choice, params" chip from the request record'sprepared; the thinking row reads "low → reasoning_effort" (thinking.wire); the prompt's file parts as attachments ("file image/png · url", the request's message lines too); the header's own / delegated token split ("60 in = 20 own + 40 via 1 child"; the panel's turn line), counting the core's<id>/…Subagent children only (a pool child session or a nestedGenerateis not delegated); a Subagent call's typed outcome ("child failed", "child awaiting a decision · refund_order", "cycle refused", "child timed out · 2.0 s limit"), kept in the drawer's call rows with the tool limit. Proven on the named seeded run shapes recorded through the real pipeline (studio/cards_test.go→studio/testdata/api/cards.golden.json;routes/run-cards.test.tsx,panel/cards.test.ts,lib/cards.test.ts, parity). -
core, mw, adapters, runtime, obsdb: the run cards' facts on the record (U25.1; O6, O9, O16; UX-S2-02, UX-S2-06, UX-S2-09, UX-S3-15, UX-H-18, UX-S1-06, UX-E-14).
core.AttemptInfogainsWaitandHTTPStatus: the attempt span carriesweft.attempt.wait_ms(the backoff slept before the try, rounded up, never 0 —mw.Retryreports it; through aFallbackbeneath it on the primary's next request only, and underRetry(Fallback(Retry(x)))the inner Retry's first try keeps the outer backoff; a Fallback's backups carry none) andhttp.response.status_code(mw.Retryandmw.Fallbackread it off the error withmw.HTTPStatus). Theexecute_toolspan carriesweft.tool.timeout_ms(the deadline in force, rounded up: aTimeout(500µs)reads 1),weft.tool.queue_ms(a call that actually waited, behind aSequentialbarrier or for aParallelismslot),weft.tool.pending.sourcebesideweft.tool.pending(approval|park_on|park_source|park_all_except|middleware— a middleware or the handler returningErrApprovalRequired: an agentParkSource—runtime.Breakpoints()— is no longer indistinguishable fromParkOn) and, onSUBAGENT_PENDING,weft.subagent.pending(the parked child's calls by tool name). AnOutputagent'sinvoke_agentspan carriesweft.output.submitted(falseis the runGenerateAsanswersErrNoOutput), and the tools record markssubmit_outputoutput: true. The request record gainsprepared(O9: the parts thePrepareStepchain changed —system,messages,tools,tool_choice,params,thinking,sequential_tools; names only) andthinking.wire(O16: how the adapter sends the level — a model declares it with the optionalThinkingWire(core.ThinkingConfig) stringmethod, found along theUnwrapchain;openaireasoning_effort|thinking|noneby dialect,anthropicthinking.adaptive|thinking.budget_tokens|thinking.disabled,googlethinking_level|thinking_budget), both derived inside the request kind's gate: a run whose destinations want no request record compares no request and asks noThinkingWire(ADR 0016;TestRequestFactsOffComputeNothing).weft/runtimenames a playground run's breakpoints in itsweft.playground.breakpointsmetadata (record contract §2). The new span keys are contract, not metadata:obsdb.MetaOfdrops them and ClickHouse migration 0022 restates both run views. ADR 0013 and ADR 0028 amended (the optionalThinkingWiremethod;prepared[],thinking.wire, the tools record'soutput);core.ThinkingConfig's godoc names the convention. Record contract §2, §3, §4, §10 (new conformance casesrun_cards,queue_breakpoints—weft.tool.queue_msandweft.playground.breakpoints— andbridge_overrides—weft.override.params' null shape andweft.override.tool_choice;parked,approval_denied,resumedgainpending_source,compactedits request'sprepared,retry_fallbackits attempts'wait_ms). -
Studio: the API's run verbs on the run page (U26; UX-A-15, UX-S1-04). The header offers export json · jsonl · otlp beside save as test (capability
export, downloaded with the bearer;GET /api/runs/{id}/exportserves json, jsonl, otlp and wefttest only —xmlis a 400 and is not offered; a read-scoped token gets json and jsonl) and delete run… (capabilitydelete): an in-page confirm saying the run goes with every child run under it at any depth ("N known", "N+ known" past the children read) and with the derived runs read from its tools' spans, and that the rows leave Studio's database only, thenDELETE /api/runs/{id}and the runs list (deleting a child reads its parent's document again); where the server would refuse it (a panel token, a non-loopback Host without the server token — metalive_all— or the capability off) one faint line says why instead. The Agents card showsstop_whenandusage_limitchips and titles every policy chip with what it does and the option that set it (src/lib/policy.ts; the cap reads "cap 64 KiB"). -
obsdb, Studio:
obsdb.RunQuery.IDPrefix/obsdb.SearchQuery.IDPrefixandGET /api/runs?id_prefix=(U26; UX-A-13): the runs whose id starts with the text, byte for byte (no wildcard, no case folding; a prefix ending inside a UTF-8 sequence matches), on both backends — SQLite as a closed primary-key range. The ⌘K palette looks a pasted id up with it (all=1, children included; a panel token finds its own public id's runs only), a single word also as a public id (an app-chosen one likearena:run-1:rw.deep-delegation, not onlypub_…), akey=valuetext bytag.key=(a bare word says tags are matched askey=value), and ranks those runs first under "Exact match": Enter opens the first, and nothing when no exact match answers until the selection is moved. -
Studio: the polish batch U29.2.
GET /api/runs/{id}/export?format=json|jsonl|otlp&tree=1exports a delegation whole (UX-A-31): every subagent run under the run, breadth first, at most 200, each its own export — json asubagentslist besidesubagent_holes(truncated/response_cappast the cap,hiddenwith acountfor the runs a panel token's public id does not reach, left out with their subtrees), jsonl each run's lines after its ownrunrecord thensubagent_holelines, otlp every run in the same two requests (re-ingests as the tree); the file is<run>.tree.<ext>;wefttestwithtreeis a 400 (its replay substitutes the subagents' recorded answers); the run header offers "with child runs" beside the formats on a run with children./api/tracesrows carryroot_run{id, agent, prompt_preview, status} (read by its id, however many runs the trace holds) andfailed_runs(failed_runs_cappedwhen that count stopped at 500: a floor) (UX-A-39): the trace list titles a row "agent · prompt" and its runs cell reads "3 runs · 1 failed", one link to the top run, never the hex id list./api/runtimescarriesbreakpoints_set_at(in memory: when this Studio delivered the set) (UX-A-33): Settings says when the set was set, or that it came from before this Studio started, warns when every tool is a breakpoint, and "clear all" PUTs an empty set. Settings has a section index — sticky beside the sections on a wide window, a row above them on a phone (UX-A-32;lib/settings.ts'sSETTINGS_SECTIONS). Every page's first tab stop is "Skip to content", landing past the runs list's filter bar (UX-F-25). Under?sort=costthe runs list says how many rows have no known cost and that they are listed last, newest first, not sorted (UX-A-35;lib/cost.ts'scostSortNote). One not-found state (UX-F-31;lib/not-found.ts,components/studio/not-found.tsx): a run, session, experiment or trace 404 and an unknown page read "No in this Studio" with the way back, a run's with "search for it" (the ⌘K palette with the id typed in), and no retry. -
Studio: the polish batch U29.3 (the playground, the replay drawer, the experiment form, toasts). A bare
/playgroundover several registered agents asks instead of taking the first one (UX-C-35): "Pick an agent (N registered on M runtimes) or paste a run id in Source run", each agent one click, the five latest failed runs as one-click sources. A rejected or lost replay command offers "send again" (a new command; held by the form's own rules) (UX-C-31). A kept tool result's edit field is a textarea labelled by its call —lookup_order(order=42) · c1, so parallel calls read apart — with "start from recorded", "reset to recorded" and a size line (UX-C-25), and "edit this result" focuses that step's field even when call ids repeat across steps (UX-C-24). The replay drawer holds back its command's finish toast while it shows the outcome (UX-C-32;lib/notify.ts'squietCommand). The playground card's "save as fixture" (one download per fixture file) is the run page's "save as test" (one wefttest zip), the panel's result pane hands off to it too (UX-C-29). The preview'sprepare_stepwarning is said only when a request record of the source names what aPrepareStepchanged (the parts listed), or none was recorded; new warningscatalog(the registered tools differ from the recorded step's) andagent(the replay's agent is not the source run's) (UX-C-26). A parked app turn's refused verbs carry the refusal's head as their tooltip and point at the one line under the calls, no longer repeating it per verb (UX-A-36). At phone width the page under the open replay drawer does not scroll sideways, and the shell's place word truncates instead of pushing Search past the edge (UX-C-33).
Changed
- core:
weft.override.paramsis the run'sParamsreplacement by the request record's lower-case names — breaking for readers of the attribute: it was the Go struct's JSON ({"Temperature":0.4,…,"MaxTokens":null}). AParamsrun option replaces the agent's struct whole, so the attribute holds each knob the replacement sets andnullfor each knob the agent set and the replacement leaves unset ({"temperature":0.4,"top_p":null}over an agent withTopP);weft.override.hashtakes the set knobs alone (equal overrides hash equal), and a params override's hash differs from 0.16.0's. A named tool choice with no name readsweft.override.tool_choicetool, nevertool:(U25.1; UX-S5b-03). Contract fixturebridge_overrides. - obsdb, Studio,
weft: one status filter for/api/runsand/api/search(I3, issue 003) — breaking for/api/runscallers that relied on an unknown word:?status=takes running, succeeded, failed, interrupted and the read-timeparked(an open park: succeeded, pending calls, no run whoseresumed_fromnames it) andcancelled(failed,error_typecancelled), mapped onto stored columns on both backends, sototal,limitand the cursor describe the filtered set; any other word is a 400 naming them on both routes (in 0.16.0/api/runs?status=parkedwas an empty 200,/api/searcha 400). Newobsdb.StatusParked,obsdb.StatusCancelled,obsdb.StatusWords,obsdb.ParseStatus,obsdb.ErrInvalidStatus,obsdb.Status.Stored; obsdb/clickhouse's README states theparkedfilter's cost (a column scan ofResumedFrom).weft runs --status <word>(beside--failed, with which--status cancelledis accepted; a word outside them exits 2) andweft mcp'sruns/searchtools document the six words. The runs list's status filter is the server's and readslib/status.ts's words (statusPick,ANY_STATUS): the parked pick's count is the API's total, and cancelled is offered; a row listed understatus=parkedthat the page reads decided through a later turn (no stored resume link: pre-U13.1 data, or a resume withoutweft.ResumeOf) says "decided by a later turn the record does not link". Revises U32.2's "Not changed, accepted". - Studio: one step for the playground page (U26; UX-C-21) — breaking for share links and drafts: the matrix's own "from step" input is gone; the source block's step picker sets the step Run and every matrix cell continue from, and the page's transcript edits go with each cell (and are saved with the experiment) past step 0. The form format is v2 (
FORM_VERSION;matrix.from_stepremoved, a cloned experiment's step is the form'sstep): a v1 share link or draft is refused, not read with one of its two steps dropped, and says it is from an older Studio with the way out — re-create it from the experiment ("clone into the form"). - Studio: the ⌘K palette matches by contiguous substrings, never a subsequence (U26; UX-A-13): "1c06797f" does not list a trace whose id merely holds those characters in order, and Enter on text nothing matches opens nothing (
src/lib/finder.ts'spaletteFilter); a run's metadata pairs are keywords. - Studio:
/comparelinks write?b=as one comma list (b=r_2,r_3), not a JSON array (UX-F-30;lib/links.ts'srunsParam: the array kept only when an id holds a comma); the old array links still read. The live page's "ingest open" chip reads "accepting OTLP from loopback without a token" and links to Settings, whose ingest fact says loopback too (UX-A-34). The token wall has a heading, "Sign in to this Studio", and the sidebar keeps Traces, Agents, Playground and Experiments listed, disabled with "needs the token", until it is entered (UX-F-26). - Studio: runs list, compare and live polish (H9; UX-A-09, UX-A-10, UX-A-16, UX-A-18, UX-A-21). No page scrolls sideways at 1440 or 360 px (
scripts/ux-gate/h9.jsmeasures it; the run page's stacked tree and drawer sit in oneminmax(0,1fr)column): the shell's main column ismin-w-0, a wide table scrolls inside its own frame (positioned, so ansr-onlycell word does not widen the page), the runs table folds its quieter columns on a narrow window, a run header's long subagent id ellipsizes. The runs toolbar is one line — search, agent, status, "+ filter", the top-level toggle, a "? keys" chip in place of the key hint; the other twelve filters are folded behind "+ filter" (its button counts the set ones, each a chip as before)./livedraws a subagent under its parent, indented; a run that ended stays a minute (was 30 s), dimmed, and the first load lists the runs that ended in the last minute, so a reload keeps them; the session cell is the session's short id with the public id under it. - Studio: the run page's default view is the run tree (U8.2; UX-A-02, UX-A-04).
/runs/$iddrawslib/waterfall.ts's rows (src/lib/run-tree.tsprojects them into the waterfall's): run › step › model call (› attempt) / tool lane › child run, each row's label by the label rule ("model · script", the tool's name — no OTel span names, no per-row service), its gist ("1 tool: ops_manager", "→ 1 call", a reply's or result's first 60 characters; content-off, a tool row's result size fromweft.tool.result_bytes), its badge (finish reason, error kind, ok), its duration under "ms" with the offset on hover, a step's lanes when it ran calls side by side, and the critical path as an accent under the bars with a legend. A step or call the events have and no span covers is drawn, never left out: "spans not reported yet" while the run goes on, else badgednot_recordedin its own words (truncatedwhen the tree was cut) —WaterfallRow.untimed,UNTIMED_HOLE. The time scale is linear in the run's size (a chat span's attempts grouped once by parent span, the fold's steps and resumed calls indexed once;run-long.test.tsxpins attribute reads per span flat). The views are renamed waterfall · story · raw (?view=and thew/s/rkeys;eis gone,RunViewis"waterfall" | "story" | "raw"); "trace" names the/tracespage only. The switch isscale: time | order | graph, written as?scale=(the order value round-trips, UX-A-02;RunSearch.axis/RunLinkOptions.axisare nowscale; a pre-U8.2?axis=time|eventslink reads as time|order; thetkey steps the scale); the dim filters are chips, "dim all but: errors · slow · tools", round-tripping?dim=errors,slow,tools(RunSearch.dim). With thespans_treecapability the page reads/api/runs/{id}/spans?tree=1once at load (spansQuery(id, tree), on the waterfall only) and a delegation's child run is one collapsed row under its tool call with "open run ↗"; the run's own spans alone feed the header and the Request pane. A row's detail reads the fold's step, call or run (the story's bodies) where the fold has one, a child row its tool call's, a model call or any other span its span's facts; a time-scale row offers no "replay here" (its extent is milliseconds). The tree is accessible: the axis row (the head, the fold-all buttons, the ms axis) sits outside itsrole=tree, a call row's toggle is a button beside its verbs, the drawer's subagent button's name starts with its visible words (jsdom axe tests over the waterfall tree, the logs view, the compare page and a status-filtered runs list). Old?sel=forms resolve:run, the root's run-qualified keys, a pre-U8.2t:<span id>(the row that span is behind), a barec:<call id>, a denied call'sc:<step>:<call>(asc:resume:<call>). 999.6 ms reads "1.0s" (spanMs). - Studio, panel: one run-tree row model for both renderers (U8.1; UX-F-18, UX-A-04, UX-A-22, UX-A-23, UX-S2-07).
src/lib/waterfall.ts'swaterfallRows()builds run › step › model call (› attempt) / tool (lane) › child run on atimescale (from/spans, sub-millisecond digits kept) or anorderscale (stream positions, for a run without spans, from the same row constructors), each row with its selection key, offset and duration, a gist from the folded events and transcript ("→ 1 call", the first 60 characters of a reply or tool result), its status, tool lanes per step, the critical path and the label rule "model · script";dimRowsdims by errors / slow / tools. A child run hangs under the tool span itsinvoke_agentspan names as parent (core numberscall_1every step, so (parent run, call id) alone is ambiguous); without that span the only child under the tool's (run, call) that starts inside it, never overwritten when two steps repeat a call id. A live delegation (the child's spans before itsinvoke_agentand the parent's tool end) is a running child row under the fold's open call, else under the root, and a running run's in-flight steps and calls are open rows; a nested Generate's run (noweft.parent.*, no fold link) is a collapsed child row. String-typed attributes (ClickHouse's fallback map) read:"true"forweft.tool.approved/denied/pending, a finish list stored as a JSON string. Span maps are keyed by trace and span id; a child run'st:keys, a step's second model call (s<step>:m2) and its attempts (s<step>:m2:a<n>) are run-qualified and stable; a step's model calls are ordered by start, the step's status and finish from the last. One selection-key scheme (rowKeys): the trace page's keys are the run tree's — a chat spans<step>:m(wass<step>), a denied callc:resume:<call>(wasc:<step>:<call>), the trace's root run bare — and the old?sel=forms still resolve. The descendant-span rules (a child's tool by span or by (run, call), a collapsed child's claim) live inlib/waterfall-tree.ts(treeRows, what Studio's run page builds with); the panel reads a run's own spans and bundles none of it — the same rows for that input. The panel'sspanWindow/waterfall(src/panel/render.ts) and its Timeline's inline seq builder are gone: the Timeline draws the model's rows (on a spanless run a step precedes its calls, instead of the order they closed in);lib/trace.ts'sspansFromTimed/timeDomainwalk the model's span tree. Studio's waterfall axis reads "ms" (was "MSS") and "events", its time ticks and its right column humanised (7.7s, a duration per row, the offset on hover; the first tick "0",tookpicking its unit on the rounded value — 99.96ms reads "100ms") instead of raw millisecond ranges. - Panel: the Timeline draws the run tree (U8.3; UX-D-34, UX-F-18). The rows are Studio's run page's, one level deep: the same keys, labels by the label rule (the gist as the label's title; a child run row's label one rule on both scales and surfaces — its agent, from the child's
invoke_agentspan, else its fold, else the run document'schildren[].agent(waterfallRows'agentsinput) — else its tool's name), kinds and glyphs (lib/waterfall.ts'sROW_KIND,KIND_ICON) and tones (rowTone, moved fromlib/run-tree.ts; coloured throughlib/palette.ts'sTONE/TONE_STUDIOand a--weft-resulttoken equal to Studio's--ev-result), indented by depth, the critical path under the bar, the extent (took) with the offset as its title, untimed rows inUNTIMED_PENDING/UNTIMED_HOLE's words, a step's and a never-finished row's bar at Studio's 60% (color-mix). A child run is one row with ⤢ to Studio's run page at that row (runLink'sseloption:?sel=r:<child>); the step rows are buttons on one roving tab stop (↑/↓; a click,EnterorSpaceselects the step in Story); the run row is not clickable. Studio's waterfall rows carrydata-kind/data-tone.renderWaterfallis gone. - Studio: the answer-first drawer on the run page (U10.1; UX-A-03, UX-A-05, UX-A-42, UX-C-23). The detail pane beside the run tree is one drawer for both scales: a kind label (a model call reads MODEL, never TOOL; ATTEMPT, RUN, STEP, CHILD RUN), the verbs drawn with their words — replay from here ▸, edit…, compare… (landing on the step) — not hover-only, and where they are not offered one faint line says why (
lib/replay.ts'sreplayOffWhy,REPLAY_OFF: "replay needs the playground — <capabilities_off's reason>", "a read token cannot replay", "replay after the run ends", a derived run's stock_spans words); for a step (or its model call, attempt or tool) the answer fromsrc/lib/summarize.ts— verdict (the stop and its detail where the run failed at this step, then finish, tokens, latency, first token), sent, replied, tools ran, then; and the lazy tabs Request · Response · Tool I/O · Logs · Raw OTel (?tab=, the tabs pattern:aria-controls/aria-labelledby, roving tabindex, ←/→ wrapping, Home/End; the attrs dump moved to Raw OTel; Logs readsruns/{id}/logson open, filtered to the row's spans; a pre-U10.1?d=reads as Raw OTel). The landing selection is chosen on the rows drawn: the first failed row, else a tool that never finished, else the step a failed run failed at, else step 0 — a model error or a cancel during step N lands on step N (core appends a step's record only after its model call and tools, soweft.run.stepsN names the open step), a max_steps run and a cancel before a step began on the run's last drawn step (failedAtStep,landStep),opt.usage-limiton step 2 with its verdict; the verdict, the then line, the end card and the header's stopped-by chip and its link name the same step; "— the loop refused step N+1" is said for a guard's stop only (usage_limit, loop_detected, model_retries_exceeded, max_steps). A derived run lands by the same rule and its step's answer reads its spans — finish and tokens from the chat span's gen_ai attributes, its execute_tool spans as the calls (lib/run-tree.ts'sderivedStep) — what no span carries reading "— a derived run has no event record". "past the playhead" is said only while replaying. "read in the story" opens the story on the selection's step, and a?sel=highlights its card there. The A3 hole words are shorter on both trees (obsdb.HoleNote/HoleNoteFor,lib/honesty.ts,holes.golden.jsonand the API goldens; tail_lost's fix is the queue fix). - Panel: the Story's step reads answer first (U10.2; UX-D-12, UX-D-13's panel half). The selected step — else Studio's landing step: the failed one, else step 0, so a succeeded or running turn reads its first step's answer too — opens with the run page's drawer at panel width:
lib/summarize.ts's verdict ([data-verdict], no measured span Studio's drawer does not add) and sent / replied / tools ran / then lines ([data-answer-line]), the same words as Studio's drawer (parity.test.ts: succeeded, settling, max_steps, a model error, a cancel during and before a step, opt.usage-limit's step 2; a derived turn draws no answer — it hands the steps off), and its Logs and Raw OTel hand off — the step head's📄 ⤢(U11.2'slogsLink) and the answer'sRaw OTel ⤢open the run page at the step's row (?sel=s<n>&tab=logs|raw; no token in the URL). Every[data-weft-handoff]link in the turn view opens through the panel token's hand-off (G3) when there is one. A step's request is one line by default (the attempts, the changed marks, the badges) and opens to the prompt, the catalog and the params; params every one the adapter's default are not listed; the "insert a message" lines show on hover or focus. Subagents expand inline at any depth, each read only once opened (a grandchild's row from its parent's document; an expanded child's stream read again while its last page was not terminal); past two levels the indent stops and the row says "depth N" (lib/waterfall.ts'sdepthWord); a child's final words that equal its call's result are drawn once, on the call. - Studio, panel: phase 8's polish on the run page, run tree and header (U29.1; UX-A-24, UX-A-26, UX-A-27, UX-A-28, UX-A-29, UX-A-30, UX-A-37, UX-A-38, UX-F-15, UX-F-35). The replay bar says "live" only while the run runs; a finished run's reads "end · all N events" and its way back "back to the end" (
lib/summarize.tsreplayEnd). A nameless agent is "unnamed agent" (muted, "set weft.Name(…)" on hover) on the run header, the runs list, the run tree, a subagent row and the panel's Timeline (lib/format.tsUNNAMED_AGENT). The runs list's j/k selection carriesaria-current. A disabled "open in app" isaria-disabledand focusable, its reason read througharia-describedby. A 404 is U29.2's one not-found state (no retry); any other failed run or trace read keeps retry and gains the way back to its list (LoadFailed,lib/not-found.tsnotFoundBack). A failed tool row on the run tree reads the story's taxonomy ("panic · handler", "timeout", "retry 1/3", a ToolError code —lib/run-tree.tstreeBadge). Focus starts at the run page's title — the skip link's target there — and the tree's row chevrons leave the tab order (the tree is one stop). The story has "collapse all steps" and "only problems" (dims the steps that went smoothly;stepHasProblem). Delegation words: the header lists "child runs", the delete confirm counts "child runs", the runs list's toggle reads "with child runs" (lib/delegation.tsRUNS_TREE) and so does the export's tree toggle, a pool child parks as "a pool session's turn … started by turn" — "delegated" stays cost and usage's word. Under 480 px the run tree's expand / collapse are two icons and the axis two ticks. - Studio, panel: the polish batch U29.4 (UX-C-36, UX-C-39, UX-C-42, UX-C-44, UX-C-46, UX-C-47, UX-F-13, UX-F-19, UX-F-20). The experiments list says how each experiment went and what it ran: passed ("3 / 4 passed", the top-level runs that succeeded), engine(s), source run and step, last run —
GET /api/experimentsand/api/experiments/{id}rows gainsucceededandlast_run(omitted past the summary's walk cap) — and a filter by agent or source run (lib/experiments.ts). A matrix's shape is pluralised, "1 variant × 1 input" (matrixSize), on the playground's Matrix header and the experiment page. A scripted run carries the panel's chip, "scripted · 0 tokens" (plain "scripted" when its usage is not zero), on the run header and the experiment grid too (lib/cost.tsscriptedChip; the panel's turn chip reads the same words, was "scripted (0 tokens)"). With the replay drawer open the story rings the step it replays from ("replaying from here", scrolled to) and marks the steps before it "kept", drawn faint (lib/replay.tsreplayMarkOf). The playground's transcript-edit fields show the recorded value as a muted "recorded: …" line, never as the placeholder, and an "edited" chip on a field holding a draft. A read token's replay line names the token that may act ("… it needs the dev token or a playground panel token"). The panel's header counts "runs" unless a session scopes it ("1 run", "3 turns"; was "1 turns" for any list). The panel sets prose — a step's text, the prompt, the notes — in a system sans stack (--weft-font-prose, a new host token; mono stays for ids, JSON and numbers); its running dot and the pill's "● step" pulse (@keyframes weft-pulse;minify-css.test.tsparses the stylesheet raw and minified and fails on a parse error or ananimationwith no@keyframes). Both surfaces draw a diff in one pair (lib/palette.tsDIFF): added green, removed red and struck, a changed cell mustard — the panel's prompt diff, result-pane diff and compare values were blue / mustard. - Studio, panel: the polish batch U29.5 (UX-F-21, UX-F-22, UX-F-32, UX-F-33). A note-toned honesty badge (stripped, redacted, hidden, …) is a neutral outline on both surfaces — Studio's was the accent, the panel's blue; a loss keeps its error outline. The panel header's actions say their word beside the glyph once the dock is 480 px wide ("Studio ⤢", "layout ⇆", "theme ◐"; the glyph and its title below, a container query on the header), and its "raw" button is gone —
rand the Raw tab remain. A turn row's accessible name is its fields in words ("turn 3, parked, <title>, 2 steps, 60 in 30 out, 3h ago"). With no turn the header's tokens read "—" (not "0→0"), no "select a turn" is drawn, and the dev list says "no runs yet — connect your app (otel.Install)" (was "no runs yet (dev)"). The raw events explorer's row toggle is a button beside the row's jump and copy verbs, not arole=buttonrow around them (axenested-interactive, on the Raw OTel tab and the raw view). - Panel: 90,091 B gzipped at this release (82,608 B at 0.16.0; the cap is 96 KiB) — the shared row model, the run cards' words and the answer-first drawer are now in panel.js (
studio/web/panel-budget.jsonhas the per-item ledger). Studio's dist budget is 700 KiB gzipped (was 600; ADR 0018 §7 amended 2026-10-16 — phase 8's run-screen items; the panel.js copy counts here and under its own cap).
Breaking
- Breaking (record):
weft.override.paramsoninvoke_agentis the run'sParamsreplacement by the request record's lower-case names, withnullfor each knob the agent set and the replacement leaves unset — it was the Go struct's JSON ({"Temperature":0.4,…,"MaxTokens":null}); a params override'sweft.override.hashchanges with it (U25; UX-S5b-03). Studio reads both stored shapes. - Breaking (API):
GET /api/runs?status=with a word outside running, succeeded, failed, interrupted, parked and cancelled is a 400 naming them, as/api/searchalready was — it listed nothing before (I3, issue 003). - Breaking (UI links): playground share links and drafts made before 0.17.0 (form v1, with the matrix's own "from step") are not read; the page says so and how to re-create them from the experiment (U26; UX-C-21).
Fixed
- Studio: a panel token's run document listed child runs of other public ids (P8-scope-fix) — in 0.16.0
GET /api/runs/{id}'schildren[], and so the export's run block, carried every direct child's row (its id, public id, prompt preview, holes) to a token scoped to the parent's public id, and every row's delegated cost priced them. Now one rule (inScope: the child row carries the token's public id — the tree scope's) cuts them at the coster every route prices with: the token reads its public id's children alone — inchildren[], a step'schildrenand its calls'child_run_id, everychildren_count/children_failed(runs list, search, a session's turns, the live lane's run frames) — and the rest as onehiddenhole with acounton the run document's and the step'sholes(drawn "hidden by your token scope · 2 child runs" on both surfaces,StepHole.count; the reason in the right number, "1 child run carries …"), never named; the run's own cost still takes their usage out, its delegated figure reads not known with the reason counting them;/compare's system cell is hidden only by the system prompt's own hole (systemHidden). The server token and setup A are unchanged (TestAuthMatrixChildScope: read and playground tokens over a pub_a run with pub_b, cleared and pool-session children and a pub_b grandchild, across the run document,/api/runs,?all=1,?parent=, search, the session, step 0, the diff, the json/jsonl exports with and withouttree=1, and a live run frame). - Studio: the runs list is no longer N+1 (U9.1; UX-E-24): a page's subagent trees (runs list, search, an experiment's runs, a run document's and a step's children) are read for the own/delegated cost split one
ChildRunsread per tree level instead of aCOUNT+ page per row, at most 10,000 child rows a request (past it the affected runs' cost and counts read not known, never read one by one); the amounts are unchanged, and the delegated reasons and unpriced models list a run's children oldest-first on the runs list too, as the run page did, so a row and its run document say the same words. - obsdb/sqlite:
RunSpansuses the partial indexspans_run(UX-E-22): its read states the index'srun_id <> ''beside the equality, without which every waterfall scanned the whole spans table. - Studio:
g r/g awork on every page, as the keys help said (P8-pre-web; in 0.16.0 only on the runs list): a shell-level chord (lib/keys.ts'sgoChord), whoserdoes not toggle the run page's raw view.