Add web product Phase 1 skeleton

This commit is contained in:
2026-03-20 00:20:38 +08:00
parent 0355d7a847
commit a7ef1e0154
24 changed files with 2287 additions and 6 deletions
+77
View File
@@ -0,0 +1,77 @@
# Event Stream Contract
## Status
Planned for the next milestone.
The Phase 1 web skeleton does not implement the event stream yet.
## Intended Endpoint
- `GET /api/events/stream`
## Transport Choice
- start with Server-Sent Events rather than websockets
- keep the stream read-only and cursor-based
- let the frontend use events to invalidate or refresh TanStack Query caches
## Query Parameters
- `after_event_id`: optional monotonic cursor
- `run_id`: optional run filter for run-scoped pages
- `thread_id`: optional thread filter for timeline views
## Event Envelope
Each SSE message should carry one JSON object with this shape:
```json
{
"event_id": 42,
"event_type": "task_blocked",
"run_id": "run_web_001",
"task_id": "T1",
"thread_id": "thr_123",
"summary": "Need the API shape",
"payload": {
"source": "orch"
},
"created_at": "2026-03-20T09:30:00Z"
}
```
## Expected First Event Types
- `task_added`
- `task_ready`
- `task_dispatched`
- `task_running`
- `task_blocked`
- `task_answered`
- `task_done`
- `task_failed`
- `thread_claim`
- `thread_update`
- `thread_reply`
- `thread_result`
- `council_tallied`
- `council_report_persisted`
## Cursor Rules
- `event_id` is the only resume cursor
- the server should emit events in ascending `event_id` order
- reconnect clients should pass the last processed `event_id`
- the server should not depend on in-memory subscriptions for correctness
## Frontend Usage
- run detail pages should subscribe with `run_id`
- thread views may subscribe with `thread_id`
- the client should prefer refetch/invalidate behavior over complex local reducers in the first realtime slice
## Backend Notes
- source data should come from the existing shared `events` table
- `orchd` should treat the stream as a projection over persisted events, not as a separate ephemeral bus
- if filtering becomes expensive later, move filtering logic into a dedicated query layer without changing the wire contract
+363
View File
@@ -0,0 +1,363 @@
openapi: 3.1.0
info:
title: Orch Web API
version: 0.1.0
summary: Initial read-only web API for the orchestration control plane.
servers:
- url: http://localhost:8080
paths:
/health:
get:
summary: Health check
operationId: getHealth
responses:
'200':
description: Service is healthy
content:
application/json:
schema:
type: object
required: [status]
properties:
status:
type: string
example: ok
/api/runs:
get:
summary: List orchestration runs
operationId: listRuns
responses:
'200':
description: Run summaries
content:
application/json:
schema:
type: object
required: [runs]
properties:
runs:
type: array
items:
$ref: '#/components/schemas/RunListItem'
/api/runs/{runID}:
get:
summary: Get run detail
operationId: getRunDetail
parameters:
- $ref: '#/components/parameters/RunID'
responses:
'200':
description: Run detail including tasks and blocked-task summaries
content:
application/json:
schema:
type: object
required: [run]
properties:
run:
$ref: '#/components/schemas/RunDetail'
'404':
$ref: '#/components/responses/NotFound'
/api/runs/{runID}/tasks:
get:
summary: List tasks for a run
operationId: listRunTasks
parameters:
- $ref: '#/components/parameters/RunID'
responses:
'200':
description: Task list
content:
application/json:
schema:
type: object
required: [tasks]
properties:
tasks:
type: array
items:
$ref: '#/components/schemas/Task'
'404':
$ref: '#/components/responses/NotFound'
/api/runs/{runID}/blocked:
get:
summary: List blocked tasks for a run
operationId: listBlockedTasks
parameters:
- $ref: '#/components/parameters/RunID'
responses:
'200':
description: Blocked task list
content:
application/json:
schema:
type: object
required: [blocked]
properties:
blocked:
type: array
items:
$ref: '#/components/schemas/BlockedTask'
'404':
$ref: '#/components/responses/NotFound'
/api/threads/{threadID}:
get:
summary: Get thread timeline
operationId: getThreadDetail
parameters:
- $ref: '#/components/parameters/ThreadID'
responses:
'200':
description: Thread detail with messages and artifacts
content:
application/json:
schema:
type: object
required: [thread]
properties:
thread:
$ref: '#/components/schemas/ThreadDetail'
'404':
$ref: '#/components/responses/NotFound'
components:
parameters:
RunID:
name: runID
in: path
required: true
schema:
type: string
ThreadID:
name: threadID
in: path
required: true
schema:
type: string
responses:
NotFound:
description: Requested resource was not found
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorEnvelope'
schemas:
ErrorEnvelope:
type: object
required: [error]
properties:
error:
type: object
required: [code, message]
properties:
code:
type: string
example: not_found
message:
type: string
Run:
type: object
required: [run_id, goal, summary, status, created_at, updated_at]
properties:
run_id:
type: string
goal:
type: string
summary:
type: string
status:
type: string
created_at:
type: string
format: date-time
updated_at:
type: string
format: date-time
RunListItem:
type: object
required: [run, task_counts, total_tasks]
properties:
run:
$ref: '#/components/schemas/Run'
task_counts:
type: object
additionalProperties:
type: integer
total_tasks:
type: integer
RunDetail:
type: object
required: [run, task_counts, total_tasks, tasks, blocked_tasks]
properties:
run:
$ref: '#/components/schemas/Run'
task_counts:
type: object
additionalProperties:
type: integer
total_tasks:
type: integer
tasks:
type: array
items:
$ref: '#/components/schemas/Task'
blocked_tasks:
type: array
items:
$ref: '#/components/schemas/BlockedTask'
Task:
type: object
required:
[run_id, task_id, title, summary, status, priority, acceptance_json, created_at, updated_at]
properties:
run_id:
type: string
task_id:
type: string
title:
type: string
summary:
type: string
status:
type: string
default_to:
type: string
priority:
type: string
acceptance_json:
description: Raw JSON acceptance criteria payload
latest_attempt_no:
type: integer
created_at:
type: string
format: date-time
updated_at:
type: string
format: date-time
TaskAttempt:
type: object
required: [run_id, task_id, attempt_no, assigned_to, thread_id, status, created_at, updated_at]
properties:
run_id:
type: string
task_id:
type: string
attempt_no:
type: integer
assigned_to:
type: string
thread_id:
type: string
base_ref:
type: string
base_commit:
type: string
branch_name:
type: string
worktree_path:
type: string
workspace_status:
type: string
result_commit:
type: string
status:
type: string
created_at:
type: string
format: date-time
updated_at:
type: string
format: date-time
Artifact:
type: object
required: [artifact_id, message_id, path, kind, metadata_json, created_at]
properties:
artifact_id:
type: string
message_id:
type: string
path:
type: string
kind:
type: string
metadata_json:
description: Raw JSON artifact metadata
created_at:
type: string
format: date-time
Message:
type: object
required:
[message_id, thread_id, from_agent, to_agent, kind, summary, body, payload_json, created_at]
properties:
message_id:
type: string
thread_id:
type: string
from_agent:
type: string
to_agent:
type: string
kind:
type: string
summary:
type: string
body:
type: string
payload_json:
description: Raw JSON message payload
created_at:
type: string
format: date-time
artifacts:
type: array
items:
$ref: '#/components/schemas/Artifact'
Thread:
type: object
required:
[thread_id, run_id, task_id, subject, created_by, assigned_to, status, priority, created_at, updated_at]
properties:
thread_id:
type: string
run_id:
type: string
task_id:
type: string
subject:
type: string
created_by:
type: string
assigned_to:
type: string
status:
type: string
priority:
type: string
latest_message_id:
type: string
created_at:
type: string
format: date-time
updated_at:
type: string
format: date-time
ThreadDetail:
type: object
required: [thread, messages]
properties:
thread:
$ref: '#/components/schemas/Thread'
messages:
type: array
items:
$ref: '#/components/schemas/Message'
BlockedTask:
type: object
required: [task, attempt, question]
properties:
task:
$ref: '#/components/schemas/Task'
attempt:
$ref: '#/components/schemas/TaskAttempt'
question:
$ref: '#/components/schemas/Message'