* feat(schema): add workflow run tracking tables (LiteLLM_WorkflowRun, LiteLLM_WorkflowEvent, LiteLLM_WorkflowMessage) * feat(proxy): add /v1/workflows/runs endpoints for durable agent workflow tracking * feat(proxy): register workflow management router in proxy_server * docs(workflows): add README for workflow run tracking API * test(workflows): add unit tests for /v1/workflows/runs endpoints * fix(workflows): atomic event+status update via tx(), run_id 404 guard, sequence retry on collision * test(workflows): add tx mock, 404 on unknown run_id, retry-on-collision tests * fix(workflows): constrain status to Literal enum, rename total→count in list responses * add tenant isolation and bounded limits to workflow endpoints * add created_by column and index to LiteLLM_WorkflowRun * add ownership and bounded-limit tests for workflow endpoints * Fix workflow run ownership for null owners * guard prisma import in workflow_management_endpoints * sync schema.prisma copies with workflow run models * black: format workflow_management_endpoints.py --------- Co-authored-by: Cursor Agent <cursoragent@cursor.com> |
||
|---|---|---|
| .. | ||
| README.md | ||
Workflow Run Tracking
Generic durable state tracking for agents and automated workflows built on the LiteLLM proxy.
The Problem
Agents like shin-builder run multi-stage pipelines (triage → plan → implement → PR). Their task state and conversation history lived in memory — a process restart lost everything.
Three-Table Design
WorkflowRun one instance of work (header + materialized status)
WorkflowEvent append-only state transitions (source of truth for replay)
WorkflowMessage conversation inbox/outbox (full content, not truncated)
WorkflowEvent is the source of truth. WorkflowRun.status is a materialized cache updated automatically when events are appended. If you need to debug a run, replay its events.
API
All endpoints require a valid LiteLLM API key (Authorization: Bearer sk-...).
Runs
POST /v1/workflows/runs Create a run
GET /v1/workflows/runs List runs (?workflow_type=&status=)
GET /v1/workflows/runs/{run_id} Get run + latest event
PATCH /v1/workflows/runs/{run_id} Update status / metadata / output
Events
POST /v1/workflows/runs/{run_id}/events Append event (auto-updates run status)
GET /v1/workflows/runs/{run_id}/events Full event log (ordered by sequence)
Messages
POST /v1/workflows/runs/{run_id}/messages Append message
GET /v1/workflows/runs/{run_id}/messages Conversation history (ordered by sequence)
Quick Start
# Create a run
curl -X POST http://localhost:4000/v1/workflows/runs \
-H "Authorization: Bearer sk-1234" \
-H "Content-Type: application/json" \
-d '{"workflow_type": "shin-builder", "metadata": {"title": "Fix login bug"}}'
# {"run_id": "abc-123", "session_id": "xyz-456", "status": "pending", ...}
# Mark step started (sets status → running)
curl -X POST http://localhost:4000/v1/workflows/runs/abc-123/events \
-H "Authorization: Bearer sk-1234" \
-H "Content-Type: application/json" \
-d '{"event_type": "step.started", "step_name": "grill", "data": {"claude_session_id": "sess-789"}}'
# Store a conversation message
curl -X POST http://localhost:4000/v1/workflows/runs/abc-123/messages \
-H "Authorization: Bearer sk-1234" \
-H "Content-Type: application/json" \
-d '{"role": "user", "content": "What is the expected behavior?", "session_id": "sess-789"}'
# Restart recovery: fetch active runs and resume from last event's data.claude_session_id
curl "http://localhost:4000/v1/workflows/runs?status=running,paused&workflow_type=shin-builder" \
-H "Authorization: Bearer sk-1234"
Status Auto-Update Rules
When you append an event, the run's status is updated automatically:
| event_type | run.status |
|---|---|
step.started |
running |
step.failed |
failed |
hook.waiting |
paused |
hook.received |
running |
Set status = completed explicitly via PATCH when the workflow finishes.
Linking to Spend Logs
WorkflowRun.session_id is generated automatically (UUID). Pass it as the x-litellm-session-id header when making completions through the proxy:
headers = {"x-litellm-session-id": run.session_id}
All spend log entries for this run are then tagged automatically. Query cost per run:
POST /ui/spend_logs/view_session_spend_logs?session_id={run.session_id}
Sequence Numbers
Sequence numbers on events and messages are assigned server-side (MAX + 1 per run). Callers never supply them. This guarantees ordering even under concurrent writes.
Using from shin-builder
Replace the in-memory tasks.py dict with calls to these endpoints:
import httpx
class WorkflowRunClient:
def __init__(self, base_url: str, api_key: str):
self._client = httpx.AsyncClient(
base_url=base_url,
headers={"Authorization": f"Bearer {api_key}"},
)
async def create_task(self, title: str, **metadata) -> dict:
r = await self._client.post("/v1/workflows/runs", json={
"workflow_type": "shin-builder",
"metadata": {"title": title, **metadata},
})
r.raise_for_status()
return r.json()
async def list_active_tasks(self) -> list:
r = await self._client.get(
"/v1/workflows/runs",
params={"workflow_type": "shin-builder", "status": "running,paused"},
)
r.raise_for_status()
return r.json()["runs"]
async def transition(self, run_id: str, step_name: str, event_type: str, data: dict = None):
r = await self._client.post(f"/v1/workflows/runs/{run_id}/events", json={
"event_type": event_type,
"step_name": step_name,
"data": data or {},
})
r.raise_for_status()
async def append_message(self, run_id: str, role: str, content: str, session_id: str = None):
r = await self._client.post(f"/v1/workflows/runs/{run_id}/messages", json={
"role": role, "content": content, "session_id": session_id,
})
r.raise_for_status()
On startup, call list_active_tasks() to restore in-flight runs. The last step.started event's data.claude_session_id gives you the --resume ID.