feat(control-plane): agent-mode steering verbs, events query resource, and --json lifecycle output#751
Open
AbirAbbas wants to merge 7 commits into
Open
feat(control-plane): agent-mode steering verbs, events query resource, and --json lifecycle output#751AbirAbbas wants to merge 7 commits into
AbirAbbas wants to merge 7 commits into
Conversation
…ecution ID Approvals could previously only be resolved through the HMAC-signed webhook (POST /api/v1/webhooks/approval-response), whose secret is held by the external approval service. Extract the resolution core (idempotency, state transitions, approval bookkeeping, events, agent callback) into webhookApprovalController.applyApprovalDecision and add POST /api/v1/executions/:execution_id/approval-response under the authenticated agentAPI group, so operators and CLIs holding an API key can approve/reject/request_changes directly. Webhook behavior is unchanged; 'expired' stays webhook-only since expiry is not an operator decision. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Add 'af agent exec pause|resume|cancel|restart|approval-status|approve'
as thin wrappers over the existing /api/v1/executions/:execution_id/*
endpoints, emitting the standard AgentResponse envelope with non-zero
exit on error. approve wires --decision approved|rejected|
request_changes [--reason] to the new authenticated approval-response
endpoint.
proxyToServer now normalizes legacy string errors ({"error":"..."},
optionally with a sibling message) into the structured envelope error
object {code,message,hint}, deriving the code from the HTTP status when
the handler didn't provide one — so agents scripting against 'af agent'
always get {ok:false,error:{...}} on failures.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Expose persisted execution lifecycle events (workflow_execution_events) through POST /api/v1/agentic/query as resource=events so agents can poll event history from a shell loop instead of consuming SSE. Queries require filters.execution_id (direct per-execution listing) or filters.run_id (fan-out over the run's execution records); results are sorted by emitted_at ascending with sequence tie-breaking, honor since/until RFC3339 bounds, and paginate with limit/offset (total is the pre-pagination count). No new persistence layer or storage interface methods — this composes the existing QueryExecutionRecords and ListWorkflowExecutionEvents queries. CLI: 'af agent query -r events --execution-id X | --run-id Y' with a new --execution-id filter flag; help documents that this is a pollable snapshot of the SSE stream, not a live subscription. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Add a --json flag to af list, af install, af run, af stop, and af logs
that emits the agent-mode {ok,data,error:{code,message,hint}} envelope
on stdout with non-zero exit on error. Default human output is
unchanged.
- list: {nodes:[{name,version,status,port,description}], total}
- install/run: envelope on the real stdout; service-layer progress
prints are redirected to stderr for the duration
- stop: {node, status: stopped|not_running}; progress output is
suppressed via a Quiet flag on AgentNodeStopper (printf wrapper only,
no changes to the signal/liveness logic)
- logs: {node, log_path, lines:[...]} honoring --tail via a Go-native
tail (no external tail binary); --follow with --json is rejected as
invalid_flags since a stream cannot be a JSON snapshot
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The raw INSERT in storeWorkflowExecutionEventTx omitted recorded_at (the GORM model's autoCreateTime does not apply to raw SQL), leaving NULL in SQLite/Postgres. ListWorkflowExecutionEvents scanned the column into a non-nullable time.Time, so listing any stored event failed with 'unsupported Scan, storing driver.Value type <nil>'. Nothing exercised this listing path end-to-end until the agentic events query resource; the existing storage test masked the bug with a manual UPDATE of recorded_at before listing. Write recorded_at in the INSERT and scan it through sql.NullTime, falling back to emitted_at for legacy NULL rows so old databases keep listing cleanly. Found via runtime verification of 'af agent query -r events' against a local control plane. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
structuredErrorFromString and defaultCodeForStatus were exercised only through subprocess exit-path tests, which record no coverage. Add direct table-driven tests so the patch-coverage gate sees these lines. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Register POST /api/v1/executions/:execution_id/approval-response in the agentic API catalog so 'af agent discover' surfaces the new resolution endpoint alongside the other approval routes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Contributor
📊 Coverage gateThresholds from
✅ Gate passedNo surface regressed past the allowed threshold and the aggregate stayed above the floor. |
Contributor
📐 Patch coverage gateThreshold: 80% on lines this PR touches vs
✅ Patch gate passedEvery surface whose lines were touched by this PR has patch coverage at or above the threshold. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes the gaps that stop an AI agent (e.g. Claude Code) from fully driving AgentField through the CLI: first-class steering verbs in agent mode, a pollable events resource, and JSON envelopes for the node-lifecycle commands.
What's in here
af agent exec pause|resume|cancel|restart|approval-status|approve --id <execution_id>— thin wrappers over the existing/api/v1/executions/:id/*endpoints, emitting exactly one{ok, data|error{code,message,hint}, meta}envelope on stdout, exit 0 on 2xx / 1 otherwise.POST /api/v1/executions/:execution_id/approval-response({decision, reason?, response?}) under normal API auth, sharing the exact resolution logic with the HMAC webhook via an extractedapplyApprovalDecision(same state transitions, idempotency, events, callbacks). Listed in the discover catalog.af agent query -r events --execution-id X | --run-id Y [--since --until --limit --offset]— exposes the already-persistedworkflow_execution_events, time-sorted, no new persistence layer. Requires an execution or run filter (400missing_filterotherwise).--jsonforaf list / install / run / stop / logs— same envelope shape; default human output is byte-identical to before.logs --jsonreturns{node, log_path, lines}honoring--tail;--json --followis rejected.cfac3b45): the raw INSERT forworkflow_execution_eventsomittedrecorded_at, so every stored event row had NULL andListWorkflowExecutionEventsfailed on scan — in both SQLite and Postgres modes (an existing test masked it with a manual UPDATE). Without this fix the events resource 500s on every query; it also fixes the latent bug for any other consumer.Validation contract
execverb: valid id → envelope withok:true+ status data; nonexistent id →{ok:false, error:{code:"not_found", …}}+ exit 1; invalid state →invalid_state(normalized from the server's string errors); missing/invalid flags →missing_required_flag/invalid_flag_valuewithout a server round-trip.already_processed; no approval request → 404no_approval_request;expiredis deliberately not accepted as an operator decision (webhook-only).emitted_atasc (tie: sequence);since/untilare RFC3339 bounds;totalreflects the pre-pagination count; futuresince→ empty list, not an error.--json: stdout is pure JSON (service progress prints redirected to stderr); non-zero exit on error.Table-driven tests derive from this contract (httptest fake servers for the CLI, in-process tests for error normalization, handler tests for the events query).
Runtime verification
Exercised end-to-end against a local server (isolated SQLite/BoltDB): create execution → request-approval →
exec approval-status(pending) →exec approve(processed, running) → idempotent re-approve → pause/resume/cancel →query -r eventsreturned the full 5-event lifecycle time-sorted; run-id fan-out, limit/offset, error paths, and pure-JSON stdout all verified.Notes for review
TestDevServiceRunDevfails on the dev machine only (a live local agent inside the port-scan range interferes); it fails identically at base6b8b404eand this diff touches nothing in that package — CI should be green.stop.gochanges are output-layer only; feat(desktop): Electron desktop app skeleton + Windows enablement groundwork #752 refactors the signal/liveness code in the same file into build-tagged helpers — whichever merges second needs a trivial rebase.install/run --jsonredirect service stdout to stderr via anos.Stdoutswap; flagged in case a cleaner output-formatter seam is wanted later.🤖 Generated with Claude Code