Files
poimen/tasks/T10.4-http-api-surface.md

4.1 KiB

T10.4 — HTTP API surface

Field Value
Phase P10 — Orchestration
Size L — over 3 days
Status Not started
Flags
Spec inlined below
Blocks T10.6, T10.7, T10.9

Goal

axum HTTP service binding one method table — spawn / status / stream / episode / transcript / rewind / cancel / retry / goal / metrics — to poimen-sdk ports. One method set: HTTP today, the same table T9.2's stdio JSON-RPC binds to later.

Facts (inlined — no spec read needed)

  • The protocol is a published artifact — INDEX.md's own rule for the sidecar applies equally here: methods added, never repurposed; fields added, never removed, from the first commit.
  • HTTP is a transport binding onto poimen-sdk (T9.1), not a bespoke access layer with its own business logic. Handlers translate, they do not decide.
  • /v1/problems and /v1/plans/{id}/approve|reject route to T10.6's workflow, not implemented here — this task defines the routes and request/response contracts; T10.6 fills in plan-draft semantics.
  • /v1/runs/{id}/stream tails the same committed log the outbox relay reads — SSE, not a second read path.

Steps

  1. Stand up the axum service, one route module per resource: runs, plans, goals, metrics.
  2. POST /v1/runs → spawn_run. Request {tenant, workflow_ref, input, goal_id?, dispatch_key?}. Response {run_id, status}.
  3. GET /v1/runs/{id}, /episode (T1.7), /attempts/{step}/{n}/transcript (T1.6 BlobRef dereference), /stream (SSE).
  4. POST /v1/runs/{id}/rewind (T2.3), /cancel, /attempts/{step}/retry (T1.4).
  5. GET /v1/goals/{id} (T10.1 GoalView).
  6. GET /v1/metrics, /v1/runs/{id}/metrics (T8.2's existing list, scoped).
  7. Auth/tenant resolution: reject any request without a resolvable TenantId before touching the kernel — no implicit tenant.
  8. Version the route set from commit one: additive only, document the rule inline in the router module.

Acceptance

  • Full request/response cycle for spawn → poll → episode → transcript against an embedded redb backend.
  • Rewind, cancel, retry each produce the same log-level effect as calling the underlying kernel function directly — no HTTP-layer divergence.

Verify

Harness: embedded redb, stub model, axum test client (tower::ServiceExt::oneshot).

Integration testtests/it_http_api_surface.rs:

  1. POST /v1/runs; assert 200 with run_id; assert kernel shows Scheduled.
  2. GET /v1/runs/{id} until terminal; assert status matches kernel state exactly at each poll.
  3. GET /v1/runs/{id}/episode; assert shape matches T1.7's EpisodeView, includes failed attempts.
  4. GET /v1/runs/{id}/attempts/{step}/{n}/transcript; assert returned bytes match BlobStore content directly (T1.6).
  5. POST .../rewind; assert new BranchId created, parent branch still readable via all-branches.
  6. POST .../retry; assert new AttemptNo, prior attempt record unmodified.
  7. POST .../cancel mid-run; assert Cancelled reached (matches T1.3's cancel matrix).
  8. Request with no resolvable tenant; assert rejected before any kernel call — instrument kernel entrypoints, assert zero calls.

Command: cargo test -p api http_api_surface

False pass:

  • Asserting HTTP 200 without checking underlying kernel state — a route that accepts and silently drops a request passes this.
  • Testing transcript retrieval only for a successful attempt — failed-attempt transcripts are the debugging case that matters most.

Traps

  • Putting business logic (retry policy, plan validation) in the HTTP handler instead of the kernel/port layer. The handler is a transport, not a second brain.
  • Resolving BlobRefs eagerly inside /episode "for convenience" — T4.3's regression, one layer up.

Background (not required to do this task): rust-agentic-sys.md §8.5, §10.1 · ../../INDEX.md (embedding contract, ~line 60-86) · T1.7-episode-query-surface.md · T1.6-prompt-and-output-blob-capture.md