Skip to content

Emit AWF agentTimeout for literal timeouts and show delivered steering notices in audit/logs - #67597

Open
SivaKesava1 with Copilot wants to merge 4 commits into
mainfrom
copilot/make-awf-budget-steering-work
Open

SivaKesava1 with Copilot wants to merge 4 commits into
mainfrom
copilot/make-awf-budget-steering-work

Conversation

Copilot AI commented Oct 11, 2026 •

Copy link
Copy Markdown
Contributor

AWF (gh-aw-firewall#9790) can warn the agent at 80/90/95/99% of its AI-credit budget, token budget or runtime deadline. In gh-aw, the runtime warnings never fire on the default Docker runtime, because only Cloud Hypervisor and NVX get container.agentTimeout. Also, the per-request steering record AWF writes is dropped before it reaches the unified session, so gh aw audit and gh aw logs can't show which warnings were delivered.

Note

These changes were not committed or pushed when the session ended. The last refactor (helpers split out of the pkg/cli session reader) builds, but its tests and the full pre-commit check have not been re-run. parallel_validation has not been run.

Important

The new behaviour is gated on AWF v0.28.51, which is not published yet. It is the first version after v0.28.50, as the issue suggested. The default AWF version is still v0.28.50, so recompiling this repo's workflows produces no lock-file changes.

Compiler: container.agentTimeout

  • New AWFAgentTimeoutSteeringMinVersion constant and awfSupportsAgentTimeoutSteering check, following the existing AWF feature-flag pattern.
  • resolveAWFAgentTimeoutMinutes:
    • Cloud Hypervisor / NVX: unchanged.
    • AWF older than v0.28.51: no timeout is sent, as before.
    • Otherwise: the resolved timeout-minutes value. It is omitted (with a debug log) when the step timeout isn't a literal, and raised to the step timeout if it would be shorter. AWF counts its deadline from agent start, so the GitHub step timeout fires first and AWF's exit 124 can't cut off the agent's final output.
  • Threat detection gets its own detection job timeout (default 10, or jobs.detection.timeout-minutes).
  • Differs from the issue's "omitted → 20" case: compiled workflows without timeout-minutes get an expression (${{ fromJSON(vars.GH_AW_DEFAULT_TIMEOUT_MINUTES || '20') }}), so they get no agentTimeout. In practice, runtime warnings need a literal timeout-minutes. This is documented in sandbox.md.

Unified session

  • firewall.token_usage (and its usage.report alias) now keeps steering, projected to {type, threshold}. Notices with an unknown type, an unknown threshold or a malformed value are dropped.
  • ai_credit_steering event-log lines now map to firewall.steering instead of the generic firewall.event. firewall.steering keeps threshold.
  • Schema regenerated from types/unified_session.d.ts. New SteeringNotice type (enumerated type and threshold) and FirewallSteeringData type.
  • Spec bumped to 1.8.0. New requirement T-UAS-071: producers keep the field; consumers treat a missing field as "no notice recorded", not "no notice delivered".

Audit and logs

  • The existing unified-session reader now returns steering notices from both agent- and detection-phase firewall.token_usage events. A malformed detection event is skipped, not treated as fatal. It does not read token-usage.jsonl directly.
  • New steering_notices list in gh aw audit (JSON and console output) and in gh aw logs --json. The run-level copy survives compact output.
  • gateway_steering_events now includes AI-credit warnings and keeps threshold and request_id. In the timeline these show as e.g. credit 90%.
  • schemas/audit.schema.json and schemas/logs.schema.json are updated to match. Runs without steering serialize exactly as before.

Example audit and logs JSON output (phase is agent or detection):

"steering_notices": [
  {"type": "ai_credit", "threshold": 80, "request_id": "d00ff6a1-…", "phase": "agent"},
  {"type": "timeout",   "threshold": 90, "request_id": "6509695f-…", "phase": "agent"}
]

Tests and fixtures

  • Compiler tests cover:
    • literal, omitted, expression and old-version timeouts;
    • the step-timeout floor;
    • the detection run;
    • a full compile.
  • JS tests cover steering projection, invalid values, the collector and schema validation.
  • Go tests cover steering notices from session files, audit/logs output, console rendering, event-log AI-credit and threshold handling, and the timeline status.
  • New synthetic golden case claude-awf-steering-notices (copy of claude-awf-selected-messages with steering fields added by hand). Existing expected*.json files are unchanged; logs_expected.json changes only because it now includes the new case.

Known gaps

  • If a request carrying a notice never produced a usage record (for example a failed upstream call), the notice won't appear. This is accepted in the issue.
  • I haven't compared any failing checks against main.

Co-authored-by: SivaKesava1 <11771739+SivaKesava1@users.noreply.github.com>
Copilot AI changed the title [WIP] Make AWF budget and timeout steering functional in gh-aw runs Emit AWF agentTimeout for literal timeouts and show delivered steering notices in audit/logs Oct 11, 2026
Copilot AI requested a review from SivaKesava1 October 11, 2026 04:00
@SivaKesava1
SivaKesava1 marked this pull request as ready for review October 11, 2026 04:31
Copilot AI balanced review requested due to automatic review settings October 11, 2026 04:31
@github-actions

github-actions Bot commented Oct 11, 2026 •

Copy link
Copy Markdown
Contributor

✅ Test Quality Sentinel completed test quality analysis.

Test Quality Sentinel skipped because pre-fetch PR data was unavailable: unable to fetch test file diff

🧪 Test quality analysis by Test Quality Sentinel

@github-actions

github-actions Bot commented Oct 11, 2026 •

Copy link
Copy Markdown
Contributor

✅ PR Code Quality Reviewer completed the code quality review.

🔎 Code quality review by PR Code Quality Reviewer

@github-actions

github-actions Bot commented Oct 11, 2026 •

Copy link
Copy Markdown
Contributor

🧠 Matt Pocock Skills Reviewer has completed the skills-based review. ✅

🧠 Reviewed using Matt Pocock's skills by Matt Pocock Skills Reviewer

@github-actions

github-actions Bot commented Oct 11, 2026 •

Copy link
Copy Markdown
Contributor

✅ Design Decision Gate 🏗️ completed the design decision gate check. See the comment below for the result and any generated ADR draft.

🏗️ ADR gate enforced by Design Decision Gate 🏗️

@github-actions

github-actions Bot commented Oct 11, 2026 •

Copy link
Copy Markdown
Contributor

✅ Ponytail Reviewer completed successfully!

Lean already. Ship.

Generated by Ponytail Reviewer for #67597

@github-actions

Copy link
Copy Markdown
Contributor

🏗️ Design Decision Gate — ADR Required

This PR makes significant changes to core business logic (1071 new lines across pkg/, actions/, and schema files) but does not have a linked Architecture Decision Record (ADR).

📄 Draft ADR committed: docs/adr/67597-version-gated-awf-agent-timeout-and-steering-notices.md — review and complete it before merging.

🔒 This PR cannot merge until an ADR is linked in the PR body.

🔍 Decision inferred from the diff
  • Decision: Emit container.agentTimeout on all runtimes behind an AWF version gate (AWFAgentTimeoutSteeringMinVersion, v0.28.51) and promote AWF steering notices to a first-class, schema-described field of the unified session surfaced in gh aw audit / gh aw logs --json.
  • Driver: Runtime budget warnings never fire on the default Docker runtime (pkg/workflow/awf_config_build.go), and the per-request steering record is dropped by actions/setup/js/unified_session_payload.cjs.
  • Alternatives: always send agentTimeout (including a guessed value for expression timeouts); read steering directly from token-usage.jsonl in the CLI; keep the Cloud Hypervisor/NVX restriction and document the gap.
  • Key trade-off: the feature is inert for the compiled default (expression-valued timeout-minutes) and depends on an unreleased AWF version.
📋 What to do next
  1. Review the draft ADR committed to your branch — it was generated from the PR diff
  2. Complete the missing sections — add context the AI couldn't infer, refine the decision rationale, and list real alternatives you considered
  3. Commit the finalized ADR to docs/adr/ on your branch
  4. Reference the ADR in this PR body by adding a line such as:

    ADR: ADR-67597: Version-Gated AWF agentTimeout and First-Class Steering Notices

Once an ADR is linked in the PR body, this gate will re-run and verify the implementation matches the decision.

❓ Why ADRs Matter

"AI made me procrastinate on key design decisions. Because refactoring was cheap, I could always say 'I'll deal with this later.' Deferring decisions corroded my ability to think clearly."

ADRs create a searchable, permanent record of why the codebase looks the way it does. Future contributors (and your future self) will thank you.

📋 Michael Nygard ADR Format Reference

An ADR must contain these four sections to be considered complete:

  • Context — What is the problem? What forces are at play?
  • Decision — What did you decide? Why?
  • Alternatives Considered — What else could have been done?
  • Consequences — What are the trade-offs (positive and negative)?

All ADRs are stored in docs/adr/ as Markdown files numbered by PR number.

🏗️ ADR gate enforced by Design Decision Gate 🏗️ · pi · opus50 · 45.4 AIC · ⌖ 51.1 AIC · ⊞ 1.8K · ◷
Comment /review to run again

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Changes recommended

The unreleased AWF gate is bypassed by latest, and fractional thresholds can be misreported as valid notices.

3 open findings
What changed in this PR

Adds AWF runtime steering deadlines and preserves delivered steering notices through unified sessions, audit, and logs.

Changes:

  • Emits version-gated container.agentTimeout.
  • Projects steering notices into unified-session and CLI reports.
  • Adds schemas, documentation, and comprehensive fixtures/tests.
File Description
schemas/​logs.schema.json Adds steering notice fields.
schemas/​logs-jsonl.schema.json Updates JSONL report schema.
schemas/​audit.schema.json Updates audit schema.
pkg/​workflow/​awf_feature_flags.go Adds the AWF capability gate.
pkg/​workflow/​awf_config_build.go Resolves container agent timeouts.
pkg/​workflow/​awf_agent_timeout_test.go Tests timeout compilation.
pkg/​constants/​version_constants.go Defines the minimum AWF version.
pkg/​cli/​token_usage_types.go Adds steering data types.
pkg/​cli/​token_usage_subagent_session.go Collects notices from sessions.
pkg/​cli/​token_usage_steering.go Processes delivered and gateway notices.
pkg/​cli/​token_usage_steering_notices_test.go Tests reporting and rendering.
pkg/​cli/​token_usage_analyze.go Applies notice collection.
pkg/​cli/​testdata/​model_routing_golden/​README.md Documents the synthetic fixture.
pkg/​cli/​testdata/​model_routing_golden/​logs_expected.json Updates aggregate expectations.
pkg/​cli/​testdata/​model_routing_golden/​claude-awf-steering-notices/​usage/​aw_session.jsonl Adds unified-session fixture data.
pkg/​cli/​testdata/​model_routing_golden/​claude-awf-steering-notices/​usage/​agent_usage.json Adds usage fixture data.
pkg/​cli/​testdata/​model_routing_golden/​claude-awf-steering-notices/​sandbox/​firewall/​logs/​api-proxy-logs/​token-usage.jsonl Adds raw steering records.
pkg/​cli/​testdata/​model_routing_golden/​claude-awf-steering-notices/​sandbox/​firewall/​logs/​api-proxy-logs/​model-routing.jsonl Adds routing fixture data.
pkg/​cli/​testdata/​model_routing_golden/​claude-awf-steering-notices/​expected.legacy.json Adds legacy expectations.
pkg/​cli/​testdata/​model_routing_golden/​claude-awf-steering-notices/​expected.json Adds unified expectations.
pkg/​cli/​testdata/​model_routing_golden/​claude-awf-steering-notices/​aw_info.json Adds workflow metadata fixture.
pkg/​cli/​testdata/​model_routing_golden/​claude-awf-steering-notices/​agent/​awf-routing-outcome.json Adds routing outcome fixture.
pkg/​cli/​testdata/​model_routing_golden/​claude-awf-steering-notices/​agent/​aw_info.json Adds agent metadata fixture.
pkg/​cli/​testdata/​model_routing_golden/​claude-awf-steering-notices/​agent-session.jsonl Adds agent session fixture.
pkg/​cli/​testdata/​model_routing_golden/​claude-awf-steering-notices/​agent_usage.json Adds legacy usage fixture.
pkg/​cli/​model_routing_golden_test.go Registers and verifies the fixture.
pkg/​cli/​logs_report.go Exposes notices in logs output.
pkg/​cli/​logs_models.go Extends report models.
pkg/​cli/​gateway_logs_timeline.go Displays AI-credit thresholds.
pkg/​cli/​audit_report.go Exposes notices in audits.
pkg/​cli/​audit_report_render.go Renders notices in the console.
docs/​src/​content/​docs/​specs/​unified-agent-session-specification.md Defines the steering contract.
docs/​src/​content/​docs/​reference/​sandbox.md Documents timeout steering.
docs/​src/​content/​docs/​reference/​artifacts.md Documents report fields.
docs/​public/​schemas/​unified-session.schema.json Extends the session schema.
actions/​setup/​js/​unified_session.test.cjs Tests end-to-end projection.
actions/​setup/​js/​unified_session.cjs Maps AI-credit events.
actions/​setup/​js/​unified_session_render.cjs Renders steering thresholds.
actions/​setup/​js/​unified_session_payload.test.cjs Tests notice validation.
actions/​setup/​js/​unified_session_payload.cjs Projects validated notices.
actions/​setup/​js/​types/​unified_session.d.ts Adds steering session types.

🧠 Review effort: Balanced

Comment on lines +98 to +104
var value float64
if err := json.Unmarshal(data, &value); err != nil || value <= 0 || value > 100 {
*t = 0
return nil
}
*t = steeringThreshold(math.Round(value))
return nil
Comment on lines +58 to +59
func awfSupportsAgentTimeoutSteering(firewallConfig *FirewallConfig) bool {
return awfVersionAtLeast(firewallConfig, constants.AWFAgentTimeoutSteeringMinVersion)
Comment on lines +235 to +237
With steering enabled, AWF adds a notice to the next model request when the run reaches 80, 90, 95 or 99% of its AI-credit budget, its effective-token budget or its runtime deadline. Runtime notices need an agent deadline: with AWF v0.28.51 or newer, gh-aw sends `container.agentTimeout` on every runtime when `timeout-minutes` is a literal number or omitted at compile time (default 20, or the `GH_AW_DEFAULT_TIMEOUT_MINUTES` compile-time override). When `timeout-minutes` is a GitHub Actions expression, including the default emitted into compiled workflows that read `vars.GH_AW_DEFAULT_TIMEOUT_MINUTES`, the deadline is omitted and AWF sends no runtime notices. Older AWF versions receive the deadline only on the Cloud Hypervisor and NVX runtimes, as before. The threat-detection run uses its own job timeout.

AWF also stops the agent at this deadline (exit code 124). AWF counts it from agent start, after the step has started, and gh-aw never sends a deadline shorter than the agent step timeout, so the GitHub Actions step timeout normally fires first and runtime notices arrive slightly after the step's own deadline. Delivered notices are listed in `gh aw audit` and `gh aw logs --json` (see [Artifacts](/gh-aw/reference/artifacts/)).
@SivaKesava1

Copy link
Copy Markdown
Collaborator

Checked 64afd9e (the PR note says the final refactor was committed without re-running its tests):

  • Ran on Linux: go build ./... and go vet pass; the new pkg/workflow agent-timeout tests pass; the unified-session JS tests pass (182); the pkg/cli steering, golden and schema tests pass. Twelve pkg/cli tests fail, but the same twelve fail on main (a1376acf2e) in the same environment, so this PR adds no failures. Please still run the full pre-commit check (make agent-finish) on the final code, and compare any failing CI check with main before fixing it.

Blocking: time warnings stay off for most workflows. Workflows without a literal timeout-minutes, which is the default, compile to timeout-minutes: ${{ fromJSON(vars.GH_AW_DEFAULT_TIMEOUT_MINUTES || '20') }}. With this PR they get no agentTimeout, so the default workflow still never gets a runtime warning. That is the main case #67596 is about. The value is available at run time: the agent step already has GH_AW_ENGINE_STEP_TIMEOUT_MINUTES (and GH_AW_TIMEOUT_MINUTES) set from that expression (for example in copilot-centralization-drilldown.lock.yml), and AWF accepts --agent-timeout <minutes> on its command line (gh-aw-firewall src/cli-options.ts). Please pass the runtime value to awf for expression timeouts (same AWF version gate, same step-timeout floor), keep the literal-timeout path, and update sandbox.md. Test: a workflow without timeout-minutes compiles to an awf invocation that passes the runtime step timeout, gated on AWF v0.28.51; with an older AWF it passes nothing.

Follow-up (no change needed now): the new golden case claude-awf-steering-notices is synthetic. Once an AWF release includes gh-aw-firewall#9790, we'll capture a real steering run with scripts/model-routing-golden.py and replace it.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Review verdict

Non-blocking overall, but I found two correctness holes in the new steering-notice path.

Details
  1. The Go-side threshold decoder rounds arbitrary numeric values into valid warning buckets, which can turn malformed telemetry into a fake 80/90/95/99 steering notice.
  2. applySteeringNotices silently drops unified-session read failures, so a parse error is reported the same way as “no notices were recorded”.

Those are both worth tightening before people start depending on steering_notices for audit/logs accuracy.

🔎 Code quality review by PR Code Quality Reviewer · copilot · gpt54 · 116 AIC · ⌖ 5.9 AIC · ⊞ 21.2K
Comment /review to run again

*t = 0
return nil
}
*t = steeringThreshold(math.Round(value))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Rounding arbitrary threshold values into one of the allowed 80/90/95/99 buckets manufactures steering notices that the unified-session schema is supposed to reject, so malformed telemetry can be reported as a real budget warning.

💡 Why this matters and how to fix it

94.6 currently becomes 95, which then passes valid() and shows up in steering_notices / gateway_steering_events even though the JS normalizer drops anything except the exact enum values. That gives Go and JS two different interpretations of the same corrupted record.

Prefer an exact integer decode instead of math.Round, so malformed thresholds stay invalid instead of being silently rewritten into a supported warning.

return
}
_, notices, err := readUnifiedTokenUsage(runDir)
if err != nil {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Dropping read errors here makes a broken aw_session.jsonl look exactly like "no steering notices were recorded", which gives the new steering_notices field a false negative meaning.

💡 Why this matters and how to fix it

The review output now treats a missing steering_notices list as a real signal, but this branch silently erases the list for any parse failure (oversized line, malformed unrelated event, unreadable file, etc.). That means gh aw audit / gh aw logs can report the absence of notices when we actually failed to read them.

Please surface this as a warning/error on the summary instead of returning quietly, or plumb the notices out of the earlier parse path so the only outcomes are "present", "empty", or a visible read failure.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Skills-Based Review 🧠

Applied /tdd and /codebase-design (pr-triage: new_feature). This is a well-scoped, thoroughly tested change: compiled-config, unified-session JS, Go reader, audit/logs rendering, and spec/schema are all updated together with matching golden fixtures.

📋 Verification performed
  • go build ./... — passes
  • go test ./pkg/workflow/... -run AgentTimeout -v — all new compiler tests pass (literal/omitted/expression/unsupported-version cases, step-timeout floor, detection job timeout)
  • go test ./pkg/cli/... -run 'Steering|ModelRoutingGolden' -v — all new steering-notice, audit/logs projection, and golden fixture tests pass (including the new claude-awf-steering-notices case)
  • go vet ./pkg/workflow/... ./pkg/cli/... — clean
  • JS vitest suite could not be run in this sandbox (npm registry blocked by self-signed cert / no node_modules), so unified_session.test.cjs / unified_session_payload.test.cjs changes were reviewed by inspection only — they look correct and symmetric with the Go-side handling (lenient decode, strict allow-list of type/threshold).
📋 Key Themes & Highlights

Positive Highlights

  • ✅ resolveAWFAgentTimeoutMinutes cleanly reuses the existing literalStepTimeoutMinutes/resolveStepTimeoutValue helpers rather than duplicating timeout-string parsing logic — consistent with the existing codebase pattern (codebase-design).
  • ✅ Steering decoding is defensively lenient end-to-end: TokenUsageSteering.UnmarshalJSON, steeringThreshold.UnmarshalJSON, and the JS STEERING_TYPES/STEERING_THRESHOLDS allow-lists all independently guard against malformed/future values without failing the surrounding record — good belt-and-suspenders symmetry between the Go and JS consumers.
  • ✅ New behavior is properly version-gated (AWFAgentTimeoutSteeringMinVersion) following the existing awfSupports* feature-flag convention, and the "unsupported version" / "default version" paths are both covered by tests.
  • ✅ Test coverage is excellent and written specification-first: awf_agent_timeout_test.go covers presence/absence across literal, expression, and runtime-variable-default timeouts, plus an end-to-end compiled-lockfile assertion (TestCompileWorkflow_AgentTimeoutDockerRuntime) that few PRs bother to add.
  • ✅ Detection-phase steering notices are collected without letting a malformed detection event fail agent token-usage parsing (token_usage_subagent_session.go), and this exact behavior is asserted in TestAnalyzeTokenUsageSteeringNotices.
  • ✅ Spec/schema/doc updates (unified-session spec v1.8.0, T-UAS-071, schemas/audit.schema.json, sandbox.md, artifacts.md) are all kept in lock-step with the code change — exactly the kind of documentation-model consistency /grill-with-docs looks for.

Minor Observations (non-blocking)

  • resolveAWFContainerAgentTimeoutMinutes's fallback path silently returns the default timeout when rawTimeout parses to a non-positive integer (e.g. "0" or "-5") without logging, unlike the non-numeric branch which logs its reasoning. Low impact since such values shouldn't reach this point after frontmatter validation, but worth a one-line debug log for symmetry if this function is touched again.
  • The PR description itself flags that parallel_validation hasn't been run and the full pre-commit check/tests weren't re-run before this session ended — worth confirming CI is green given the scope of schema regeneration involved (hand-verifying generated JSON Schema from .d.ts is error-prone).

No blocking issues found; approving.

🧠 Reviewed using Matt Pocock's skills by Matt Pocock Skills Reviewer · copilot · sonnet50 · 159.6 AIC · ⌖ 14.8 AIC · ⊞ 10.3K
Comment /matt to run again

Co-authored-by: SivaKesava1 <11771739+SivaKesava1@users.noreply.github.com>

This branch has not been deployed

No deployments
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.

Make AWF budget and timeout steering work in gh-aw runs, and show delivered notices in audit

3 participants