Skip to content

feat(cli): add minimal trigger surface — af call, af ls, af tail #576

@santoshkumarradha

Description

@santoshkumarradha

Summary

Add a minimal, human-facing trigger surface to the af CLI so developers and scripts can invoke reasoners and watch workflows directly from the terminal. Today the server exposes POST /api/v1/execute/:target, /reasoners/:id, and /skills/:id, but no CLI command wraps them — the only way to call a reasoner from the shell is af agent batch with a hand-written JSON envelope.

This issue specifies the complete scope. Implementation must not exceed it.

Scope: 3 new verbs

af call <node>.<reasoner>      # trigger a reasoner
af ls   [query]                # discover reasoners (recency-first, fuzzy filter)
af tail <run_id>               # attach to a running execution stream

No other verbs. No af find, af show, af history, af repeat, af ctx, af workflow, af cancel. Existing af execution cancel|pause|resume covers run control.

Non-goals (explicit)

  • No client-side state files, history database, or contexts file
  • No per-user metrics (no "5x today" call counts)
  • No TUI / interactive browser
  • No pipe-chaining or batch DSL (af agent batch already covers programmatic batching)
  • No editor-based input authoring in v1 (--edit flag deferred)
  • No new env vars (reuse existing AGENTFIELD_SERVER, AGENTFIELD_API_KEY)

af call <node>.<reasoner>

Triggers a reasoner. TTY-aware: prompts humans, runs silently for scripts.

Flags

Flag Purpose
--in <json|@path> Input payload — inline JSON string, or @path/to/file.{json,yaml} (curl convention, format auto-detected from extension)
--schema Print the reasoner's input schema and exit; do not execute
--async Return run_id immediately and exit; do not stream
--no-interactive Never prompt, even on TTY (script-safe override)
--interactive Force prompt, even when not on TTY
-o pretty|json|yaml Output format. Auto: pretty on TTY, json otherwise
--field <jq-path> Extract a single field from the result (e.g. --field .findings[0].score)

Input precedence

  1. --in <value> (inline JSON or @file)
  2. stdin if not a TTY and no --in passed (e.g. cat payload.json | af call X)
  3. TTY + no input + reasoner has a non-empty input schema → prompt for required fields, apply defaults for optional, validate per-field against schema, confirm before submit
  4. TTY + no input + reasoner has empty/no input schema → call immediately with {}
  5. Non-TTY + no input + reasoner requires input → exit 2 with hint

Execution behavior

  • Default (TTY, sync): stream progress as a live tree of sub-agent calls via the server's SSE endpoint; print final result when done
  • Ctrl-C during stream: detach gracefully (run continues server-side); print af tail <run_id> to resume
  • --async: print run_id on stdout, exit 0 if queued successfully
  • Non-TTY (script mode): no progress rendering; final JSON result to stdout, logs/status to stderr

Exit codes

Code Meaning
0 Reasoner completed successfully
1 Reasoner ran but returned an error / failed status
2 Client-side error (bad input, schema validation, missing required field)
3 Transport / network / auth error (server unreachable, 401, 403)

Output streams

  • stdout: result payload only (so af call … | jq . works)
  • stderr: progress tree, prompts, status messages, errors

Examples

# Inline
af call sec-af.hunt --in '{"target":"acme.com"}'

# From file (JSON or YAML auto-detected)
af call contract-af.review --in @payload.yaml

# Stdin pipe (script-friendly)
cat payload.json | af call contract-af.review

# Interactive form (TTY, no --in)
af call sec-af.hunt

# Inspect schema without running
af call sec-af.hunt --schema -o yaml

# Fire-and-forget
af call contract-af.review --in @payload.yaml --async

# Extract one field
af call sec-af.hunt --in '{"target":"acme.com"}' --field .findings[0].severity

af ls [query]

Lists reasoners. Recency-first by default to keep the screen scannable at 100-reasoner scale.

Flags

Flag Purpose
[query] Positional fuzzy filter on <node>.<reasoner> (no flag needed)
--all Show every reasoner, not just recent
--node <name> Filter to a single node
--live Only reasoners whose node is currently healthy
-o pretty|json|yaml Output format

