LLM Plan + Dispatch API (Shell/DB)

This describes the natural language → structured plan → policy → Runner dispatch flow. It is designed for file search/edit and DB queries with a strict, safe-by-default schema.

Endpoints

  • POST /api/agent/llm-plan
  • POST /api/agent/execute-planned-job
  • POST /api/agent/llm-plan-and-dispatch
  • GET /api/agent/decision-traces

LLM Provider Config (YAML)

Set LLM_CONFIG_YAML to load provider routing/config from YAML instead of individual LLM_* env vars.

Example: - LLM_CONFIG_YAML=core/config/llm-providers.example.yml

Validation behavior: - Invalid provider names in default_chain cause startup error - Invalid provider names in tenant_overrides cause startup error - Invalid provider keys in providers cause startup error

api_key supports: - literal key string - env:VARNAME (resolved at runtime)

Plan Request

{
  "tenant_id": "default",
  "env": "dev",
  "prompt": "Find references to grpc in the repo.",
  "workspace_root": "/workspace",
  "db_dsn_ref": null
}

Fields: - tenant_id: tenant scope for provider routing and policy - env: prod or non-prod value - prompt: natural language request - workspace_root: cwd must be under this path for shell jobs - db_dsn_ref: optional DB reference; if provided, LLM must use it

Dispatch-only Additions

POST /api/agent/llm-plan-and-dispatch accepts the same fields plus:

  • runner_id: target Runner
  • approval: optional approval payload (used when policy requires approval)

Execute Planned Job Request

POST /api/agent/execute-planned-job executes a previously planned planned_job after re-validating it:

{
  "tenant_id": "default",
  "runner_id": "runner-1",
  "env": "dev",
  "workspace_root": "/workspace",
  "db_dsn_ref": null,
  "approval": null,
  "planned_job": {
    "workflow_id": "llm-ad-hoc",
    "kind": {
      "tag": "JobShell",
      "contents": {
        "command": "rg -n \"grpc\" .",
        "cwd": "/workspace",
        "shell_timeout_seconds": 30,
        "allowlist_tag": "read"
      }
    }
  }
}

LLM Output Schema (strict)

The LLM must return ONLY JSON:

{
  "decision": "no_op|request_approval|execute_plan",
  "rationale": "short reason",
  "action_kind": "shell|db",
  "risk": "read_only|write",
  "job": {
    "kind": "shell|db",
    "command": "rg -n \"grpc\" .",
    "cwd": "/workspace",
    "timeout_seconds": 30,
    "allowlist_tag": "read",
    "driver": "postgres|mysql|sqlite",
    "dsn_ref": "primary",
    "query": "select ... limit 10"
  }
}

Policy Rules (v0.1)

  • prod: approval required for any execution
  • shell/db + risk=write: approval required in any env
  • shell/db + risk=read_only: allowed in non-prod

Shell constraints: - cwd must be under workspace_root - risk=read_only requires allowlist_tag=read - risk=write requires allowlist_tag=write - Read-only shell commands are enforced with heuristics (e.g. no rm, no redirects)

DB constraints: - risk=read_only requires a read-only query (no INSERT/UPDATE/DELETE/...) - If db_dsn_ref is provided, it must match the job dsn_ref

Plan Response

POST /api/agent/llm-plan returns the validated plan and policy result without dispatching:

{
  "llm_provider": "ProviderInternal",
  "llm_decision": "LlmExecute",
  "llm_rationale": "Safe read-only grep.",
  "final_decision": "FinalExecute",
  "planned_job": {
    "workflow_id": "llm-ad-hoc",
    "kind": {
      "tag": "JobShell",
      "contents": {
        "command": "rg -n \"grpc\" .",
        "cwd": "/workspace",
        "shell_timeout_seconds": 30,
        "allowlist_tag": "read"
      }
    }
  }
}

Dispatch Response

POST /api/agent/llm-plan-and-dispatch returns the same plan plus dispatch status:

{
  "llm_provider": "ProviderInternal",
  "llm_decision": "LlmExecute",
  "llm_rationale": "Safe read-only grep.",
  "final_decision": "FinalExecute",
  "job_id": "job-abc123",
  "planned_job": {
    "workflow_id": "llm-ad-hoc",
    "kind": {
      "tag": "JobShell",
      "contents": {
        "command": "rg -n \"grpc\" .",
        "cwd": "/workspace",
        "shell_timeout_seconds": 30,
        "allowlist_tag": "read"
      }
    }
  }
}

Execute Planned Job Response

POST /api/agent/execute-planned-job returns the inferred action/risk plus dispatch status:

{
  "action_kind": "ActionShell",
  "inferred_risk": "RiskReadOnly",
  "final_decision": "FinalExecute",
  "job_id": "job-abc123"
}

Job Result Query

GET /api/runner/job-results/{jobId}

Returns a normalized JSON shape:

{
  "tenant_id": "default",
  "runner_id": "runner-1",
  "job_id": "job-abc123",
  "status": "SUCCEEDED",
  "output": {
    "stdout": "...",
    "stderr": "...",
    "exit_code": 0
  },
  "error": "",
  "started_unix_ms": 1710000000000,
  "ended_unix_ms": 1710000000500
}

Decision Trace Query

GET /api/agent/decision-traces?limit=50&trace_kind=agent.llm_plan

Returns the most recent redacted decision traces for the authenticated tenant.