Skip to content

feat(playground): add a decision model mode to the playground - #16876

Merged
RogerHYang merged 42 commits into
tonypowell/phx-1373-typesafe-decision-providerfrom
tonypowell/phx-1374-decision-playground
Oct 19, 2026
Merged

RogerHYang merged 42 commits into
tonypowell/phx-1373-typesafe-decision-providerfrom
tonypowell/phx-1374-decision-playground

Conversation

@cephalization

@cephalization cephalization commented Oct 8, 2026 •

Copy link
Copy Markdown
Contributor

resolves #16711

What

Adds a Decision tab to the playground model picker; choosing a decision model swaps that instance's chat template for a decision request, laid out like a chat template, and runs it through createDecision. Stacked on #16875.

A TypeSafe jev-latest run: the request as State and question cards above, and the Output card below with a probability bar per option, confidence, a Pretty/Raw toggle, and token counts in the footer.

Decision playground

  • Each instance owns its request like a prompt, so decision and chat instances mix; the state is one card and each question another, with the type picker where a message's role sits. Option, level, and true/false descriptions are one-line template inputs, so a variable typed there highlights like one in the instructions.
  • A Duplicate as an LLM prompt button beside each decision task's duplicate button adds a chat task that asks an LLM the same questions: the questions and criteria become the system message, the state the user message, and a JSON schema built from the questions (a choice is a string enum, a true/false question a boolean, a score an integer enum over the level indices) the response format. The decision task stays put for a side-by-side run, and the copy keeps the decision request, so picking a decision model on it restores the original.
  • The span slideover's Playground button replays a decision span with its request, model, and response loaded.
  • The picker searches first and switches model type under the search field. Decision models and datasets are mutually exclusive: the Decision tab warns that choosing a decision model clears the loaded dataset and then does so, the dataset picker is disabled while a decision instance exists, and a run that would mix them is refused with the reason on Run.
  • An empty required field is reported only after the user leaves it, so a new question, option, or level never opens in an error state; Run still refuses an invalid request and names the problem.
  • Add task (formerly Compare) and each task's duplicate button copy an existing task into a new one, with a copy-plus glyph distinct from the copy-to-clipboard icon.
  • The picker lists providers by the model types the server reports for them, and the output footer shows token counts read from the span's decision.token_count.* attributes; decision usage is not folded into LLM token or cost rollups. Nothing about decision mode is encoded in the URL; persisting playground state is follow-up work.

Try it

  1. Open the playground, pick Decision → TypeSafe → jev-latest in the model menu, press Run; observe a probability bar per option under department and View Trace opening a DECISION span.
  2. Press Add task, change B's question type to Score, press Run; observe A and B answering different questions.
  3. Press Duplicate as an LLM prompt beside the decision task's duplicate button; observe a new task with the questions as the system message, the state as the user message, and the schema as the response format, next to the untouched decision task.
  4. Open the run's trace, select the Decision span, press Playground; observe the same state and questions loaded, then press Run again.

How this PR was made

Agent User turns Steering turns Questions asked Sessions
opencode (unknown model), then Claude Code (Claude Fable 5.1) 2 approx. + 24 13 0 4
Claude Code (Claude Fable 5.1), review, fix, scorecard, review-response, and dogfooding passes 8 5 1 1

Starting point: review and test the implemented decision playground, with the meeting transcript as spec; then a Notion page of dogfooding notes and meeting decisions
Steering: approach (capability guards, catalog defaults, per-instance requests), scope (drop import/export and URL encoding, keep Compare as Add task, duplicate as an LLM prompt rather than convert in place, skip label renames), style (design review, chat-like layout, CodeMirror inputs for substituted fields), correctness (tab padding)
Abandoned: JSON criteria textareas; a nested request card; one shared request; folding decision tokens into LLM cost rollups; import/export dialogs; decision mode in the URL; a per-question copy-as menu, superseded by duplicate as LLM
Verified: tsc, oxlint, oxfmt, relay, vitest, 8-domain scorecard and Playwright checks on an isolated instance, dev-server checks of the picker, template inputs, validation timing, and duplicate as LLM. Not verified: OpenAI Decisions live, PostgreSQL, screenshot predates the fix passes
Look hardest at: useModelMenuData provider visibility, PlaygroundModelMenu dataset clearing, useDecisionRunner.ts run lifecycle, maskUntouchedErrors, buildChatFromDecisionDraft, duplicateInstance returning the copy's id, TemplateEditor singleLine mode

Stats as of 6d023e9, counted from the Claude Code transcripts; the opencode session is recall.

🤖 Generated with Claude Code

@cephalization
cephalization added this pull request to stack #16877 October 8, 2026 15:54
@cephalization
cephalization force-pushed the tonypowell/phx-1374-decision-playground branch 3 times, most recently from b83ab70 to 016f1bf Compare October 9, 2026 01:21
@cephalization
cephalization force-pushed the tonypowell/phx-1373-typesafe-decision-provider branch 2 times, most recently from 7a59aef to 50bcaaf Compare October 9, 2026 14:43
@cephalization
cephalization force-pushed the tonypowell/phx-1374-decision-playground branch 6 times, most recently from 455b93d to b975d38 Compare October 15, 2026 17:20
cephalization and others added 17 commits October 19, 2026 14:39
Introduce decision editing and execution in the playground.

Fold the immediately following tab-sizing fix into the model-menu change it completes.
Decision instances now share one request (state plus typed questions) and
differ only by model, so comparing instances means asking the same
questions over the same evidence. Model name and endpoint move into the
model parameters popover, matching chat providers.

- Typed question editor: option/description rows for Choice, ordered
  levels for Score, true/false descriptions for Noul. Field-level errors
  in each field's error slot; Run is enabled only when the request is valid.
- State editor with a Text/JSON toggle and template variables; the Inputs
  panel and variable substitution work as they do for prompts.
- Import accepts a TypeSafe System One or OpenAI Decisions body (format
  detected) and loads it into the editor. Export shows the exact body each
  instance sends, as sent or as a template, with a copy button.
- Output is one comparison grid when every instance is a decision model:
  one row per question, one column per model, probability bars per option
  or level, confidence, usage, and trace link. Single-instance output shows
  the same distributions.
- The request is stored once in canonical System One shape and converted
  per provider at the boundary, mirroring the server's conversion.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…the design system

Addresses an independent design review of the decision playground.

- Request and each question are Cards, matching the chat playground's tool
  cards; remove/add controls use Trash and quiet buttons consistently.
- Type select shows plain text in its trigger; type descriptions move to
  the field description slot. Switching type seeds valid criteria so the
  editor never opens on an error.
- Section-level errors use Alert; helper text stacks under labels.
- Probability bars use ProgressBar with gray fill for non-chosen rows and
  the primary fill for the answer; no duplicated headline for choices.
- Comparison grid is a real table built on the shared table styles, with
  a fixed question column, skeleton loading cells, the shared repetition
  selector, footer cells that let RunMetadataFooter own its padding, and a
  collapsed "How to read this" disclosure.
- Dialogs use DialogFooter and default-size buttons; export uses a banner
  warning and an EmptyState; monospace terms use Text fontFamily="mono".
- Base URL helper text lives in the field's description slot.
- Compare mode wraps decision headers instead of forcing horizontal scroll.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Apply the shared template layout and model-row spacing as one visual change.

Consolidate the small spacing follow-up instead of preserving a separate correction commit.
Decision instances no longer share one request. Each owns its state and
questions like a prompt, so instances can be compared on different
requests and mixed with chat instances in the same playground. The
request editor renders in the instance column with Import and Export
where the prompt menu sits, and output is one card per instance; the
comparison grid is gone with the shared request.

Every instance header gains a duplicate button that compares against a
copy of that instance; the page's Compare button still copies the first.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…asets

The decision output is now an Output card holding an answers card, like
the chat output's AI message, with a Pretty/Raw toggle in its header in
place of the raw-response disclosure that shifted the layout and left a
floating rule. Raw shows the pretty-printed response body and the header
copy button copies it.

With a dataset loaded the model menu still shows the Decision tab, but
disabled, with a tooltip saying why. The dataset picker was already
disabled once a decision instance exists.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Display decision usage and cost through the persisted span attributes and keep the dataset-run guard explicit.

Consolidate the initial cost path, its immediate correction, and the explanatory note so the history presents the final tracing contract.
The Playground button in the span slideover is enabled for DECISION spans
as well as LLM spans. The span-to-instance transform recognizes a decision
span and rebuilds a decision instance from it: the span's input body is
parsed with the import parser, so both System One and OpenAI Decisions
shapes work, the provider and requested model come from the decision
attributes, and the response becomes the instance's first output. Anything
it cannot recover is reported in the parsing banner and the editor still
opens on a usable request.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…conventions

- Show chat providers by the server-reported model types instead of catalog
  membership, so a provisioned provider with no built-in model list (Fireworks)
  stays in the LLM picker and decision-only providers stay out of it