Default output (TTY)

$ af ls
RECENT
sec-af.hunt          1m ago      ● live
contract-af.review   12m ago     ● live
finance.audit_q1     2h ago      ● live
sec-af.prove         yesterday   ○ stale

… 14 more recent  •  100 total  •  use `af ls --all`

$ af ls hunt
sec-af.hunt          1m ago      ● live
bug-hunter.scan      —           ● live

Recency source

last_run_at per reasoner from the server's existing executions table. No client-side state. Server response shape per row:

{ "node": "sec-af", "reasoner": "hunt", "last_run_at": "2026-05-19T10:42:00Z", "status": "live" }

Required server work

The control plane needs a list endpoint that returns reasoners with {node, reasoner, last_run_at, status: live|stale|down}. If /api/v1/agentic/discover doesn't already return last_run_at, extend it; otherwise add a dedicated endpoint (e.g. GET /api/v1/reasoners).


af tail <run_id>

Attaches to a running execution and streams progress events until terminal status.

Flags

Flag Purpose
<run_id> Required positional
--from <step> Resume stream from a specific step (skip already-shown events on reconnect)
-o pretty|json Output format

Behavior

  • Consumes the server's existing SSE/event stream for the given run
  • Renders the same tree view as af call sync mode
  • Exits 0 on terminal success, 1 on failure, 3 on transport error
  • Ctrl-C disconnects; run continues server-side

Required server work

If a per-run SSE event stream doesn't already exist, add one (e.g. GET /api/v1/executions/:run_id/events with Accept: text/event-stream).


Configuration

Reuses existing mechanisms only:

  • AGENTFIELD_SERVER env var (default http://localhost:8080)
  • AGENTFIELD_API_KEY env var
  • --server and --api-key flags on every command (already global in af)

No new env vars. No config files.


Implementation notes

  • New CLI files alongside control-plane/internal/cli/agent_commands.go:
    • call.goaf call command and input/output handling
    • ls.goaf ls command
    • tail.goaf tail command and SSE consumer
  • Wire commands in control-plane/internal/cli/root.go (NewRootCommand)
  • Reuse agentHTTP() helper from agent_commands.go for HTTP requests
  • Schema fetch: use the reasoner's input schema from the existing reasoner registration metadata (whatever endpoint the web UI already calls to render the playground)
  • TTY detection: term.IsTerminal(int(os.Stdin.Fd())) and os.Stdout.Fd()
  • Form prompting: use an existing lightweight library already in the module (check go.mod); if none, prefer github.com/AlecAivazis/survey/v2 — but only if it adds <1MB to the binary
  • SSE consumer: standard library bufio.Scanner over text/event-stream response — no new dependency

Acceptance criteria

  • af call <node>.<reasoner> --in '{…}' runs a reasoner sync, streams progress tree on TTY, returns result on stdout
  • af call X with no --in on a TTY prompts for required fields from the schema, validates per-field, confirms before submit
  • af call X with no --in and no TTY exits non-zero with a clear hint (does not hang)
  • cat input.json | af call X works without --in
  • af call X --in @file.yaml accepts YAML and JSON files
  • af call X --schema prints the input schema without executing
  • af call X --async returns run_id immediately
  • af call X --field .a.b extracts and prints the named field
  • Ctrl-C during a sync run detaches and prints the af tail command to resume
  • af ls shows recency-sorted reasoners on TTY, JSON on pipe
  • af ls hunt fuzzy-filters; af ls --all shows everything; af ls --node sec-af scopes to one node
  • af tail <run_id> streams the same tree view as a live af call
  • Exit codes match the table above
  • All output to stdout/stderr split correctly so af call … | jq . works
  • No new state files written under ~/.agentfield/
  • No new env vars introduced
  • Unit tests cover: TTY vs non-TTY mode selection, input precedence (--in > stdin > prompt > empty), schema validation errors, exit-code mapping
  • Integration test: end-to-end af call against a local control plane with a real reasoner

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions