4.1 KiB
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/problemsand/v1/plans/{id}/approve|rejectroute 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}/streamtails the same committed log the outbox relay reads — SSE, not a second read path.
Steps
- Stand up the
axumservice, one route module per resource: runs, plans, goals, metrics. POST /v1/runs→ spawn_run. Request{tenant, workflow_ref, input, goal_id?, dispatch_key?}. Response{run_id, status}.GET /v1/runs/{id},/episode(T1.7),/attempts/{step}/{n}/transcript(T1.6BlobRefdereference),/stream(SSE).POST /v1/runs/{id}/rewind(T2.3),/cancel,/attempts/{step}/retry(T1.4).GET /v1/goals/{id}(T10.1GoalView).GET /v1/metrics,/v1/runs/{id}/metrics(T8.2's existing list, scoped).- Auth/tenant resolution: reject any request without a resolvable
TenantIdbefore touching the kernel — no implicit tenant. - 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
redbbackend. - 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 test — tests/it_http_api_surface.rs:
POST /v1/runs; assert 200 withrun_id; assert kernel showsScheduled.GET /v1/runs/{id}until terminal; assert status matches kernel state exactly at each poll.GET /v1/runs/{id}/episode; assert shape matches T1.7'sEpisodeView, includes failed attempts.GET /v1/runs/{id}/attempts/{step}/{n}/transcript; assert returned bytes matchBlobStorecontent directly (T1.6).POST .../rewind; assert newBranchIdcreated, parent branch still readable via all-branches.POST .../retry; assert newAttemptNo, prior attempt record unmodified.POST .../cancelmid-run; assertCancelledreached (matches T1.3's cancel matrix).- 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