Skip to content

🤖 feat(workflows): evaluation adapter, step admission schema and evaluation model default (L3a) - #4324

Merged
ThomasK33 merged 6 commits into
thomask33/eval-l2b-evaluation-servicefrom
thomask33/eval-l3a-evaluation-adapter
Sep 23, 2026
Merged

ThomasK33 merged 6 commits into
thomask33/eval-l2b-evaluation-servicefrom
thomask33/eval-l3a-evaluation-adapter

Conversation

@ThomasK33

@ThomasK33 ThomasK33 commented Sep 22, 2026

Copy link
Copy Markdown
Member

Summary

Layer L3a of the workflow evaluate() plan (stack: L1 SDK pins → L2a schemas → L2b resolver + EvaluationServiceL3a adapter + persistence schemas + config → L3b runner evaluate() + CLI → L4 Settings/timeline → L5 TypeSafe → L6 example). Adds the headless pieces L3b wires into WorkflowRunner, without touching the runner, the sandbox API, or any construction site yet: the persisted admission on step records and an evaluation run event, the evaluationDefaults.model app-config field, and WorkflowEvaluationAdapter (selection, dispatch, accounting). Nothing user-visible changes in this PR.

Implementation

  • Schemas (src/common/orpc/schemas/workflow.ts): WorkflowStepRecordSchema.evaluation?: EvaluationAdmissionSchema (the L2a admission; written atomically with the started record and carried on every later record so the latest-wins merge always retains it) and a new evaluation event (started|completed|failed|cached, display data only; title/modelString/responseModelId are untrusted display metadata per the plan's security model). Failure identity is a finite allowlist: EvaluationStepFailureReason/Code in src/common/types/evaluation.ts = the L2 service reasons + deadline, unauthorized, admission-mismatch, admission-missing, attempts-exhausted (+ codes no-model, unsupported-provider|route, unknown-model). The exhaustive WorkflowRunEvent switches in WorkflowRunToolCall.tsx and projectWorkflowRun.ts gain the new case (label, detail = usage, tone, step-bearing).
  • Config: EvaluationDefaultsSchema { model? } on disk, ProjectsConfig.evaluationDefaults, load/save normalization (trimmed; blank clears the block so no stale {} lingers), getClientConfig()/config.getConfig projection, and config.updateEvaluationDefaults({ model }) (router + Config.updateEvaluationDefaults), mirroring updateHeartbeatDefaultPrompt. The L4 Settings card reads/writes this.
  • WorkflowEvaluationAdapter (src/node/services/workflows/WorkflowEvaluationAdapter.ts), constructed from { evaluationService, aiService, sessionUsageService, config, workspaceId, evaluationModelOverride? }:
    • resolveSelection(spec, persisted?): first attempt = per-call model > CLI --evaluation-model > Settings default (none ⇒ invalid-input/no-model, no resolver call); later attempts use the persisted selection only and refuse a changed configFingerprint (admission-mismatch); every call re-reads credentials so revocation surfaces as unauthorized. Resolver rejections map through an exhaustive Record.
    • dispatch(pinned, call, { runtimeAbortSignal, attemptDeadlineAt })EvaluationOutcome via L2b's runEvaluationToOutcome; the service call is built inside Effect.suspend so an already-aborted or already-expired attempt constructs nothing.
    • recordUsage(pinned, result, { runId, stepDigest, attempt }): one recordHeadlessUsage(..., { analyticsSource: "workflow_evaluation", metadataModel }) with null counts dropped and only object-shaped usage metadata forwarded; never throws. recordHeadlessUsage swallows its own write failures (logs the cause, resolves undefined — the same value as for a removal-tombstoned workspace), so the adapter logs the fixed code evaluation-ledger-not-recorded with run/step-digest/attempt context whenever no row landed, wakes analytics ingestion only when one did, and keeps a defensive evaluation-ledger-failed catch for an unexpected throw (digest/number fields only, no error text). Usage with an unknown input or output count is skipped (evaluation-ledger-skipped-unknown-usage) rather than priced as zero.

Validation

  • WorkflowEvaluationAdapter.test.ts (fake resolver/service/ledger/config): precedence per-call > override > default; no model ⇒ no-model with zero resolver calls; all four resolver rejections mapped; persisted selection wins over a changed default and override; fingerprint mismatch ⇒ admission-mismatch; revocation on resume ⇒ unauthorized; blank override rejected at construction; dispatch passes the pinned instance and provider options and classifies success/typed failure (429); already-fired abort ⇒ interrupted and elapsed deadline ⇒ deadline with zero service calls; ledger row shape + analytics ingest wake; unknown input/output ⇒ no row + skipped code (missing total derived); ledger undefinednot-recorded code with step context and no ingest; an unexpected throw ⇒ failed code and the thrown text never reaches the log line (sentinel); blank per-call model refused with zero resolver calls.
  • schemas/workflow.test.ts: admission-bearing and legacy step records both parse; evaluation event lifecycle round-trips; a free-text reason is rejected. appConfigOnDisk.test.ts + tests/ipc/config/evaluationDefaults.test.ts (trim, clear on blank/null, getConfig projection through the real oRPC router).
  • make static-check green; bun test src/node/services/workflows src/common/orpc/schemas src/browser/features/RightSidebar/Workflows src/node/services/evaluation 518/0.

Follow-ups

  • Timeline coalescing of evaluation events (Codex round 5, thread, deferred): getWorkflowDisplayRows in WorkflowRunToolCall.tsx coalesces task/workflow/patch lifecycles into one row per step but currently renders each evaluation event as its own row (and a terminal event without title falls back to the step id). No runner emits these events until L3b, so nothing is user-visible yet; the plan's L4 layer (timeline rendering) owns this and will coalesce by stepId/inputHash/attempt, keeping the started title and showing the latest status.
  • Usage of billed-but-rejected evaluations (Codex round 6, thread, deferred): when the provider answers but EvaluationService rejects the output (invalid-output), the response's usage is dropped with the error, so that spend is not ledgered. The plan's v1 contract records usage only for completed attempts; carrying sanitized usage on EvaluationErrorEvaluationOutcome → a post-recordStepFailed ledger write is a cross-layer change (L2b error contract + L3b runner) to be delivered as its own stacked follow-up.

Rebase note (2026-09-22)

Rebased onto main after #4307 landed (see #4322 for the two-factories decision). #4307 also added autoModelRouting.evaluationModel (the auto-router's classifier, defaulting to TypeSafe's Jev). It is independent of this PR's evaluationDefaults.model (explicit workflow evaluator with its own admission and billing semantics); the schema comment says so and neither setting falls back to the other. Unifying the two Settings surfaces, if ever, is L4/L5 territory.

Risks

Low. Additive schema fields (optional; existing records parse unchanged — the accepted downgrade scope is that a previous release cannot read runs containing the new event, which no run can contain until L3b exists), an additive config field, and a new class with no callers yet. The only edits to existing behavior are the new case "evaluation" branches in two UI switches.


Generated with xum • Model: coder:anthropic/claude-fable-5-1 • Thinking: xhigh • Cost: $244.23

@ThomasK33
ThomasK33 added this pull request to stack #4323 September 22, 2026 03:24
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 22, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review Completed 2026-09-23T06:00:51.670378Z febb058 New commits
🔒 Security Review Completed 2026-09-23T05:59:12.541085Z febb058 New commits
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: ba77f82e48

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/node/services/workflows/WorkflowEvaluationAdapter.ts Outdated
Comment thread src/node/services/workflows/WorkflowEvaluationAdapter.ts

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 3846af8324

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/node/services/workflows/WorkflowEvaluationAdapter.ts
@ThomasK33

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

🛡️ Codex Security Review · Automatically triggered

Security review completed. No security issues were found in this pull request.

Reviewed commit: 3846af8324

View security finding report

Only the user who started this review can view the report in Codex.

ℹ️ About Codex security reviews in GitHub

This is an experimental Codex feature. Security reviews are triggered when:

  • You comment "@codex security review"
  • A regular code review gets triggered (for example, "@codex review" or when a PR is opened), and you’re opted in so security review runs alongside code review

Once complete, Codex will leave suggestions, or a comment if no findings are found.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 3846af8324

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/browser/features/RightSidebar/Workflows/projectWorkflowRun.ts
Comment thread src/node/services/workflows/WorkflowEvaluationAdapter.ts

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 786ff21362

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/common/orpc/schemas/workflow.ts

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 6f1d0fbcfa

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/browser/features/Tools/WorkflowRunToolCall.tsx
Comment thread src/node/services/workflows/WorkflowEvaluationAdapter.ts

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: abdd1ad5e3

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/node/services/workflows/WorkflowEvaluationAdapter.ts
…tion model default (L3a)

Adds the headless pieces the workflow `evaluate()` step (L3b) wires into
WorkflowRunner, without touching the runner yet:

- `WorkflowStepRecordSchema.evaluation` (persisted admission) and a new
  `evaluation` run event with a finite failure identity
  (`EvaluationStepFailureReason`/`Code`); exhaustive UI switches extended.
- `evaluationDefaults.model` app-config field with load/save normalization,
  `getConfig` projection and `config.updateEvaluationDefaults`.
- `WorkflowEvaluationAdapter`: selection precedence (per-call > CLI override >
  Settings default; persisted selection + fingerprint check on later
  attempts), lazy dispatch through `runEvaluationToOutcome`, and a
  never-throwing headless usage write tagged `workflow_evaluation`.

_Generated with `xum` • Model: `coder:anthropic/claude-fable-5-1` • Thinking: `xhigh` • Cost: `$244.23`_

<!-- mux-attribution: model=coder:anthropic/claude-fable-5-1 thinking=xhigh costs=244.23 -->
…and never price unknown counts as zero

Codex round 1 on the L3a adapter:
- `requestAnalyticsIngest?` hook (same as AgentStatusService) called when
  `recordHeadlessUsage` returns a row; headless runs have no stream-end.
- Usage with an unknown input or output count is skipped with the fixed code
  `evaluation-ledger-skipped-unknown-usage` instead of being recorded with the
  missing component priced at zero; a missing total is derived.

_Generated with `xum` • Model: `coder:anthropic/claude-fable-5-1` • Thinking: `xhigh` • Cost: `$244.23`_

<!-- mux-attribution: model=coder:anthropic/claude-fable-5-1 thinking=xhigh costs=244.23 -->
…ct blank per-call models

Codex round 3 on the L3a adapter:
- `projectWorkflowRun` takes an evaluation step's title from the `evaluation`
  event like it does for agent-step/task events.
- `WorkflowEvaluateSpecSchema.model` rejects blank strings so a blank per-call
  model cannot shadow the CLI/Settings default; the adapter asserts the same
  invariant before applying precedence.

_Generated with `xum` • Model: `coder:anthropic/claude-fable-5-1` • Thinking: `xhigh` • Cost: `$244.23`_

<!-- mux-attribution: model=coder:anthropic/claude-fable-5-1 thinking=xhigh costs=244.23 -->
Codex round 4 on L3a: the `evaluation` run event requires a non-empty title
when present, so `WorkflowEvaluateSpecSchema.title` now refuses blank strings
up front (like `id` and `model`) instead of letting the event append fail
after admission.

_Generated with `xum` • Model: `coder:anthropic/claude-fable-5-1` • Thinking: `xhigh` • Cost: `$244.23`_

<!-- mux-attribution: model=coder:anthropic/claude-fable-5-1 thinking=xhigh costs=244.23 -->
…ge row does not land

Codex round 5 on L3a: `recordHeadlessUsage` swallows its own write failures
and resolves `undefined` (also its tombstoned-workspace value), so the
adapter's catch never ran in production. The adapter now logs
`evaluation-ledger-not-recorded` with run/step-digest/attempt context whenever
no row was recorded and wakes analytics ingestion only when one was; the
catch stays as a defensive backstop.

_Generated with `xum` • Model: `coder:anthropic/claude-fable-5-1` • Thinking: `xhigh` • Cost: `$244.23`_

<!-- mux-attribution: model=coder:anthropic/claude-fable-5-1 thinking=xhigh costs=244.23 -->
…ing.evaluationModel

Main landed #4307 (auto model routing) with its own `autoModelRouting.evaluationModel`
classifier setting while this stack was in review. The two settings stay
independent by design (different defaults, admission and billing semantics);
say so at the schema so neither grows a fallback onto the other.

_Generated with `xum` • Model: `coder:anthropic/claude-fable-5-1` • Thinking: `xhigh` • Cost: `$244.23`_

<!-- mux-attribution: model=coder:anthropic/claude-fable-5-1 thinking=xhigh costs=244.23 -->
@ThomasK33
ThomasK33 added this pull request to the merge queue Sep 23, 2026
Merged via the queue into main with commit 1433ff2 Sep 23, 2026
19 of 20 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant