Skip to content

Commit d39be74

Browse files
jamesadevineCopilot
andcommitted
feat(compile): runtime prompt injection via prompt.js bundle
Default behaviour now renders the agent prompt at pipeline runtime via a new `prompt.js` ado-script bundle, instead of embedding the body in the compiled YAML. Body-only edits to the agent .md no longer require recompiling the pipeline. Set `inlined-imports: true` in front matter to opt out and keep the legacy heredoc-embedded behaviour. The runtime contract is a new `PromptSpec` IR (mirrors `GateSpec`): schemars-derived JSON Schema, `json-schema-to-typescript` codegen into `types-prompt.gen.ts`, base64-encoded into a single `ADO_AW_PROMPT_SPEC` env var on the prompt.js step. `prompt.js` reads the source from the workspace, strips front matter, appends extension supplements, and substitutes `${{ parameters.NAME }}` and `$(VAR)` patterns at runtime via env vars. Secret variables stay secure-by-default (not auto-exposed). Existing compiled YAMLs will fail `ado-aw check` after upgrade until recompiled. Use `inlined-imports: true` as the one-line escape hatch. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 4c1c209 commit d39be74

25 files changed

Lines changed: 1292 additions & 89 deletions

.github/workflows/ado-script.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,9 +39,9 @@ jobs:
3939

4040
- name: Verify generated TypeScript is up to date
4141
run: |
42-
if ! git diff --exit-code -- scripts/ado-script/src/shared/types.gen.ts; then
42+
if ! git diff --exit-code -- scripts/ado-script/src/shared/types.gen.ts scripts/ado-script/src/shared/types-prompt.gen.ts; then
4343
echo ""
44-
echo "::error::types.gen.ts is out of date with the Rust IR."
44+
echo "::error::Generated TS types are out of date with the Rust IR."
4545
echo "Run 'cd scripts/ado-script && npm run codegen' and commit the result."
4646
exit 1
4747
fi

.github/workflows/release.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -69,8 +69,8 @@ jobs:
6969
npm ci
7070
npm run build
7171
# `npm run build` runs codegen + ncc + copies dist/gate/index.js
72-
# to ../gate.js (i.e. scripts/gate.js), which is then included in
73-
# scripts.zip by the next step.
72+
# to ../gate.js and dist/prompt/index.js to ../prompt.js, both of
73+
# which are then included in scripts.zip by the next step.
7474

7575
- name: Package scripts bundle
7676
run: |

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
target
22
examples/sample-agent.yml
33
scripts/gate.js
4+
scripts/prompt.js
45
*.pyc
56
__pycache__/

