Skip to content

Enforce agent-state UX for app→agent features - #21

Open
ahmad-ajmal wants to merge 1 commit into
mainfrom
ahmad/enforce-agent-state
Open

ahmad-ajmal wants to merge 1 commit into
mainfrom
ahmad/enforce-agent-state

Conversation

@ahmad-ajmal

@ahmad-ajmal ahmad-ajmal commented Oct 2, 2026 •

Copy link
Copy Markdown
Collaborator

Closes #13.

Work an app queues for an agent now stays visible in the View until it is done. validate enforces it, walk-verify checks it, and every blueprint with a queue and a View ships a component that does it, so the default path is the good one. Getting a real agent to complete a task on Windows also needed three bridge fixes, included here.

What it looks like

The react-node starter, with "Ask an agent" on each task. The badge beside the title and the panel under the row follow the task live, with no reload.

Waiting. Shown the moment the work is queued, with elapsed time.

01-waiting

No agent listening. After ~20 s unclaimed, it says so and names the command that starts one, instead of "waiting" forever.

02-no-agent-listening

Working. The agent's own step, its progress, and how long it has been running.

03-working

Done. The answer gets its own full-width block, never the title cell. Line breaks are kept, links are clickable, and long text is clamped behind "Show more".

04-done 05-done-expanded

Failed. The reason in words, and "Ask again", which queues new work rather than returning the old failure. Codes the queue and the bridge write themselves (redelivery_exhausted, harness_exited_nonzero, …) are translated into a sentence.

06-failed

Phone width. The row badge steps aside so the title keeps its room. The panel carries the state.

07-phone

Real agents, through agent-app <dir> bridge start. Claude Code (first) and Pi (second) each picked up tasks queued from the UI, did the triage through the a2app CLI, and completed with summaries the View showed as they arrived.

08-real-run-claude-code 09-real-run-pi

What changed

1. Validate gate: new step, "agent work shown in the View" (framework/cli/src/lib/agentState.ts).

  • What it scans for: a static, stack-agnostic scan finds trigger(…) calls in app-owned code that name a capability. It recognises the JS, Go, Ruby, Python (capability=) and Rust (Some(…)) spellings, plus the adapter's object form.
  • When it fails: when no View code renders the agent-task component or reads /api/_a2app/tasks/. The failure names each call site and points at the creator-skill rule.
  • What it ignores: comments, strings, definitions of trigger, system-owned files, dependencies, build output and tests. The component's own file doesn't count, so shipping it is not using it.
  • Apps with no human View (python-fastapi) get no verdict.

A View that queues work and only shows a toast now fails the gate like this:

✗ agent work shown in the View (app→agent triggers) failed
This app queues work for an agent, but no View code shows that work:
  a2app.schema.mjs:145 queues work for capability "triage"

An agent run takes seconds to minutes. A control that queues one and then goes quiet looks
broken. Keep the task id the trigger returns on the record, and render it in the View with the
blueprint's AgentTask component (see reference/blueprint.md), or follow
GET /api/_a2app/tasks/{id} yourself and show submitted (with elapsed time, and a "no agent is
listening" hint after ~20 s), working (progress.step), completed (result.summary in its own
full-width block) and failed (reason, and a way to ask again). …
See the creator skill, "The UI that queues work must show that work until it is done".

2. UI kit: an agent-task panel, badge and useAgentTask hook.

  • Files: AgentTask.jsx for react-node, go-react and rust-react; Vue components plus agentTask.js for rails-vue.
  • Polling: one poller per task, shared by badge and panel, only while unfinished, and slower when unclaimed, rate-limited or the tab is hidden.
  • Fallbacks:
    • record-written progress for multi-user apps, where the task read answers 401
    • the run's printed output when the bridge closed a task without a summary

3. Starters (react-node, go-react, rust-react, rails-vue, python-fastapi):

  • Ask an agent: an "Ask an agent" button, and an agentTask field set by request-triage.
  • Re-asks: the previous task id goes in the payload. Identical triggers dedupe to one task even after it finished, so a retry would otherwise get the old failure back.

4. Skills:

  • creator states the result-rendering rule (own full-width block, never the title cell) and the gate.
  • walk-verify gets check 6c, which drives every state with the verifier playing the agent from the CLI.
  • modify points at both.

5. Bridge (framework/cli/src/lib/bridge.ts, harness.ts). Needed for a real agent to complete a task on Windows:

  • Starting the harness: npm .cmd shims and one-line node wrappers (Pi's pi.cmd) are started by running their target directly, never through a shell. Before this, every delivery failed with spawn EINVAL and left the task claimed until it ran out of redeliveries.
    • Any other batch file is refused before a task is claimed.
    • A synchronous spawn failure now fails the task at once.
  • Claude Code permission: headless Claude Code may run only the a2app CLI, with file edits denied. Without that grant, every a2app call needed an approval nobody could give.
  • Heartbeat: it keeps the claim without overwriting the agent's own step.

6. rails-vue: ships ui/public/tokens.css and ui.css. The template's unanchored public/ ignore rule had kept them out of the repo, so scaffolded apps were unstyled.

How it was verified

  • Unit tests:
    • framework/cli/test/agent-state.test.mjs covers every stack spelling, the false-positive cases, the View verdicts, and all five starters passing their own gate.
    • bridge.test.mjs covers shim parsing, delivery through an npm-style shim with a payload of cmd.exe metacharacters (arrives verbatim, nothing executed), a non-shim .bat refused before any claim, a spawn that throws, and heartbeats that carry no step.
  • CI: build, typecheck, test and conformance all pass.
  • Gate as a builder hits it: rewrote a starter View to queue work and only toast. validate failed with the output above, and passed once the panel was rendered.
  • Live:
    • react-node: every state driven in a browser, phone width included.
    • Vue View: the same flow, served against the react-node backend.
    • Go: runner exercised over HTTP.
    • Rust: builds and passes its self-test.
    • Python: runner called directly.
    • Real agents: Claude Code and Pi (both its plugin route and a direct node pi-launcher.js route) completed real tasks through the bridge.

Not covered

  • Ruby: the Ruby starter's runner (lib/a2app_schema.rb) has not been executed (no Ruby on the test machine).
  • Go and Rust Views: not driven in a browser on their own servers. They are the same App.jsx/AgentTask.jsx as react-node.
  • Other harnesses: Codex, Gemini and Aider are untested. Their built-in profiles pass no permission flags and may hit the same headless approval wall Claude Code did.
  • Gate scope: the check is app-wide. If one feature already shows its work, a second that only toasts still passes. walk-verify 6c covers each feature.
  • PocketBase: pocketbase-react has no agent queue yet. That is Add the app→agent queue (operations, tasks, events) to blueprint-pocketbase-react #22.

@ahmad-ajmal ahmad-ajmal self-assigned this Oct 2, 2026
@ahmad-ajmal
ahmad-ajmal marked this pull request as ready for review October 2, 2026 16:08
@ahmad-ajmal
ahmad-ajmal requested a review from zfoong October 2, 2026 16:08
@ahmad-ajmal
ahmad-ajmal added this pull request to stack #26 October 5, 2026 13:20
@ahmad-ajmal
ahmad-ajmal force-pushed the ahmad/enforce-agent-state branch from e02e0a5 to 05c88a8 Compare October 5, 2026 15:22
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.

Enforce agent-state UX for app→agent features (live task status, readable results)

1 participant