Repository navigation
Expand experiment model expressions before writing inline sub-agents; fail smoke on failed or missing delegation #70386
Description
Problem
smoke-copilot.md declares experiments.subagent_model: [small, large], but its inline file-summarizer is written with the unresolved model expression:
## agent: `file-summarizer`
---
model: ${{ experiments.subagent_model }}
description: Summarizes the content of a file in a few concise sentences
---The selected experiment variant and recorded metadata can be correct while the actual agent definition is wrong. When invoked, the sub-agent sends the expression itself to AWF and fails. Actions can nevertheless report success. A smoke that never invokes the sub-agent can also stay green.
Verified runs
Audited both supplied runs using a freshly built development CLI from main at b68bd31219d023ae8b4b9137ba1c6a397d7918f1 (gh aw version dev-b68bd31219), not the older installed release. Used the built executable's equivalent of:
gh aw audit 39181772988 39325986042 --repo github/gh-aw --group --json --artifacts info,usage,agent,experiment,activation,firewall
Inspected the downloaded agent definitions, experiment assignments, aw_info.json, unified sessions, agent output, and activation/job logs. Times below are UTC.
| Run | Revision / trigger | Observed result |
|---|---|---|
| 39181772988, October 19, 2026, 00:39 | main at 58c1d50e46434238b52b1bd2d7c976cd438b5b6e; schedule |
Actions success. Variant small; written agent still contains the literal expression. One file-summarizer invocation fails with AWF HTTP 400. Audit reports subagent_failed. |
| 39325986042, October 19, 2026, 21:14 | #70294 branch copilot/add-session-contract-checks at 893ffe1377ec1997588ac17dfdbbcb8ecb499a3f; workflow dispatch |
Actions success. Same variant small and same malformed agent file, but no sub-agent invocation or sub-agent events. Agent output explicitly lists file-summarizer as not run. The unified-session post-step passes E2/U1/U2/U3, with only the main resolution required. |
Important distinction: the second run reproduces the malformed artifact, not the runtime 400. Its lack of subagent.failed is not evidence that delegation worked.
Both runs contain:
smokecopilot-experiment/assignments.json: {"caveman":"yes","subagent_model":"small"}
aw_info.json: sub_agent_models[0].model == "small"
.github/agents/file-summarizer.agent.md: model: ${{ experiments.subagent_model }}
In the first run, usage/aw_session.jsonl records:
00:45:45.881Z subagent.started: agentName=file-summarizer,
model="${{ experiments.subagent_model }}",
modelSelectionSource=agent_definition_default
00:45:46.200Z subagent.failed: durationMs=280, totalToolCalls=0,
error='400 Unsupported model selector "${{ experiments.subagent_model }}" on copilot: no advertised display name matches. ...'
Evidence entry points: main activation, interpolation step, main agent execution, PR-branch activation, PR-branch agent execution, passing PR-branch contract assertions. Both runs use Copilot CLI 1.0.98 and AWF v0.28.62. Neither run's lock metadata or aw_info.json exposes a compiler version; do not infer one from the installed CLI.
These two runs do not demonstrate either experiment arm successfully executing. This investigation does not establish that the experiment has never worked anywhere in its entire history.
Code diagnosis
References below are pinned to the inspected main revision.
-
Variant selection exists and precedes prompt rendering.
pick_experiment.cjsselects a variant and exposes per-experiment outputs and the assignments map.ExperimentExpressionMappingssuppliesGH_AW_EXPERIMENTS_*fromsteps.pick-experiment.outputs.*.expression_extraction.gorewrites direct prompt experiment expressions to those outputs. Main-model configuration is also rewritten to the activation outputs. -
The runtime ordering loses the sub-agent before later interpolation.
interpolate_prompt.cjsperforms inline sub-agent extraction at step 1.5, then variable interpolation at step 2, experiment-placeholder/{{#if experiments.* }}substitution at step 2.5, and conditional rendering at step 3.writeInlineSubAgentspreserves the frontmatter and immediately writes the file. Later passes see only the remaining main prompt, not the extracted agent. -
Runtime imports do not rescue this field.
runtime_import.cjspermits experiment expressions but does not resolve them throughGH_AW_EXPERIMENTS_*inevaluateExpression; its unresolved-expression path returns the literal${{ ... }}. Step 2.5 handles__GH_AW_EXPERIMENTS_*__placeholders and experiment condition tags, not arbitrary raw${{ experiments.* }}fields. Merely moving extraction a little later without covering the actual expression forms is insufficient. The latersubstitute_placeholders.cjsonly processes prompt/split-prompt files. -
Metadata is already partially fixed; do not misdiagnose it as still literal in these runs.
ExtractSubAgentModelsreads the raw declaration, butwriteSubagentModelDeclarationsrewrites declared experiments to activation-local step outputs before emittingGH_AW_INFO_SUB_AGENT_MODELS. The observed environment andaw_info.jsonalready containsmall. Fix the execution artifact, not just metadata. -
The simple expression is intentionally accepted today.
validateExperimentalSubagentModelsaccepts a declared simple experiment reference while rejecting undeclared/compound references.TestCompileCopilotWithExperimentalSubagentModelsexplicitly expects successful compilation for this form, including imported declarations. Its generated-YAML/metadata assertions do not verify the runtime-written agent. -
Green is not a sub-agent success assertion.
smoke-copilot.mdcallssessionContractMainwithresolutionRoles: ["main"].evaluateSessionContractchecks session collection, inventory, and explicitly required resolution roles, not successfulfile-summarizerexecution. The same helper file already has S1/S2 sub-agent lifecycle checks in its routing-check path; prefer reusing appropriate existing evidence/checks rather than duplicating infrastructure.
Local reproduction: on the same main, pinned GH_AW_EXPERIMENTS_SUBAGENT_MODEL separately to small and large, passed a minimal inline declaration through the real processExpressions and writeInlineSubAgents, and read the emitted file. Both retained the literal expression; a plain-model control was unchanged. This component reproduction used disposable local files and no workflow dispatch.
Historical corroboration: #48639, "[aw] Smoke Copilot has no AI credits pricing for model (${{ experiments.subagent_model }})", reported the same literal reaching pricing and was closed not planned on July 29, 2026 (03:46:20Z). That symptom is consistent with this unresolved-expression gap; adding pricing or an AWF alias for the expression itself is not a fix. The July run was not re-audited here.
Preferred solution
Support the already-accepted simple ${{ experiments.<name> }} form in inline sub-agent frontmatter, at least model:.
- Resolve it using the same already-selected assignment as the prompt, before writing the engine's agent file. Do not reselect a variant, evaluate arbitrary expressions, or weaken expression safety checks. Cover both ordinary runtime-imported workflow content and inlined prompt content.
- Pass the resulting
small/largevalue through normal model/alias handling; do not hardcode a concrete model or alter pricing, endpoint policy, or budgets to accommodate a literal expression. - Preserve existing metadata rewriting. Ensure assignment, emitted agent model, and sub-agent model metadata agree. If retaining the authored expression for provenance, identify it as an expression/experiment rather than a model ID; distinguish the selected alias from the ultimately resolved provider model.
- Add a narrow assertion to smoke-copilot's existing post-steps requiring
file-summarizerto be invoked and complete successfully, with nosubagent.failedfor it. Fail on a missing invocation as well as a failed start. Use actual lifecycle/model evidence, not the parent agent's prose or a green exit code. If this check cannot be added, explain why in the PR.
If maintainers choose not to support this syntax, reject it at compile time with a clear error naming the sub-agent and model field, and change the smoke to a supported form. Silently accepting a nonfunctional declaration is not acceptable.
Acceptance cases: named tests
| Proposed test name | Required assertion |
|---|---|
writes_selected_experiment_model_before_agent_file_write |
A workflow with experiments: {subagent_model: [small, large]} and this inline model: produces small or large matching the assignment, never a raw expression/placeholder. Exercise the real interpolation/write pipeline, not only parser or YAML metadata output; cover runtime-import and inlined forms. |
both_subagent_model_variants_are_reachable |
Deterministically pin each assignment and demonstrate both small and large reach the emitted agent file. No probabilistic assertions. |
preserves_plain_subagent_model_and_frontmatter |
A plain model is unchanged; unrelated frontmatter and agent content remain intact. |
subagent_metadata_matches_selected_experiment_variant |
Recorded selected model agrees with the emitted agent and assignment; no literal expression is represented as a concrete model ID. Preserve the existing metadata tests. |
smoke_fails_when_file_summarizer_fails_to_start |
A failed-start / subagent.failed fixture makes the smoke post-step fail; successful completion passes. |
smoke_fails_when_file_summarizer_is_not_invoked |
A session with no file-summarizer invocation fails even when inventory and main-model resolution are valid. |
TestCompileRejectsUnsupportedInlineSubagentExperimentModel (only if support is declined) |
Compilation fails with an error naming file-summarizer and its model field; the smoke uses the supported alternative. |
End to end: in a permitted execution context, dispatch smoke-copilot on the implementation PR branch and list the run URL and tested commit in the PR. Verify that file-summarizer actually starts with a real model resolved from the selected small/large variant, completes, produces successful model/request evidence, and records no AWF unsupported-selector 400 or subagent.failed for it. A skipped sub-agent does not satisfy this requirement.
Scope
- Keep the PR to this one fix. Do not change files or behavior unrelated to it.
- Before fixing a failing check, compare it with the same check on
main. If it also fails onmain, do not fix it in this PR; mergemainoncemainis fixed, and note in the PR which checks are known failures onmain. - This applies even if a bot comment asks to fix a failing check: fix only failures caused by this PR's changes.
Activity