AGENTS.md

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ Every compiled pipeline runs as three sequential jobs:
5555
│ │ ├── gitattributes.rs # .gitattributes management for compiled pipelines
5656
│ │ ├── filter_ir.rs # Filter expression IR: Fact/Predicate types, lowering, validation, codegen
5757
│ │ ├── pr_filters.rs # PR trigger filter generation (native ADO + gate steps)
58+
│ │ ├── prompt_ir.rs # PromptSpec IR: schemars-typed runtime contract for prompt.js
5859
│ │ ├── extensions/ # CompilerExtension trait and infrastructure extensions
5960
│ │ │ ├── mod.rs # Trait, Extension enum, collect_extensions(), re-exports
6061
│ │ │ ├── github.rs # Always-on GitHub MCP extension
@@ -120,8 +121,9 @@ Every compiled pipeline runs as three sequential jobs:
120121
├── ado-aw-derive/ # Proc-macro crate: #[derive(SanitizeConfig)], #[derive(SanitizeContent)]
121122
├── examples/ # Example agent definitions
122123
├── scripts/ # Supporting scripts shipped as release artifacts
123-
│ ├── ado-script/ # TypeScript workspace for bundled gate.js (and future bundles)
124-
│ └── gate.js # Bundled gate evaluator (built from scripts/ado-script/, see docs/ado-script.md)
124+
│ ├── ado-script/ # TypeScript workspace for bundled gate.js + prompt.js
125+
│ ├── gate.js # Bundled gate evaluator (Setup job; see docs/ado-script.md)
126+
│ └── prompt.js # Bundled prompt renderer (Agent job; see docs/ado-script.md)
125127
├── tests/ # Integration tests and fixtures
126128
├── docs/ # Per-concept reference documentation (see index below)
127129
├── Cargo.toml # Rust dependencies
@@ -133,7 +135,7 @@ Every compiled pipeline runs as three sequential jobs:
133135
- **Language**: Rust (2024 edition) - Note: Rust 2024 edition exists and is the edition used by this project
134136
- **CLI Framework**: clap v4 with derive macros
135137
- **Error Handling**: anyhow for ergonomic error propagation
136-
- **Bundled scripts**: TypeScript + ncc (`scripts/ado-script/`) — compiled gate evaluator and future internal helpers; see [`docs/ado-script.md`](docs/ado-script.md).
138+
- **Bundled scripts**: TypeScript + ncc (`scripts/ado-script/`) — compiled gate evaluator (`gate.js`), prompt renderer (`prompt.js`), and future internal helpers; see [`docs/ado-script.md`](docs/ado-script.md).
137139
- **Async Runtime**: tokio with full features
138140
- **YAML Parsing**: serde_yaml
139141
- **MCP Server**: rmcp with server and transport-io features
@@ -185,8 +187,9 @@ index to jump to the right page.
185187
specification: `Fact`/`Predicate` types, three-pass compilation (lower →
186188
validate → codegen), gate step generation, adding new filter types.
187189
- [`docs/ado-script.md`](docs/ado-script.md)`ado-script` workspace
188-
(`scripts/ado-script/`): the bundled TypeScript runtime helpers (today:
189-
`gate.js`), schemars-driven type codegen, and the A2 design decision.
190+
(`scripts/ado-script/`): the bundled TypeScript runtime helpers
191+
(`gate.js` for trigger gates, `prompt.js` for runtime prompt
192+
rendering), schemars-driven type codegen, and the A2 design decision.
190193
- [`docs/local-development.md`](docs/local-development.md) — local development
191194
setup notes.
192195

docs/ado-script.md

Lines changed: 76 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,13 @@
22

33
`ado-script` is the umbrella name for **internal**, compiler-targeted
44
TypeScript bundles that ado-aw emits into compiled pipelines as runtime
5-
helpers. The first (and currently only) bundle is **`gate.js`**, the
6-
trigger-filter gate evaluator.
5+
helpers. There are currently two bundles:
6+
7+
- **`gate.js`** — the trigger-filter gate evaluator (Setup job).
8+
- **`prompt.js`** — the runtime prompt renderer that reads the agent
9+
`.md` from the workspace, strips front matter, applies variable
10+
substitution, and assembles the prompt with extension supplements
11+
(Agent job; default behaviour as of v0.22).
712

813
> Internal-only: `ado-script` is not a user-facing front-matter feature.
914
> Authors do **not** write `ado-script:` blocks in their agent markdown.
@@ -46,26 +51,34 @@ scripts/ado-script/ # TS workspace
4651
├── tsconfig.json # NodeNext, ESNext target
4752
├── src/
4853
│ ├── shared/ # Reusable across all bundles
49-
│ │ ├── types.gen.ts # AUTO-GENERATED from Rust IR
54+
│ │ ├── types.gen.ts # AUTO-GENERATED from Rust IR (gate)
55+
│ │ ├── types-prompt.gen.ts # AUTO-GENERATED from Rust IR (prompt)
5056
│ │ ├── auth.ts # ADO token / collection URI plumbing
5157
│ │ ├── ado-client.ts # azure-devops-node-api wrapper + retries
5258
│ │ ├── env-facts.ts # Pipeline-variable readers
5359
│ │ ├── policy.ts # Failure-policy state machine
5460
│ │ └── vso-logger.ts # ##vso[…] command emitters
55-
│ └── gate/ # gate.js entry point
61+
│ ├── gate/ # gate.js entry point
62+
│ │ ├── index.ts # main()
63+
│ │ ├── bypass.ts # build-reason auto-pass
64+
│ │ ├── facts.ts # fact acquisition (env + REST)
65+
│ │ ├── predicates.ts # 11 predicate evaluators
66+
│ │ └── selfcancel.ts # best-effort build cancellation
67+
│ └── prompt/ # prompt.js entry point
5668
│ ├── index.ts # main()
57-
│ ├── bypass.ts # build-reason auto-pass
58-
── facts.ts # fact acquisition (env + REST)
59-
├── predicates.ts # 11 predicate evaluators
60-
└── selfcancel.ts # best-effort build cancellation
61-
├── test/ # End-to-end smoke tests
62-
└── dist/gate/index.js # ncc-bundled output (gitignored)
69+
│ ├── frontmatter.ts # YAML front-matter stripper
70+
── substitute.ts # ${{ parameters.* }} / $(VAR) substitution
71+
├── test/ # End-to-end smoke tests (gate + prompt)
72+
└── dist/ # ncc-bundled output (gitignored)
73+
├── gate/index.js
74+
└── prompt/index.js
6375
```
6476

6577
The release workflow (`.github/workflows/release.yml`) runs `npm ci &&
66-
npm run build` and copies `dist/gate/index.js` to `scripts/gate.js`,
67-
which is then included in the `scripts.zip` release asset that pipelines
68-
download at runtime.
78+
npm run build` and copies `dist/gate/index.js` to `scripts/gate.js` and
79+
`dist/prompt/index.js` to `scripts/prompt.js`, both of which are then
80+
included in the `scripts.zip` release asset that pipelines download at
81+
runtime.
6982

7083
## Schema codegen — preventing drift
7184

@@ -91,16 +104,20 @@ The pipeline:
91104
└──────────────────────────────┘
92105
```
93106

94-
`npm run codegen` runs both stages. The `ado-script` CI workflow
95-
(`.github/workflows/ado-script.yml`) regenerates the file and runs
107+
`npm run codegen` runs the full pipeline: it exports both the
108+
`gate-spec.schema.json` and `prompt-spec.schema.json` from Rust, then
109+
runs `json2ts` to regenerate `src/shared/types.gen.ts` (gate) and
110+
`src/shared/types-prompt.gen.ts` (prompt). The `ado-script` CI workflow
111+
(`.github/workflows/ado-script.yml`) regenerates both files and runs
96112
`git diff --exit-code` to fail on drift. If you change the IR shape in
97113
Rust, you must run `cd scripts/ado-script && npm run codegen` and
98-
commit the regenerated `types.gen.ts`.
114+
commit the regenerated `*.gen.ts` files.
99115

100-
The Rust subcommand that emits the schema is intentionally hidden:
116+
The Rust subcommands that emit the schemas are intentionally hidden:
101117

102118
```sh
103-
cargo run -- export-gate-schema --output schema/gate-spec.schema.json
119+
cargo run -- export-gate-schema --output schema/gate-spec.schema.json
120+
cargo run -- export-prompt-schema --output schema/prompt-spec.schema.json
104121
```
105122

106123
## How the gate bundle is wired into emitted pipelines
@@ -120,6 +137,30 @@ steps when any `filters:` block is active:
120137
The IR-to-bash codegen lives in `compile_gate_step_external`
121138
(`src/compile/filter_ir.rs:~1100`).
122139

140+
## How the prompt bundle is wired into emitted pipelines
141+
142+
`generate_prepare_agent_prompt` in `src/compile/common.rs` injects a
143+
parallel three-step bundle into the **Agent job** when
144+
`inlined-imports: false` (the default):
145+
146+
1. **`NodeTool@0`** — same Node 20.x install as for `gate.js`.
147+
Idempotent across multiple invocations in the same job.
148+
2. **`curl` download** — fetches `scripts.zip` and extracts
149+
`prompt.js` to `/tmp/ado-aw-scripts/prompt.js`. Each pool agent
150+
downloads its own copy; the Setup and Agent jobs run on independent
151+
agents so the download isn't shared.
152+
3. **`bash: node '/tmp/ado-aw-scripts/prompt.js'`** — runs the renderer
153+
with `ADO_AW_PROMPT_SPEC` (base64 JSON of the `PromptSpec`) plus one
154+
`ADO_AW_PARAM_<NAME>: ${{ parameters.<NAME> }}` env per declared
155+
parameter.
156+
157+
When `inlined-imports: true`, the same helper instead emits the legacy
158+
heredoc step that embeds the prompt body and supplements directly into
159+
the YAML; `prompt.js` is not invoked.
160+
161+
Both download steps share the helper `scripts_download_step()` in
162+
`src/compile/extensions/mod.rs` so URL/version stay in lockstep.
163+
123164
## Adding a new internal use site
124165

125166
Suppose we want a `poll.js` bundle (e.g. for polling external systems):
@@ -132,17 +173,28 @@ Suppose we want a `poll.js` bundle (e.g. for polling external systems):
132173
```
133174
and extend `build` to also run it and copy `dist/poll/index.js` to
134175
`../poll.js`.
135-
3. Add tests under `src/poll/__tests__/`.
176+
3. Add tests under `src/poll/__tests__/` (unit) and `test/` (smoke,
177+
gated on `dist/poll/index.js` existing).
136178
4. Wire from a new `CompilerExtension` (or extend an existing one) that
137-
downloads and invokes `poll.js` as a runtime step.
138-
5. Update `.github/workflows/release.yml` if the zip exclusion list
179+
downloads and invokes `poll.js` as a runtime step. Use the shared
180+
`scripts_download_step()` and `node_tool_step()` helpers.
181+
5. If the contract is non-trivial, follow the `gate.js` /
182+
`prompt.js` pattern: define a `Spec` struct in Rust with
183+
`#[derive(Serialize, JsonSchema)]`, add a hidden
184+
`export-poll-schema` CLI, and extend `npm run codegen` to regenerate
185+
`types-poll.gen.ts`. For trivial contracts (a couple of env vars),
186+
hand-written types are fine.
187+
6. Update `.github/workflows/release.yml` if the zip exclusion list
139188
needs to include the new `dist/poll` directory.
140189

