Source: heuristic discovery from
app/main.pyrouter wiring and per-featureroutes.py. Full request/response schemas live in the Pydantic models atapp/features/<slice>/schemas.py. Swagger UI athttp://localhost:8123/docsis the authoritative live contract.
All endpoints serve JSON; error responses use application/problem+json (RFC 7807) via app/core/problem_details.py. Schemas are Pydantic v2 (app/features/<slice>/schemas.py).
| Slice | Method | Path | Purpose |
|---|---|---|---|
| health | GET | /health |
Liveness probe — {"status":"ok"} |
| ingest | POST | /ingest/sales-daily |
Batch upsert with natural-key resolution, idempotent ON CONFLICT DO UPDATE |
| dimensions | GET | /dimensions/stores |
List stores (1-indexed pagination, region/store_type filter, case-insensitive search, optional allow-listed sort_by/sort_order) |
| dimensions | GET | /dimensions/stores/{store_id} |
Get store by ID |
| dimensions | GET | /dimensions/products |
List products (category/brand filter, sku/name search, optional allow-listed sort_by/sort_order) |
| dimensions | GET | /dimensions/products/{product_id} |
Get product by ID |
| analytics | GET | /analytics/kpis |
Aggregated KPIs (revenue, units, transactions, avg unit price, avg basket) |
| analytics | GET | /analytics/drilldowns |
Group-by dimension: store / product / category / region / date |
| analytics | GET | /analytics/timeseries |
Period-bucketed sales series (granularity = day/week/month/quarter) for revenue-over-time charts; reuses validate_date_range (inverted/over-730-day ranges 400) |
| analytics | GET | /analytics/inventory-status |
Latest inventory_snapshot_daily row per (store, product) grain (Postgres DISTINCT ON); optional store_id/product_id filters; 200 + empty list on an empty table (never 404) |
| featuresets | POST | /featuresets/compute |
Compute time-safe features (lag/rolling/calendar, leakage-prevented) |
| featuresets | POST | /featuresets/preview |
Preview features with sample rows |
| forecasting | POST | /forecasting/train |
Train a model. PRP-36 expands the model set: target-only naive/seasonal_naive/moving_average/weighted_moving_average/seasonal_average/trend_regression_baseline; feature-aware regression/prophet_like (always-on); opt-in lightgbm/xgboost/random_forest behind the matching forecast_enable_* flag. regression wraps HistGradientBoostingRegressor on lag + calendar + exogenous features — the baseline a model_exogenous scenario re-forecasts through |
| forecasting | POST | /forecasting/predict |
Generate horizon predictions from a trained model |
| backtesting | POST | /backtesting/run |
Time-series CV. PRP-36 — aggregated_metrics now carries rmse alongside MAE/sMAPE/WAPE/bias; every fold_results[i].horizon_bucket_metrics is a per-bucket metric dict keyed by h_1_7/h_8_14/h_15_28/h_29_plus (empty buckets dropped); main_model_results.bucketed_aggregated_metrics (and same on each baseline_results[i]) carries per-bucket means across folds, or null when no fold emitted a bucket dict |
| explainability | POST | /explain/forecast |
Rule-based explanation of the h=1 forecast a named baseline model (naive/seasonal_naive/moving_average) produces on the series ending at as_of_date; returns a ForecastExplanation — driver contributions, advisory retail reason codes (correlation, not causation), confidence band, caveats, agent summary. Time-safe (<= as_of_date); a non-baseline model_type or a too-short series → RFC 7807 400 |
| explainability | GET | /explain/runs/{run_id} |
Explain a registry model_run — config reconstructed from model_run.model_config, cutoff data_window_end. Missing run → 404; a non-baseline (lightgbm/regression) run → 400 |
| explainability | GET | /explain/jobs/{job_id} |
Explain a completed predict job — store/product/model read from job.result, cutoff = day before the first forecast date. Missing job → 404; a job that is not a completed predict job → 400 |
| scenarios | POST | /scenarios/simulate |
Stateless what-if: load a baseline model, forecast, apply price/promotion/holiday/inventory/lifecycle assumptions, return a ScenarioComparison. A regression baseline genuinely re-forecasts through a leakage-safe future feature frame (method="model_exogenous"); any other baseline applies a deterministic post-forecast multiplier (method="heuristic"). Bogus run_id → RFC 7807 404 |
| scenarios | POST | /scenarios |
Run a simulation and persist it as a named scenario_plan (raw assumptions + full comparison snapshot); optional tags + cloned_from |
| scenarios | GET | /scenarios |
List saved scenario plans, newest first (limit/offset, optional repeated tags filter — JSONB containment); 200 + empty list on an empty table |
| scenarios | GET | /scenarios/{scenario_id} |
Saved plan + embedded comparison snapshot; 404 when missing |
| scenarios | POST | /scenarios/compare |
Rank 2-5 saved plans (scenario_ids, rank_by) against a shared baseline; returns a MultiScenarioComparison with ranked rows + merged multi-series chart data. Unknown scenario_id → 404 |
| scenarios | DELETE | /scenarios/{scenario_id} |
Delete a saved plan; 404 when missing |
| registry | POST | /registry/runs |
Create model run (pending) |
| registry | GET | /registry/runs |
List with filters + pagination + optional allow-listed sort_by/sort_order (created_at/model_type/status/store_id/product_id; unknown → default created_at desc) |
| registry | GET | /registry/runs/{run_id} |
Run details + JSONB metrics + runtime_info. PRP-36 — response gains Optional computed fields feature_frame_version: int | null and feature_groups: dict[str, list[str]] | null (both read from runtime_info; null for V1 / pre-PRP-35 runs) |
| registry | PATCH | /registry/runs/{run_id} |
Update status / metrics / artifact_uri |
| registry | GET | /registry/runs/{run_id}/verify |
SHA-256 artifact integrity check |
| registry | POST | /registry/aliases |
Create/update alias (only on success runs) |
| registry | GET | /registry/aliases |
List aliases |
| registry | GET | /registry/aliases/{alias_name} |
Get alias |
| registry | DELETE | /registry/aliases/{alias_name} |
Delete alias |
| registry | GET | /registry/compare/{run_id_a}/{run_id_b} |
Diff two runs |
| jobs | POST | /jobs |
Submit train / predict / backtest (returns 202-style job_id) |
| jobs | GET | /jobs |
List with filters + optional allow-listed sort_by/sort_order (created_at/completed_at/job_type/status; unknown → default created_at desc) |
| jobs | GET | /jobs/{job_id} |
Status + result JSON |
| jobs | DELETE | /jobs/{job_id} |
Cancel pending |
| rag | POST | /rag/index |
Index a markdown/openapi document; idempotent via content hash |
| rag | POST | /rag/index/project-docs |
Bulk-index bundled docs/, PRPs/, and root markdown; per-file + aggregate summary; idempotent via content hash; 502 if the embedding provider fails. PRP-40 — body accepts an additive Optional path_prefix: str | None (default None) that restricts the docs/ root scan to a sub-path (e.g. docs/user-guide); a path-traversal-escaping prefix returns 422 application/problem+json. |
| rag | POST | /rag/retrieve |
Semantic search (HNSW), top-k with similarity threshold |
| rag | GET | /rag/sources |
List indexed sources |
| rag | DELETE | /rag/sources/{source_id} |
Delete source + cascaded chunks |
| agents | POST | /agents/sessions |
Create session (agent_type: experiment or rag_assistant) |
| agents | GET | /agents/sessions/{session_id} |
Status + message history (Postgres JSONB) |
| agents | POST | /agents/sessions/{session_id}/chat |
Send user message; returns full response. #335 — when every model in the agent's fallback chain fails with a provider error, returns 502 application/problem+json with code="AGENT_FALLBACK_EXHAUSTED", type=/errors/agent-fallback-exhausted, and an additive failures: [{model_name, status_code, reason, detail}] extension member (secret-scrubbed, 300-char-capped details) |
| agents | POST | /agents/sessions/{session_id}/approve |
Approve/reject a pending tool call (HITL gate) |
| agents | DELETE | /agents/sessions/{session_id} |
Close session |
| agents | WS | /agents/stream |
Token-by-token streaming + tool-call events |
| seeder | (see app/features/seeder/routes.py) |
/seeder/* |
Trigger scenarios, status, customization. E3 (#409) — POST /seeder/generate accepts an additive Optional overrides object (SeederOverrides, app/shared/seeder/overrides.py) with 7 allow-listed knobs: stores (1-100), products (1-500), window_days (75-365; recomputes start_date from end_date), sparsity (0-0.9), promotion_intensity (0-0.5), stockout_intensity (0-0.5), noise_sigma (0-0.5). extra=forbid → an unknown knob is a 422; applied LAST in _build_config_from_params so it wins over the scalar stores/products/sparsity params; absent = byte-identical legacy behavior |
| seeder | POST | /seeder/phase2-enrichment |
PRP-38 — run Phase 2 generators (lifecycle, replenishment, exogenous, returns) against the existing seeded data. 422 application/problem+json on an empty database. |
| demo | POST | /demo/run |
Run the end-to-end demo pipeline in-process; returns a DemoRunResult. 409 application/problem+json if a run is already active. PRP-38 — body accepts an Optional scenario: 'demo_minimal' | 'showcase_rich' | 'sparse' field; default 'demo_minimal' (back-compat). E1 (#390) — body accepts additive Optional preservation: 'ephemeral' | 'keep' (default 'ephemeral', today's no-row behavior) and workspace_name: str | null (pattern ^[a-z0-9][a-z0-9\-_]*$, ≤100 chars); workspace_name without preservation='keep' → 422 application/problem+json. preservation='keep' records the run as a showcase_workspace row; DemoRunResult gains an additive Optional workspace_id: str | null. E2 (#391) — scenario accepts all 8 ScenarioPreset values (retail_standard / holiday_rush / high_variance / stockout_heavy / new_launches / sparse / demo_minimal / showcase_rich); only showcase_rich changes the step table (24 rows), every other preset runs the legacy 11-row flow. E1 (#407) — body accepts additive Optional replayed_from_workspace_id: str | null (^[0-9a-f]{32}$); requires preservation='keep' (else 422 application/problem+json); recorded verbatim on the new showcase_workspace row as a SOFT reference (no existence check — dangles are designed). E3 (#409) — body accepts additive Optional seed_overrides (the same SeederOverrides object as POST /seeder/generate; requires skip_seed=false else 422; window_days rejected on the calendar-pinned holiday_rush preset; {} normalizes to null) and user_scope ({store_id: int>=1, product_id: int>=1}, extra=forbid — the focus pair the pipeline models instead of the auto-discovered first pair; validated by the status step, WARN + fallback to discovery on a dangling pair). Both persist into the kept workspace row's story slots and replay verbatim. E4 (#410) — body accepts additive Optional train_model_types: list[str] | null (1-10 items, allow-listed against the 11 KNOWN_MODEL_TYPES in app/shared/model_taxonomy.py; unknown/duplicate → 422) and backtest: DemoBacktestConfig | null ({horizon 1-90 def 14, strategy expanding|sliding, n_splits 2-20 def 3, min_train_size ≥7 def 30, gap 0-30 def 0, metric wape|mae|rmse}; gap ≥ horizon → 422). Both None → byte-identical legacy behaviour (the baseline trio + default split). A selected opt-in model whose forecast_enable_* flag is off fails the train step (NOT validation — D6) with a detail naming the flag. On preservation='keep' runs the config is recorded verbatim in the showcase_workspace.run_config column and replayed verbatim; pipeline_complete.data.run_config echoes it (null on default-config runs). |
| demo | WS | /demo/stream |
Stream one StepEvent per pipeline step for the live Showcase page. E4 (#410) — the start frame additively accepts train_model_types + backtest (same shapes/validation as POST /demo/run); a bad selection (unknown/duplicate model, gap ≥ horizon) is one error event then close. The frontend sends both keys only when the operator changed them (dirty-only rule), so an untouched run streams a byte-identical legacy frame. |
| demo | GET | /demo/workspaces |
E4 (#393) — list saved showcase workspaces, newest first (limit 1-100 default 20 / offset); 200 + empty list on an empty table. E1 (#407) — list items additively carry archived, pinned, tags, replayed_from_workspace_id. E2 (#408) — additive query params: q (name ILIKE search, min 2 chars), repeated tags (JSONB containment — all listed tags must match), include_archived (default false — archived rows are now HIDDEN by default), allow-listed sort_by (created_at/name/seed/status; unknown → default created_at desc, no 422) + sort_order (asc/desc); pinned rows always order first; total respects the active filters. E3 (#409) — list items additively carry the seed_overrides / user_scope story slots (null on runs without them) — deliberately on the LIST item, because the frontend Replay builds its verbatim start frame from list rows. E4 (#410) — list items additively carry run_config ({train_model_types, backtest} or null on default-config rows) — also on the LIST item so Replay rebuilds the run config from list rows |
| demo | GET | /demo/workspaces/{workspace_id} |
E4 (#393) — full workspace row incl. created_objects soft references + grain/window columns; 404 application/problem+json when missing. E1 (#407) — response additively carries the list-item lifecycle fields plus notes, config_schema_version, and the six story slots (seed_overrides / user_scope / approval_events / rag_events / job_ids / phase_summaries — null until their writer epic lands; schemas in docs/_base/DOMAIN_MODEL.md). E3 (#409) — seed_overrides and user_scope are now WRITTEN (recorded at create time from the start frame) and surfaced on the LIST item as well (Detail inherits) |
| demo | GET | /demo/workspaces/{workspace_id}/health |
E2 (#408) — probe the workspace's soft references in-process (model runs, scenario plans, alias, batch, agent session, job_ids slot) via httpx.ASGITransport; per-reference status ∈ alive (2xx) / dead (404 — deleted after the run) / unknown (anything else — never a 500), plus alive/dead/unknown counts and partial_run (true when the row's status ≠ completed); non-probeable keys (v2_model_path, scenario_artifact_key, train_model_types) are skipped; 404 application/problem+json when the workspace is missing |
| demo | PATCH | /demo/workspaces/{workspace_id} |
E1 (#407) — partial lifecycle update (name / notes / tags / archived / pinned; exclude_unset semantics — only provided fields change; explicit null clears name/notes; explicit null on archived/pinned/tags → 422 (send [] to clear tags); status NOT patchable — the pipeline owns it); returns the updated WorkspaceDetailResponse; empty body = 200 no-op; 404 application/problem+json when missing; 422 on unknown keys / bad name pattern / >20 tags |
| demo | DELETE | /demo/workspaces/{workspace_id} |
Delete one saved workspace METADATA row; 204 on success, 404 application/problem+json when missing. The run's created objects (model runs, scenario plans, aliases, jobs, artifacts) are soft references and are NOT deleted |
| demo | POST | /demo/workspaces/{workspace_id}/export |
E6 (#412) — write a checksum-validated bundle under artifacts/showcase/<workspace_id>/: a versioned manifest.json (full WorkspaceDetailResponse snapshot + bundle_format_version: 1 + exported_at + model-run references), one scenario_plans/<scenario_id>.json per resolvable plan, and a sha256sum-compatible checksums.sha256 covering every other file. Re-reads + recomputes every checksum before returning (validated: bool). Soft references resolve over the in-process HTTP surface (GET /registry/runs/{id} + /verify, GET /scenarios/{id}); model artifacts are REFERENCED (uri + registry hash + live artifact_verified), never copied. Dangling soft references (deleted run / plan) become unresolved_references entries and the export still returns 200. Returns WorkspaceExportResult (bundle_path, full files inventory with hashes/sizes, counts, unresolved_references, validated). 404 application/problem+json when missing; 409 while status="running" (references not yet settled); 500 on a disk write failure. Re-export overwrites the bundle deterministically (exported_at records the moment). No migration, no DB writes — stateless and re-runnable; artifacts/ is gitignored so bundles never enter version control. failed and archived workspaces export normally |
| demo | POST | /demo/hitl-decision |
E5 (#411) — relay the Showcase HITL step card's Approve/Reject to the in-flight pipeline. Body {action_id: str, decision: 'approved' | 'rejected', reason?: str ≤500} (ConfigDict(strict=True, extra='forbid')). 204 on success; 404 application/problem+json when no matching action is pending; 409 when the action was already decided; 422 on a malformed body. The in-memory single-slot relay is safe because the pipeline runs one-at-a-time under the module _pipeline_lock; the pipeline forwards the real decision to /agents/sessions/{id}/approve (approved=true|false + reason) — agent_require_approval is untouched. A reject keeps the pipeline GREEN (D5); the gated save_scenario never executes |
| demo | GET | /demo/approval-events |
E5 (#411) — recent HITL approval events flattened across the newest saved workspaces carrying the approval_events slot, newest-workspace-first (limit 1-200 default 50); 200 + empty list when none. Each item carries workspace_id / workspace_name plus the entry's base + additive keys (decision, tool_name, auto_approved, reason, execution_status, transcript_summary, …). Audit-glance surface — no pagination/offset (D6). Backs the /ops page's Approval History table (frontend-only — the ops slice does not import demo code) |
| config | GET | /config/ai |
Effective AI-model config (agent LLM + RAG embeddings); API keys masked, never raw |
| config | PATCH | /config/ai |
Persist + apply AI-model changes live (no restart). 409 if an embedding-dimension change would orphan indexed RAG chunks (resend with force=true) |
| config | GET | /config/providers/health |
Per-provider connectivity — Ollama probed live, cloud providers by API-key presence |
| config | GET | /config/ollama/models |
Models pulled on the configured Ollama host. 502 if the host is unreachable |
Verified against app/features/agents/websocket.py and app/features/agents/schemas.py:229 (StreamEvent):
- Client → server (per message):
{"session_id": str, "message": str}— both fields required; missing fields return a recoverableerrorevent. The connection stays open for multiple messages within one session; each message gets a fresh DB session. - Server → client (every frame):
{"event_type": <one of below>, "data": {...}, "timestamp": <iso8601 UTC>}(Pydantic-serializedStreamEvent). event_typevalues (Literal inStreamEvent):text_delta—data: {"delta": str}(TextDeltaEvent)tool_call_start—data: {"tool_name": str, "tool_call_id": str, "arguments": dict}(ToolCallStartEvent)tool_call_end—data: {"tool_name": str, "tool_call_id": str, "result": Any, "duration_ms": float}(ToolCallEndEvent)approval_required— emitted when a tool inagent_require_approvalis pending; the chat REST/agents/sessions/{id}/approveendpoint releases itcomplete—data: {"message": str, "tokens_used": int, "tool_calls_count": int}(CompleteEvent)error—data: {"error": str, "error_type": str, "recoverable": bool}(ErrorEvent). Onrecoverable: false(e.g.,session_not_found,session_expired), the client should close. #335 — when every model in the agent's fallback chain fails with a provider error, the event carrieserror_type="fallback_exhausted",recoverable=true, a human-actionable per-leg summary inerror, and an additive Optionalfailures: [{model_name, status_code: int|null, reason, detail}]key (reason∈model_not_found/quota_exhausted/auth_error/provider_unavailable/provider_error/response_rejected/unknown;detailis secret-scrubbed and 300-char-capped).
Drives the end-to-end demo pipeline for the dashboard Showcase page. Verified against app/features/demo/routes.py and app/features/demo/schemas.py (StepEvent).
- Client → server (one start frame):
{"seed": int, "reset": bool, "skip_seed": bool, "scenario"?: "demo_minimal" | "showcase_rich" | "sparse", "preservation"?: "ephemeral" | "keep", "workspace_name"?: str}— all fields optional (DemoRunRequestsupplies defaultsseed=42,reset=false,skip_seed=true,scenario="demo_minimal",preservation="ephemeral",workspace_name=null). E1 (#390) —workspace_namerequirespreservation="keep"(else oneerrorevent from validation); unknown start-frame keys remain ignored (forward/backward compat). E2 (#391) —scenarioaccepts all 8ScenarioPresetvalues (retail_standard/holiday_rush/high_variance/stockout_heavy/new_launches/sparse/demo_minimal/showcase_rich); onlyshowcase_richchanges the step table (24 rows), every other preset runs the legacy 11-row flow. E1 (#407) — the start frame additively acceptsreplayed_from_workspace_id?: str(^[0-9a-f]{32}$, requirespreservation="keep"else oneerrorevent from validation); the Showcase Replay button sends the source row'sworkspace_id, recorded verbatim on the NEW row as a soft reference. E3 (#409) — the start frame additively acceptsseed_overrides?: {stores?, products?, window_days?, sparsity?, promotion_intensity?, stockout_intensity?, noise_sigma?}(allow-listed — an unknown knob is oneerrorevent; requiresskip_seed=false;window_daysrejected onholiday_rush) anduser_scope?: {store_id, product_id}; the seed step forwardsseed_overridesverbatim toPOST /seeder/generate(itsdataechoesoverrides_applied), the status step adopts a validuser_scope(detail says "(user-selected)",data.user_scope_applied=true) or WARNS and falls back to discovery on a dangling pair; both persist to the kept workspace row and replay verbatim. The pipeline runs once, then the server closes. - Server → client (every frame): Pydantic-serialized
StepEvent—{"event_type", "step_name", "step_index", "total_steps", "status", "detail", "duration_ms", "data", "timestamp", "phase_name"?, "phase_index"?, "phase_total"?}. PRP-38 — the threephase_*fields are Optional + Nullable so legacy clients that don't render phases keep working. event_typevalues (Literal inStepEvent):step_start— a step began;statusisnull.step_complete— a step finished;status ∈ {pass, fail, skip, warn},datacarries structured payload (backtestper_modelWAPE +winner+bucketed_aggregated_metricson PRP-36/38 feature-aware runs; registerrun_id+alias; PRP-38v2_train→v2_run_id+feature_frame_version+feature_columns_count+feature_groups+artifact_uri_full). E5 (#411) —agent_hitl_flowALSO emits an INTERMEDIATEstep_completewithstatus="running"+data.awaiting_approval=truewhile it is still awaiting; that frame now reaches the browser DURING the decision window (the orchestrator drains the intermediate-event sink concurrently with the in-flight step — D2; pre-E5 it flushed only after the step returned, so the button could never render in time). Itsdatagainsdecision_url: "/demo/hitl-decision"+decision_window_s: float(the FE countdown reads this, never hardcodes) alongside the existingaction_id/session_id/approval_url.pipeline_complete— final event;datacarrieswinner_model_type,winner_wape,winning_run_id,alias,wall_clock_s,v2_run_id(PRP-38; null when no V2 run was registered), andworkspace_id(E1 #390; additive — a string onpreservation="keep"runs, null otherwise).error— bad start frame or a concurrent run already in progress; one event, then the server closes.
- Concurrency: a module-level
asyncio.Lockallows one pipeline at a time. A secondPOST /demo/runreturns409; a secondWS /demo/streamreceives oneerrorevent. - PRP-38 —
scenario="showcase_rich"extends the data phase withphase2_enrichment+historical_backfillsteps and the modeling phase withv2_train(one V2prophet_likerun). Phase ids aredata/modeling/decision/verify/agent/cleanup(6 phases). - PRP-40 —
scenario="showcase_rich"ALSO adds two phases inserted BEFOREverify:planning(2 steps —scenario_simulate_and_save,multi_plan_compare) andknowledge(3 steps —embedding_provider_probe,rag_index_subset,rag_retrieve_probe). Total step count: 19 forshowcase_rich, 11 fordemo_minimalandsparse. Phase ids onshowcase_richaredata/modeling/decision/planning/knowledge/verify/agent/cleanup(8 phases). The knowledge steps SKIP gracefully when the embedding provider is unreachable; the pipeline still goes green. - E3 (#392) — the planning-phase steps tag the plans they save: pipeline-saved plans now carry
source:showcase(alongside the legacyshowcase+price/holidaytags), and onpreservation="keep"runs additionallyworkspace:<workspace_name|workspace_id>— retrievable viaGET /scenarios?tags=workspace:<label>(JSONB containment, all listed tags must match). Thescenario_simulate_and_savestep'sdataadditively echoes thetagslist it sent. - E4 (#393) — the start frame's E1 preservation fields are now exercised by the Showcase UI ("Save as workspace" checkbox + name + seed inputs). Replay re-submits a recorded workspace's config verbatim (
seed/scenario/reset/skip_seed) withpreservation="keep"(+ the recordedworkspace_name), creating a NEWshowcase_workspacerow each time — the original row is never mutated; names are non-unique by design. Saved rows are read back overGET /demo/workspaces(+/{workspace_id}). E1 (#407) — the Replay start frame now also sendsreplayed_from_workspace_id: <source workspace_id>, so replays carry lineage. E2 (#408) — every panel Replay now requires an explicit confirmation dialog with a recorded-vs-sent config preview (destructive copy + destructive-styled confirm onreset=trueworkspaces); the saved-workspaces panel renders the lineage as a replay badge + clickable ancestor chain (deleted ancestors marked, never an error), and a two-workspace compare page lives at/showcase/compare?a=&b=(frontend-only diff — no new backend endpoint). - E5 (#411) —
scenario="showcase_rich"agents phase: the HITL decision window grew 3 s → 10 s (a human can now actually click) and theagent_hitl_flowstep card renders Approve + Reject buttons that POST/demo/hitl-decision(the relay; NOT/agents/.../approvedirectly). An operator Reject keeps the run GREEN (D5) — terminal("pass", "rejected by operator", …), and the gatedsave_scenarionever runs (noscenario_planrow). No decision in the window → auto-approve (auto_approved=true). Onpreservation="keep"runs the resolved approvals land in the workspaceapproval_eventsslot and the three knowledge steps land inrag_events(schema v2 — seedocs/_base/DOMAIN_MODEL.md); a replay keep-run additionally recordsresult_summary.story_reproduction. Capture is warn-and-continue — it never fails a green pipeline. NoDemoRunRequestchange; legacy /demo_minimal/sparseframes stream byte-identically (no relay events, no slots written).
None. Job execution is synchronous-with-async-shaped-API (per app/features/jobs/). No Kafka / SQS / pub-sub. Per .claude/rules/product-vision.md, not a streaming system.
| Integration | Direction | Auth | Rate Limit | Fallback |
|---|---|---|---|---|
| OpenAI (embeddings + agent LLM) | egress HTTPS | OPENAI_API_KEY |
provider-side | switch RAG_EMBEDDING_PROVIDER=ollama; switch agent model |
| Anthropic (agent LLM) | egress HTTPS | ANTHROPIC_API_KEY |
provider-side | AGENT_FALLBACK_MODEL |
| Google Gemini (agent LLM, optional) | egress HTTPS | GOOGLE_API_KEY |
provider-side | switch model |
| Ollama (local embeddings, optional) | egress HTTP LAN | none | local | switch back to OpenAI |
- Pre-1.0: API contracts under
/dimensions,/analytics,/ingest,/forecasting,/backtesting,/registry,/rag,/agents,/jobsMAY change in MINOR releases. Pin the version. (See.claude/rules/versioning.md.) - Every DB-touching change ships with an Alembic migration. Forward-only after merge.
- Pydantic v2 schema additions: prefer additive; breaking field renames go behind a
feat!:or call out in PR description. - New endpoints must register in
app/main.pyand have a route test in the slice'stests/test_routes.py(per.claude/rules/test-requirements.md).