- Strip modelType from saved model defaults and ignore it when a saved default
  is applied, so saving from a decision instance cannot turn chat instances
  into decision instances
- Stay in LLM mode when the URL asks for decisions but the catalog has none,
  and write decision URL params only for the first instance, which is the one
  the URL recreates
- Read credentials from the store when a run starts rather than subscribing,
  so a key edited mid-run never restarts the run
- Fetch span attributes only for decision footers and the decision catalog only
  for menus that offer decisions
- Parse the trailing GraphQL errors array in Relay network errors in the shared
  utility, so no caller needs to slice around request variables
- Take the provider's decision wire format from the server for export instead
  of naming OpenAI on the client
- Restore the chat base URL when a decision instance returns to chat without a
  remembered chat model
- Conventions: drop useMemo under the React Compiler, object parameters for
  multi-argument utilities, no single-letter identifiers, verb-prefixed
  booleans, Partial records for sparse lookups, shared limit constants, one
  ambient ModelType, chat message tints and semantic tokens for the state card
  and bars, and template-level props off the shared instance props
- Tests: hover the blocked Run button and assert the tooltip, assert the footer
  mounts for a failed call's span, cover the wire-format switch and the Relay
  error parser

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…locked state

- Keep chat invocation-parameter parsing out of decision instances in updateModel, so the
  model name and base URL of a TypeSafe decision instance can be edited again
- Keep template placeholders in Export > Template by not applying the template format there
- Drop "save as default" from decision instances, whose model name and endpoint would send
  new chat instances to a decision endpoint
- Enforce the 255-question cap on the client and name the first invalid instance and problem
  in a tooltip on the disabled Run button; explain the disabled dataset picker the same way
- Sync the first instance's decision target to the URL from the page, so prompt-backed
  switches, deletions, and rejected URL params all leave a truthful URL
- Replay a failed decision span as a failed run with its recorded error instead of an empty
  output; load span status for that
- Explain a JSON state that stops parsing once variables are filled in, name the field in
  import schema errors, label the copy buttons, and keep focus after deleting a question,
  option, or level
- Mark decision-only providers in the settings providers table
- Tests: store update on a decision-only provider, question cap, JSON state error, import
  issue paths, failed-span replay, and the invalid-request tooltip

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Dogfooding asked for the import and export dialogs to go, and for the
decision search params to go with them; playground state will be persisted
holistically in follow-up work. The span replay parser stays, since it reads
a recorded request back into a draft. "Compare" becomes "Add task", and
both it and the per-task duplicate button use a copy-plus glyph so they no
longer share the copy-to-clipboard icon.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
… clearing a dataset

Dogfooding found the LLM / Decision tabs above the search field clunky, so
the search now comes first and the tabs sit under it. The Decision tab is no
longer disabled while a dataset is loaded; it shows a notice that choosing a
decision model clears the dataset, and choosing one does so through the
same path as the dataset picker's clear button.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ves them

A new question opened with its instructions already flagged, and every
added option or level did the same. Required-field errors now wait until
the field has been visited and left, while duplicate, count, and JSON
errors still show at once; Run keeps refusing an invalid request and
naming the problem. Score levels report an empty description per level
rather than as one banner. The import-only `revision` counter goes with the
import dialog, and the design skill records the rule.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ields

Option, level, and true/false descriptions are substituted before the
request is sent, but were plain inputs, so a variable typed there had no
highlight while the same variable in the instructions did. They are now
one-line CodeMirror template inputs: the editor gains a singleLine mode that
drops any edit adding a second line, and TemplateInput dresses it in the
design system's field chrome with an error slot.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Provide duplication into an LLM prompt while preserving the original decision task.

Consolidate the superseded in-place conversion and per-question copy menu so reviewers see the final interaction.
The Compare menu replaced the Add task button, so its Button and Icon
imports are unused; canRun already folds in hasInvalidDecision, so it is no
longer a toggleRunning dependency.
- Build runDecision's inputs, state, and inputValues from the draft: a text
  state with template variables is sent as a template the server renders, a
  JSON object as an object state, and anything else whole through one input.
- Send questions as @OneOf predicate, choice, and scale inputs.
- Select the typed result and store it in the answer shape the result view
  already reads, so live runs and replayed spans display the same way.