141190
## Bundle-size budget
142191

143-
Each bundled artifact must stay **under 5 MB**. The current `gate.js` is
144-
~1.1 MB, dominated by `azure-devops-node-api`. If a future bundle blows
145-
the budget:
192+
Each bundled artifact must stay **under 5 MB**. Current sizes:
193+
194+
- `gate.js` is ~1.1 MB, dominated by `azure-devops-node-api`.
195+
- `prompt.js` is ~5 KB (no SDK dependency).
196+
197+
If a future bundle blows the budget:
146198

147199
- First, check ncc's `--minify` and `--target` flags.
148200
- If still too large, weigh dropping the SDK in favor of hand-rolled

docs/extending.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,18 @@ Execution job (before the agent runs). `setup_steps()` injects into the Setup
5353
job (before the Execution job starts). Use `setup_steps()` for pre-activation
5454
gates or checks that must complete before the agent is launched.
5555

56+
**`prompt_supplement()` and runtime substitution**: the markdown returned
57+
from this method is appended to the agent prompt at runtime by the
58+
`prompt.js` ado-script bundle. It is rendered through the same
59+
substitution pipeline as the user's prompt body, so supplements may
60+
contain `${{ parameters.NAME }}` (for declared parameters) and
61+
`$(VAR)` / `$(VAR.SUB)` references that will be resolved at pipeline
62+
runtime. See `docs/template-markers.md` for the full substitution
63+
contract. When `inlined-imports: true`, the supplement is instead
64+
embedded into the YAML at compile time via `wrap_prompt_append` and ADO
65+
substitution rules apply (no parameters, no runtime variable
66+
substitution).
67+
5668
**Phase ordering**: Extensions are sorted by phase — runtimes
5769
(`ExtensionPhase::Runtime`) execute before tools (`ExtensionPhase::Tool`).
5870
This guarantees runtime install steps run before tool steps that may depend

docs/front-matter.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,7 @@ parameters: # optional ADO runtime parameters (surfaced in UI
135135
displayName: "Clear agent memory"
136136
type: boolean
137137
default: false
138+
inlined-imports: false # opt out of dynamic prompt injection (default: false)
138139
---
139140

140141