Shared pieces for creating and editing decision evaluators: a scoring-policy editor that also edits a dataset binding's per-question override, a saved decision and version picker, and a read-only judge summary, with policy helpers mirroring the GraphQL input shape.
A decision binding's details page shows its decision version, model, inputs, questions with effective scoring (overrides marked), input mapping, and annotation names. Evaluator tables show the decision and version where LLM rows show the prompt. Editing a decision binding comes next.
Parse an annotation's provisional metadata.decision in one place. A refused question reads as Refused rather than n/a, and annotation details show the answer's probability, expected level, confidence, per-option probabilities, and model.
A decision judge's output configs are named after its questions, but its annotations are named after the binding (one question) or binding.question (several), so result columns now follow that rule. Opening a decision judge as an evaluator task stays unavailable until calibration lands.
Name decision binding annotations in one helper next to the other kinds' annotation configs, merge binding overrides with the policy helpers, read decision names through DecisionVersion.decision instead of a second query, and give the DECISION kind its own token color.
Add Evaluator offers a decision evaluator: reuse a saved judge or create one from a decision version, then map its inputs and optionally override its scoring for the dataset. Editing covers the binding and the judge (repin within the decision, scoring), warns that judge changes reach every dataset using it, and previews the saved judge. A failed binding after a created judge keeps that judge, so a retry doesn't duplicate it.
Label versions v{n} with their tags instead of an id fragment, keep the decision select full width, collapse the optional scoring sections behind a summary, lay choice scores out as a compact grid with narrow threshold and direction fields, and show a field's empty-name error only once the user has edited it or tried to save. The details page drops the judge's internal tag and the repeated direction, and decision rows no longer offer Open in playground, which decision judges can't run as yet.
The edit slideover's test now passes the binding, so the preview is named after it and scored with its override, as experiments run it.
…bject

Saved decisions with an object state, such as the datagen accuracy judge,
failed to load. The state editor gains an Inputs mode listing the input
names; it runs, saves, and runs over datasets as an object state.
…ssages

- Replaying an OpenAI decision span reads its answer list by name, not
  by array index.
- A custom provider's span replays on the built-in provider of its SDK
  (decision.system), with a note naming the custom provider.
- The save dialog takes default values, so picking a decision while
  typing no longer resets the description, tags, or name.
- Pruning a binding's override also drops scores for removed choices.
- Decision page actions show the server's message, not Relay's envelope.
@RogerHYang
RogerHYang force-pushed the tonypowell/phx-1373-typesafe-decision-provider branch from 5848e9b to 8541808 Compare October 19, 2026 21:49
@RogerHYang
RogerHYang force-pushed the tonypowell/phx-1374-decision-playground branch from 6d023e9 to 640b1ad Compare October 19, 2026 22:02
@RogerHYang
RogerHYang force-pushed the tonypowell/phx-1373-typesafe-decision-provider branch from 8541808 to 2157113 Compare October 19, 2026 22:02
@github-actions

Copy link
Copy Markdown
Contributor

Card links check

No broken Card links found. Checked external links in 5.4s

@pkg-pr-new

pkg-pr-new Bot commented Oct 19, 2026 •

Copy link
Copy Markdown

Open in StackBlitz

@arizeai/phoenix-cli

npm i https://pkg.pr.new/@arizeai/phoenix-cli@16876

@arizeai/phoenix-client

npm i https://pkg.pr.new/@arizeai/phoenix-client@16876

@arizeai/phoenix-config

npm i https://pkg.pr.new/@arizeai/phoenix-config@16876

@arizeai/phoenix-evals

npm i https://pkg.pr.new/@arizeai/phoenix-evals@16876

@arizeai/phoenix-mcp

npm i https://pkg.pr.new/@arizeai/phoenix-mcp@16876

@arizeai/phoenix-otel

npm i https://pkg.pr.new/@arizeai/phoenix-otel@16876

commit: 640b1ad

@RogerHYang
RogerHYang marked this pull request as ready for review October 19, 2026 22:10
@RogerHYang
RogerHYang requested review from a team as code owners October 19, 2026 22:10
@RogerHYang
RogerHYang removed this pull request from stack #16877 October 19, 2026 22:12
@RogerHYang
RogerHYang merged commit 33eeb0e into tonypowell/phx-1373-typesafe-decision-provider Oct 19, 2026
72 of 110 checks passed
@RogerHYang
RogerHYang deleted the tonypowell/phx-1374-decision-playground branch October 19, 2026 22:14
@github-project-automation github-project-automation Bot moved this from 📘 Todo to ✅ Done in phoenix Oct 19, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: ✅ Done

Development

Successfully merging this pull request may close these issues.

[playground] support decision models in the playground

2 participants