@@ -143,6 +144,33 @@ parameters: # optional ADO runtime parameters (surfaced in UI
143144
Build the project and run all tests...
144145
```
145146

147+
## Dynamic Prompt Injection (`inlined-imports`)
148+
149+
By default, the agent's prompt body is **not** embedded in the compiled
150+
pipeline YAML. Instead, the pipeline reads the source `.md` from the
151+
checked-out workspace at runtime, strips its front matter, applies
152+
variable substitution, and assembles the final prompt in a step backed
153+
by the `prompt.js` ado-script bundle. This means body-only edits to the
154+
agent's `.md` no longer require recompiling the pipeline.
155+
156+
Set `inlined-imports: true` to opt out and embed the prompt body and
157+
extension supplements directly into the YAML at compile time (legacy
158+
behaviour). Use the inlined form when:
159+
160+
- The Agent pool can't reach `github.com` to download `scripts.zip`.
161+
- You need a fully self-contained pipeline file (offline archival).
162+
- You want to inspect the exact rendered prompt by reading the YAML.
163+
164+
Substitution patterns recognised at runtime by `prompt.js` (default
165+
mode only):
166+
167+
| Pattern | Resolved via | Notes |
168+
|-------------------------------|------------------------------------------------------|---------------------------------------------------------------------------------------------|
169+
| `${{ parameters.NAME }}` | env `ADO_AW_PARAM_<NAME upper, hyphen→underscore>` | Only declared parameters substitute; others left verbatim with a warning. |
170+
| `$(VAR)` / `$(VAR.SUB)` | env `<name upper, dot→underscore>` (ADO native) | Unset variables left verbatim with a warning. Secrets are not auto-exposed and stay verbatim. |
171+
| `$[ ... ]` | not substituted | Left verbatim with one warning per render. |
172+
| `\$(...)` | escape | Backslash stripped; `$(...)` left literal. |
173+
146174
## Workspace Defaults
147175

148176
The `workspace:` field controls which directory the agent runs in. When it is

docs/template-markers.md

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -305,9 +305,29 @@ resources:
305305
- release/*
306306
```
307307

308-
## {{ agent_content }}
308+
## {{ prepare_agent_prompt }}
309309

310-
Should be replaced with the markdown body (agent instructions) extracted from the source markdown file, excluding the YAML front matter. This content provides the agent with its task description and guidelines.
310+
Replaced with the YAML step(s) that prepare `/tmp/awf-tools/agent-prompt.md` for the agent.
311+
312+
The expansion depends on the `inlined-imports` front-matter field:
313+
314+
- **`inlined-imports: false`** (default) — emits a three-step bundle:
315+
1. `NodeTool@0` to install Node 20.x.
316+
2. A `curl` download of `scripts.zip` from the matching `githubnext/ado-aw` release, extracting `prompt.js` to `/tmp/ado-aw-scripts/prompt.js`.
317+
3. A bash step that runs `node /tmp/ado-aw-scripts/prompt.js` with `ADO_AW_PROMPT_SPEC` (a base64-encoded `PromptSpec` JSON) and one `ADO_AW_PARAM_<NAME>: ${{ parameters.<NAME> }}` env entry per declared parameter. The renderer reads the source `.md` from the workspace, strips its front matter, applies variable substitution, appends extension supplements, and writes the result to `/tmp/awf-tools/agent-prompt.md`.
318+
319+
Substitution patterns recognised at runtime by `prompt.js`:
320+
321+
| Pattern | Resolved via | Notes |
322+
|-------------------------------|------------------------------------------------------|---------------------------------------------------------------------------------------------|
323+
| `${{ parameters.NAME }}` | env `ADO_AW_PARAM_<NAME upper, hyphen→underscore>` | Only declared parameters substitute; others left verbatim with a warning. |
324+
| `$(VAR)` / `$(VAR.SUB)` | env `<name upper, dot→underscore>` (ADO native) | Unset variables left verbatim with a warning. Secrets aren't auto-exposed and stay verbatim. |
325+
| `$[ ... ]` | not substituted | Left verbatim with one warning per render. |
326+
| `\$(...)` | escape | Backslash stripped; `$(...)` left literal. |
327+
328+
- **`inlined-imports: true`** — emits a single legacy heredoc step that writes the markdown body verbatim into `/tmp/awf-tools/agent-prompt.md`. Extension prompt supplements are emitted as separate `cat >>` heredoc steps via `wrap_prompt_append`. No variable substitution beyond what ADO macros already do natively.
329+
330+
This marker replaces the older `{{ agent_content }}` placeholder, which is no longer emitted by the compiler.
311331

312332
## {{ mcpg_config }}
313333

0 commit comments

Comments
 (0)