feat(dashboard): pause/resume/abort buttons on run detail page - #30
Merged
Conversation
Surfaces the engine's run action endpoints in the UI. Buttons render conditionally on final_status: - running → Pause + Abort - paused → Resume + Abort - terminal → no actions Abort uses a native confirm prompt since it's a terminal, irreversible action; pause and resume fire immediately. All three mutations invalidate the run query so polling resumes/stops correctly based on the new status. Closes #29 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Adds run-control actions to the Dashboard run detail page by wiring existing Engine endpoints (POST /runs/{id}/pause|resume|abort) into the frontend API client, React Query mutations, and a conditional action bar UI on /runs/[id].
Changes:
- Added API client methods for abort/pause/resume run endpoints.
- Introduced TanStack Query mutations to call these endpoints and invalidate relevant run queries.
- Rendered Pause/Resume/Abort buttons on the run detail page based on
final_status, with a confirmation prompt for Abort.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
| apps/dashboard/src/lib/api/client.ts | Adds typed client helpers for abortRun, pauseRun, resumeRun. |
| apps/dashboard/src/hooks/api.ts | Adds shared mutation helper + hooks (useAbortRun, usePauseRun, useResumeRun) that invalidate run queries. |
| apps/dashboard/src/app/runs/[id]/page.tsx | Adds conditional action buttons on run detail page and wires them to the new mutations. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
rlagowski
previously approved these changes
Apr 25, 2026
- useRunActionMutation is now generic <T>, preserving the Run return type from the underlying client methods - formatActionError extracts FastAPI's detail field from ApiError so users see "Run is not running (final_status=success)" instead of the generic "API error 409" Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
rlagowski
approved these changes
Apr 25, 2026
rlagowski
added a commit
that referenced
this pull request
May 12, 2026
* docs(workflow): add CONTRIBUTING, PR/issue templates, pre-push hook
Establish gitflow-like branching model:
- main ←── PR ── develop ←── PR ── issue/<nr>-<slug>
- No direct commits to main/develop
- Conventional Commits for all changes
- Squash/rebase merge preferred
Bootstrap note: this single commit goes directly to develop as it
establishes the convention itself; all subsequent work must follow
the issue → branch → PR flow.
Files:
- CONTRIBUTING.md — full workflow documentation
- .github/PULL_REQUEST_TEMPLATE.md
- .github/ISSUE_TEMPLATE/{feature,bug,chore}.md
- .githooks/pre-push — blocks direct push to main/develop
(activate: git config core.hooksPath .githooks — already done
in this clone)
Remote branch protection (GitHub) documented in CONTRIBUTING.md
but requires manual configuration after remote is added.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat: pivot stack from TypeScript/Node to Python + uv (#2)
* feat: pivot stack from TypeScript/Node to Python + uv
Replace TS/Node monorepo with Python 3.13 + uv workspace.
Dashboard remains Next.js (only Node component) — F6.
Motivation:
- LangGraph Python is significantly more mature than LangGraph.js
- Pydantic v2 >> Zod for schema-heavy DAP use case
- AI/ML ecosystem (LangSmith, evaluation libs, vector DB clients)
- Anthropic/OpenAI/Google SDKs treat Python as reference
- uv UX equivalent to npx (uvx dap), with single-binary distribution
- Original DAP docs naturally fit Python
Stack mapping:
- pnpm workspaces → uv workspaces
- TypeScript strict → mypy strict + ruff
- commander.js → Typer + Rich
- Fastify → FastAPI + uvicorn
- Drizzle ORM → SQLAlchemy 2.0 + Alembic
- Zod → Pydantic v2
- execa → anyio + asyncio.subprocess
- @langchain/langgraph → langgraph (Python, F5+)
Workspace members:
- packages/types — Pydantic models (Agent, Pipeline, Run, State, Runtime Protocol)
- packages/runtimes — RuntimeAdapter Protocol + 7 stub adapters
- apps/engine — FastAPI factory, SQLAlchemy ORM (5 tables), SQLite WAL
- apps/cli — Typer CLI: init / start / stop / status / --version
- apps/dashboard — Next.js placeholder (unchanged, F6)
Verified end-to-end:
- uv sync --all-packages — 4 workspace pkgs built, 28 deps installed
- uv run dap --version → 0.0.1
- dap init → .dap/ with config + subdirs
- dap start → uvicorn on 127.0.0.1:7333
- GET /health, GET /runtimes, GET /runtimes/:id/health all OK
- SQLite state.db created with WAL mode
- ruff check + format clean
Closes #1
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix: address Copilot review comments on PR #2
- engine/app.py: restrict CORS allow_origins to localhost dashboard
(http://localhost:7332, http://127.0.0.1:7332) instead of "*",
with explicit allow_methods + allow_headers
- engine/README.md: clarify Alembic migrations are planned for later;
current F0 uses Base.metadata.create_all() (no alembic scaffold yet)
- runtimes/README.md: typo fix (delegata → deleguje)
- engine/persistence/models.py: switch *_at columns from String to
DateTime(timezone=True) to match Pydantic datetime types in dap-types
- pyproject.toml: align ruff target-version (py312 → py313) and
mypy python_version (3.12 → 3.13) with .python-version pin
- runtimes/registry.py: make create_default_registry() sync — no I/O
in adapter constructors; will revisit if plugin loading needs async
- engine/app.py: drop `await` for sync create_default_registry()
- cli/commands/start.py: fix message "--no-headless" → "non-headless mode"
to match actual CLI flag (--headless)
Verified:
- ruff check + format clean
- dap --version, dap init, dap start E2E OK
- CORS now returns access-control-allow-origin: http://localhost:7332
Refs: PR #2
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Rafal Lagowski <rla@Rafals-MacBook-Pro.local>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(ci): gate PRs with ruff + pytest (#4)
Add GitHub Actions workflow that gates every PR to develop/main with:
- uv run ruff check apps packages
- uv run ruff format --check apps packages
- uv run pytest -q
Workflow uses astral-sh/setup-uv@v3 with uv.lock-keyed cache. Concurrency
group cancels in-progress runs when new push lands on the same branch/PR.
Smoke tests (tests/smoke/):
- test_imports.py — verifies all 4 workspace packages import cleanly
and exposes expected public API (Pydantic models, runtime adapters,
registry, CLI Typer commands)
- test_engine_endpoints.py — boots FastAPI app via TestClient (no real
uvicorn start), exercises /health, /runtimes, /runtimes/:id/health
for both available (bash) and missing (claude-code) runtimes plus
404 path
pytest config (pyproject.toml):
- testpaths gains "tests"
- per-file-ignores for tests/**: PLR2004 (magic values), PLC0415
(intentional per-test isolated imports)
Docs:
- README.md: CI badge, CI section, mypy out-of-scope note
- CONTRIBUTING.md: DoD references CI as authoritative gate, local
equivalence note
- ../LOCAL_APP_PLAN.md: F0+ CI bootstrap entry
Verified locally:
- ruff check + format clean
- 9/9 smoke tests passing
- YAML syntax valid
Out of scope (separate issues):
- mypy in CI — codebase needs type cleanup pass first
- "main only from develop" enforcement Action
- dashboard (Next.js) CI — wait for F6 scaffolding
Closes #3
Co-authored-by: Rafal Lagowski <rla@Rafals-MacBook-Pro.local>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore(ci): enforce "main merges only from develop" (#6)
Add GitHub Actions workflow that runs on every pull_request targeting
main and fails if head_ref != develop. GitHub native branch protection
cannot restrict PR source branch — this Action fills that gap and is
configured as a required status check post-merge.
Workflow:
issue branch → PR to develop → PR develop → main (release)
Closes #5
Co-authored-by: Rafal Lagowski <rla@Rafals-MacBook-Pro.local>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore: type cleanup + add mypy to CI (#8)
Fix all 3 mypy errors in F0 codebase and integrate mypy as a blocking
CI step.
Type fixes:
- engine/api/runtimes.py: introduce _registry(request) helper that
type-narrows Starlette app.state.runtime_registry (declared Any)
to RuntimeRegistry, eliminating Any-return error
- tests/smoke/test_engine_endpoints.py: fixture client annotation
Iterator[TestClient] (was TestClient — wrong for yield-based fixture)
- tests/smoke/test_imports.py: iterate over instances instead of types
to avoid mypy narrowing the loop var to type[BaseAdapter] (abstract)
Config:
- pyproject.toml: drop unused [[tool.mypy.overrides]] for langgraph.*
(will add back when langgraph is integrated in F5)
CI:
- .github/workflows/ci.yml: add `uv run mypy apps packages tests` step
between ruff format and pytest
- Job display name kept as "Python (ruff + pytest)" for branch-protection
stability — required-check name unchanged, mypy is part of the same job
Docs:
- README.md: drop F0 mypy out-of-scope caveat, mention enforce-main-source
workflow alongside main CI
- CONTRIBUTING.md: DoD now requires mypy clean
Verified locally:
- ruff check + ruff format clean
- mypy: Success: no issues found in 38 source files
- pytest: 9/9 passed
- YAML syntax valid
Closes #7
Co-authored-by: Rafal Lagowski <rla@Rafals-MacBook-Pro.local>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(cli): F1 — process management (PID file, dap stop, runtime status) (#10)
Replace stub commands with real process lifecycle handling.
New module dap_cli/process.py:
- write_pid_file / read_pid_file / remove_pid_file
PID file is JSON with {pid, port, started_at} at .dap/dap.pid
- is_process_alive: psutil-based liveness check (handles zombies)
- stop_process: SIGTERM, poll for graceful exit (5s default),
SIGKILL fallback. Idempotent on already-dead PIDs.
- install_pid_cleanup_handlers: SIGINT/SIGTERM remove PID file
before re-raising default handler so uvicorn shuts down cleanly.
- uptime_from_started_at: human-readable formatter
dap start:
- Refuses to start if PID file exists and process is alive
- Cleans up stale PID file (process gone) before proceeding
- Writes own PID + port + ISO timestamp to PID file
- Installs signal handlers
- finally-clause removes PID file on uvicorn exit
dap stop:
- Reads PID file, fails cleanly if missing or stale
- Calls stop_process and removes PID file regardless
dap status:
- Three states: not initialized / project but engine stopped / running
- For running: shows PID, port, uptime
- Hits /runtimes + /runtimes/{id}/health (httpx, 2s timeout)
- Renders Rich Table with all 7 runtime adapters and availability ✓/✗
- Cleans up stale PID file when detected
Tests (tests/smoke/test_cli_process.py, 14 cases):
- PID file roundtrip (write → read → remove)
- Edge cases: missing, corrupt, incomplete, missing fields
- is_process_alive: self (✓), nonexistent PID (✗)
- stop_process: dead PID (False), real subprocess (sleep 30) → terminated
- uptime formatter: valid, invalid
Deps:
- apps/cli: psutil>=6.1.0, httpx>=0.27.2 (status hits /runtimes)
- root: types-psutil dev dep for mypy
Manual verification:
$ dap init # → .dap/ created
$ dap status # → engine: stopped
$ dap start & # → PID file written, uvicorn :7333
$ cat .dap/dap.pid # → {pid, port, started_at}
$ dap status # → running + Rich table of 7 adapters
$ dap stop # → SIGTERM → ✓ Stopped, PID file removed
Closes #9
Co-authored-by: Rafal Lagowski <rla@Rafals-MacBook-Pro.local>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(engine): F2 — REST CRUD for agents, pipelines, runs (read-only) (#12)
Schema refactor (immutable versioning):
- agents → agents (stable identity, current_version pointer) +
agent_versions (immutable snapshots, UNIQUE(agent_id, version))
- pipelines → pipelines + pipeline_versions analogically
- AgentVersion includes name; PipelineVersion includes name+description
so version history reflects metadata at save time
- runs/state_snapshots/node_execution_logs unchanged
New module structure:
- api/schemas.py — AgentCreate/Update, PipelineCreate/Update (extra="forbid")
- api/deps.py — get_session (transactional), get_registry
- api/agents.py — CRUD: list, get, create, update, archive, versions
- api/pipelines.py — same shape as agents
- api/runs.py — read-only: list, get, /state, /state/history,
/nodes/{node_id}
- persistence/repository.py — all DB queries; raises NotFoundError; routes
are thin (validate → repo → serialize)
Versioning semantics:
- POST /agents → v1
- PUT /agents/{id} → vN+1, name updated on AgentORM AND mirrored
into the new version row
- GET /agents/{id}/versions/{v} returns Agent with version-snapshot name
and version.created_at as updated_at
- DELETE archives (sets archived_at); list excludes archived by default
- Update on archived agent → 404
Pagination + filters:
- offset/limit (default 0/50, max 500)
- agents: ?role=, ?archived=
- pipelines: ?archived=
- runs: ?pipeline_id=, ?final_status=
Tests (51 total, 33 new):
- tests/smoke/test_crud_agents.py (15) — create/get/update/archive,
versions, validation, extra-field rejection, pagination, filters
- tests/smoke/test_crud_pipelines.py (6) — create/update/archive,
version history, edge condition validation
- tests/smoke/test_crud_runs.py (8) — seed runs directly in DB
(trigger ⇢ F5), exercise all read endpoints incl. 404 paths
Tooling:
- ruff: extend-immutable-calls for FastAPI Depends/Query/Path/Body/...
(eliminates B008 noise — Depends-in-defaults is THE FastAPI idiom)
Verified locally:
- ruff check + format: clean
- mypy: 49 source files, 0 errors
- pytest: 51 passed in 0.4s
- Real engine E2E: POST /agents → 201, GET /agents → total=1, /health → ok
Closes #11
Co-authored-by: Rafal Lagowski <rla@Rafals-MacBook-Pro.local>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(prompt-dsl): F4 — Jinja2 → XML compiler + render-preview endpoint (#14)
New workspace package packages/prompt-dsl/ providing the deterministic
prompt compilation step:
build_prompt(template, context) -> BuildResult(xml, valid, warnings, errors)
Pure function, idempotent. The same input always yields the same output.
Components:
- builder.py — orchestrates render + validate; raises PromptBuildError
on Jinja syntax or undefined-variable failures
- validator.py — defusedxml-backed XML well-formedness + root element check
- context.py — Pydantic helpers for common roles (TestAuthorContext,
ImplementerContext, VerifierContext)
Security:
- Jinja2 SandboxedEnvironment (StrictUndefined) — blocks attribute access
to dunder methods, file I/O, etc. Templates may come from user input
via the dashboard, so we treat them as untrusted.
- defusedxml — protects against XXE, billion laughs, external entities.
- Verified by tests: dunder access raises PromptBuildError; DOCTYPE/XXE
payloads marked invalid in the build result.
Engine integration:
- POST /agents/{id}/render-preview?version=N
body: {context: {...}}
200 response: {rendered_xml, valid, warnings, errors}
Returns 200 with valid=False for malformed XML output (so the dashboard
can show diagnostics); only Jinja-side errors return 422.
Repository:
- get_agent_template(agent_id, version=None) — fetches the prompt_template
string for the requested (or current) version; raises NotFoundError.
Tests (18 new — 11 builder, 6 endpoint, 1 smoke import):
- Happy path (simple, loops, conditionals)
- Undefined variable → 422
- Template syntax error → 422
- Malformed XML → valid=False with errors
- Wrong root → valid=False
- Custom expected_root override
- Sandbox blocks dunder access
- XXE / DOCTYPE blocked
- Pure function idempotency
- Specific-version preview
Tooling:
- types-defusedxml dev dep for mypy
- HTTPException uses literal 422 (avoids deprecation warning for
HTTP_422_UNPROCESSABLE_ENTITY → _CONTENT in newer FastAPI)
Verified locally:
- ruff check + format: clean
- mypy: 55 source files, 0 errors
- pytest: 69 passed (51 prior + 18 new)
- Real E2E: created agent + render-preview returned validated XML
Closes #13
Co-authored-by: Rafal Lagowski <rla@Rafals-MacBook-Pro.local>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(runtimes): F3 — api-call adapter (Anthropic SDK) (#16)
First real runtime adapter — replaces stub with full Anthropic SDK
implementation. Takes a rendered XML prompt (output of dap_prompt_dsl)
and produces text + token usage + USD cost.
Architecture:
- Async (anthropic.AsyncAnthropic) — matches RuntimeAdapter.execute()
protocol; client is per-call and explicitly closed in `finally`.
- XML → system message: the rendered prompt contains role/task/
constraints/output-spec which are instructions, so it goes into
`system`. The single user message is "Execute the task as specified."
Optional `system_prompt` from runtime_config is prepended before XML.
- Single-shot text → text (no streaming, no tools, no vision in F3).
runtime_config schema:
- provider: must be "anthropic" in F3 (openai/google land in F9)
- model_id: required (e.g. "claude-haiku-4-5")
- max_tokens: default 4096
- system_prompt: optional, prepended before XML
- enable_thinking: bool — adaptive thinking (Opus 4.7/4.6 + Sonnet 4.6)
- effort: null | low | medium | high | xhigh | max
- prompt_cache: bool — top-level cache_control: ephemeral
Cost calculation:
- Hardcoded per-model pricing (USD/1M tokens):
- Opus 4.7/4.6/4.5: $5 input / $25 output
- Sonnet 4.6/4.5: $3 input / $15 output
- Haiku 4.5: $1 input / $5 output
- Cache writes 1.25x input rate; cache reads 0.1x.
- Returns None for unknown models (forward-compat).
Error handling — typed exception mapping:
AuthenticationError, PermissionDeniedError, NotFoundError,
BadRequestError, RateLimitError, APITimeoutError,
APIConnectionError, APIStatusError → RuntimeResult(success=False)
with descriptive errors[].
Healthcheck:
No ANTHROPIC_API_KEY → available=False, missing=[...]
With key → available=True, version="anthropic-sdk X.Y.Z"
Tests (17 new in tests/smoke/test_api_call_adapter.py):
- Healthcheck with/without API key
- Config validation: bad provider, missing key, missing model_id,
invalid max_tokens, invalid effort
- Successful execute with mocked AsyncAnthropic
- XML goes into system, default user message in messages
- Optional features: prompt_cache, enable_thinking, effort,
system_prompt prepended
- Cache pricing roundtrip (cache write + cache read + uncached + output)
- Error path mapping: AuthenticationError → "Authentication failed",
RateLimitError → "Rate limited"
- Cost calc unit tests: Haiku, Opus, Sonnet w/ cache, unknown model
Verified locally:
- ruff check + format clean
- mypy: 56 source files, 0 errors
- pytest: 86 passed (69 prior + 17 new)
- Adapter loads, healthcheck behaves correctly with/without env var
Out of scope (future issues):
- bash, http, claude-code, gemini-cli, codex, aider implementations
- Streaming, tool use, batches, files, vision
- OpenAI / Google providers (F9 with CLI adapters)
Closes #15
Co-authored-by: Rafal Lagowski <rla@Rafals-MacBook-Pro.local>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(engine): F5 — LangGraph pipeline execution + POST /runs (#18)
First working end-to-end pipeline execution. Spina F2 (CRUD) + F3
(api-call adapter) + F4 (Prompt Builder) w realnie wykonujący flow:
POST /runs → PipelineRunner.run()
→ LangGraph StateGraph(PipelineState) (Pydantic state)
→ for each node:
render prompt (F4) → call adapter (F3) → save snapshot + log
→ conditional edges via EdgeCondition
→ final state + final_status
→ 201 Created with full Run
New module dap_engine.execution:
- conditions.py — evaluate_condition(EdgeCondition, PipelineState) -> bool
Pure function, supports ==/!=/</<=/>/>= + and/or with rekurencyjnym
nesting. Unknown fields → False, type mismatches → False (no crash).
- node_executor.py — make_node_fn(NodeContext) factory. Each node:
1. Renders prompt template with state as Jinja context (F4)
2. Builds RuntimeTask, calls adapter (F3)
3. Saves NodeExecutionLog (status, tokens, cost, prompt_xml, output)
4. Saves StateSnapshot
5. Returns state diff: {} on success unless adapter's RuntimeResult.structured
contains keys matching PipelineState fields (then merged); on error
returns {"final_status": "failed", "verification_reason": ...}.
- runner.py — PipelineRunner.run() builds StateGraph from pipeline JSON
(loads agent versions, wires nodes + edges incl. conditional), invokes
via LangGraph's ainvoke, returns final PipelineState. recursion_limit=50
safeguard against infinite retry loops.
Repository:
- create_run — insert Run row in 'running' state
- finalize_run — set final_status, ended_at, aggregate tokens + cost
from node logs
Endpoint:
- POST /runs (status 201) — synchronous trigger.
Body: {pipeline_id, pipeline_version?, initial_state}
Validates pipeline + version exist (404), initial_state schema (422),
builds initial PipelineState (defaults + overrides), creates Run,
invokes runner, finalizes Run, returns full Run object.
Errors: RunnerError → 500 with detail.
Tests (19 new in tests/smoke/):
- test_conditions.py (10) — eq/neq/lt/gte, unknown field, logical and/or,
nested AND/OR, type mismatch fallback
- test_pipeline_runner.py (4) — single-node, two-node linear,
conditional edge picks END based on stub structured output,
unknown-agent → RunnerError
- test_runs_trigger.py (5) — happy E2E, 404 unknown pipeline,
422 extra field, 422 invalid initial_state, specific version pin
Verified locally:
- ruff check + format clean
- mypy: 63 source files, 0 errors
- pytest: 105 passed (86 prior + 19 new)
- Real engine E2E: agent + pipeline + POST /runs → adapter called via
LangGraph (bash stub fails as expected — proves flow reaches runner)
Out of scope (osobne issues):
- Background execution (POST /runs async, returns 202)
- Lifecycle endpoints: pause/resume/abort/retry-node/skip-node
- Per-role output parsing (e.g. test_author → state.test_files);
for now adapters opt in via RuntimeResult.structured matching state fields
- LangGraph SqliteSaver checkpointing — używamy własnego state_snapshots
- Streaming events to dashboard (F6)
- DAG validation endpoint (POST /pipelines/:id/validate)
Closes #17
Co-authored-by: Rafal Lagowski <rla@Rafals-MacBook-Pro.local>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(dashboard): F6 — Dashboard MVP (Next.js 15 + React Flow) (#20)
First frontend code in the project. Replaces the placeholder package
with a working Next.js 15 dashboard that consumes the engine's REST API
(F2/F4/F5) — gives visual access to runs and agents.
Stack:
- Next.js 15 (App Router) + TypeScript strict
- Tailwind CSS v3 + shadcn/ui (slate base color, new-york style)
- @xyflow/react 12 — read-only pipeline graph
- @tanstack/react-query 5 — polling 2s for runs list/detail; stops when
no running runs left
- react-hook-form + zod — agent create form
- Hand-rolled types (lib/api/types.ts) mirroring dap_types Pydantic
models; `pnpm gen:api` regenerates from openapi-typescript when engine
is running
Routes:
- / → redirect to /runs
- /runs → table of runs with polling (status, tokens, cost, duration)
- /runs/:id → React Flow graph + node-click drawer with state diff,
prompt XML, stdout, structured output, error message,
tokens/cost/duration metrics
- /agents → table of agents (name, role, runtime, version)
- /agents/new → form with Zod validation; POST /agents on submit
UI primitives (hand-rolled subset of shadcn/ui — pre-built CLI not used
to keep the diff transparent):
- Button (cva variants), Badge (with status variants), Card,
Input, Label, Textarea, Dialog (Radix-based, side-drawer style)
- StatusBadge components for run + node status mappings
- Sidebar with active-route highlight
API client (lib/api/client.ts):
- Typed fetch wrapper with ApiError for non-2xx
- listRuns/getRun/getRunState/getRunStateHistory/getRunNodeLog/triggerRun
- listPipelines/getPipeline/getPipelineVersion
- listAgents/getAgent/createAgent
- Reads NEXT_PUBLIC_DAP_ENGINE_URL (default http://127.0.0.1:7333)
CI:
- New `Dashboard (typecheck + build)` job in .github/workflows/ci.yml
- pnpm/action-setup@v4, actions/setup-node@v4 with Node 22
- pnpm install --frozen-lockfile, pnpm typecheck, pnpm build
- Existing Python job unchanged
Verified locally:
- pnpm install (185 packages, 7s)
- pnpm typecheck → clean
- pnpm build → 5 routes generated, 105kB shared JS, /runs/:id has
React Flow as dynamic route (ƒ)
- Python tests still 105/105 passing (no Python changes)
Out of scope (separate issues):
- ESLint setup — Next.js build skips ESLint via next.config flag
- Pipeline Designer (F7) — visual DAG editor
- Pipelines list/detail UI — F6 only ships runs + agents views
- Edit/delete agents
- Auth (Entra ID, NextAuth)
- Settings UI, dark mode polish
- Background runs UI (when POST /runs goes async + lifecycle endpoints)
- Real syntax highlighting in XML viewer (Shiki)
- JSON diff library (currently raw JSON)
- Playwright E2E tests
Closes #19
Co-authored-by: Rafal Lagowski <rla@Rafals-MacBook-Pro.local>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(engine): DAG validation endpoint (POST /pipelines/validate) (#22)
Standalone validator that Pipeline Designer (F7) calls before save.
Catches structural and reference errors that would otherwise fail at
runtime (or surface as confusing 500s).
New module dap_engine.execution.validator:
- validate_pipeline_dag(payload, session) → ValidationResult
- ValidationResult: {valid: bool, errors: list[str], warnings: list[str]}
- Pure function broken into per-rule helpers (one branch each)
Validation rules:
Errors (block save):
- Duplicate node ids / edge ids
- entry_point doesn't match any node.id
- Edge source/target references unknown node (sentinels __start__/__end__ allowed)
- Agent not found in DB or archived
- Node unreachable from START (BFS)
- Node has no path to END (BFS on reverse adjacency)
- Multiple unconditional edges from same source — ambiguous routing
Warnings (inform but allow save):
- ComparisonCondition.field not in PipelineState model fields
- Recursively walks LogicalCondition.children (and/or)
Endpoint:
- POST /pipelines/validate (200) — body: PipelineCreate, response: ValidationResult
- 422 only for Pydantic-level malformed body
- 200 with valid=false for semantic errors (so the designer can render
errors inline)
Tests (13 new):
- valid pipeline (no errors, no warnings)
- unknown agent → error
- archived agent → error
- entry_point missing → error
- duplicate node ids → error
- unknown edge target → error
- unreachable node → error
- node with no path to END → error
- ambiguous routing → error
- unknown condition field → warning (still valid)
- known condition field (tests_passed) → no warning
- nested LogicalCondition with unknown field → warning
- malformed body → 422
Verified locally:
- ruff check + format clean (refactored 20-branch function into per-rule helpers)
- mypy: 65 source files, 0 errors
- pytest: 118 passed (105 prior + 13 new)
Out of scope (separate issues):
- state_schema_ref version registry (no central schema versioning yet)
- Cycle detection beyond what runtime recursion_limit catches
- Agent runtime_id existence check (would couple validator to RuntimeRegistry)
Closes #21
Co-authored-by: Rafal Lagowski <rla@Rafals-MacBook-Pro.local>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(dashboard): F7 — Pipeline Designer (visual DAG editor) (#24)
First user-facing pipeline authoring UI. DAP transitions from "API +
viewer" to "real authoring tool" — users can now create and edit
pipelines through the dashboard instead of curl.
Routes:
- /pipelines — list (table with edit links + new button)
- /pipelines/new — blank designer
- /pipelines/[id]/edit — load existing → save creates new version
Designer layout (apps/dashboard/src/components/designer/):
- toolbar.tsx — top bar: name/description inputs, Validate, Save,
inline validation result with errors+warnings
- agent-palette.tsx — left sidebar with agents from registry; click an
agent to add as a node at offset position
- designer.tsx — main component, wraps React Flow edit mode with
useNodesState/useEdgesState; tracks edge metadata
(condition, label) separately since RF doesn't
model these natively
- inspector.tsx — right sidebar for selected node or edge; shows
agent details for nodes, condition builder for
edges; Set as entry point + Delete actions
- condition-builder.tsx — recursive form for ComparisonCondition and
LogicalCondition (AND/OR) with type-aware value
inputs (bool/num/str/null) and nested children
API client extensions:
- createPipeline / updatePipeline / validatePipeline
- useCreatePipeline / useUpdatePipeline / useValidatePipeline hooks
- usePipelinesList hook
Validation:
- Click "Validate" → POST /pipelines/validate → render errors+warnings
- Save button disabled when name is empty or last validation result was
invalid; validation is opt-in to avoid spamming the engine on each
keystroke
- Server-side validation is the source of truth (catches references,
reachability, ambiguous routing — see #21)
Save flow:
- New pipeline: POST /pipelines → redirect to /pipelines/[id]/edit
(so subsequent saves create new versions of the same pipeline)
- Existing: PUT /pipelines/{id} → creates v(N+1)
Sidebar:
- Added Pipelines nav link (GitBranch icon between Runs and Agents)
Verified locally:
- pnpm typecheck → clean
- pnpm build → 8 routes generated (added /pipelines, /pipelines/new,
/pipelines/[id]/edit); /pipelines/[id]/edit is dynamic (loads pipeline)
- Python tests still 118/118 passing (no Python changes)
Out of scope (separate issues):
- Drag-drop from agent palette (only click-to-add for MVP)
- Per-node runtime_config overrides (uses agent defaults)
- Versions sidebar / browse history (saves create versions but no UI)
- Undo/redo
- Auto-layout (manual positioning, default offsets)
- Diff view between versions
- Real-time collaboration
Closes #23
Co-authored-by: Rafal Lagowski <rla@Rafals-MacBook-Pro.local>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(engine): background runs + abort + graceful shutdown (#26)
Removes the single largest blocker for real-world use: synchronous
POST /runs blocked HTTP requests for the entire pipeline duration.
Real LLM calls take 30s-5min per node, multi-step pipelines easily
run 5-30min — HTTP requests timed out and the dashboard saw "loading…"
forever. After this PR, POST /runs returns immediately and execution
happens in a background asyncio.Task tracked by RunRegistry.
Architecture:
- New `dap_engine.execution.RunRegistry` — in-memory map run_id → Task,
with auto-cleanup on completion. Used for abort + graceful shutdown.
Process-local (restart forgets); persistent state lives in Run rows.
- `POST /runs` refactored to async:
- Validates pipeline + version (sync, blocks)
- Creates Run row + commits (sync, blocks)
- Spawns asyncio.create_task(_execute_run_background(...))
- Returns 201 with Run (final_status="running") immediately
- Background task uses fresh DB session from session_factory,
runs LangGraph, finalizes Run row in its own transaction
- On CancelledError → finalize as "aborted" in fresh session
- On unexpected exception → finalize as "failed" in fresh session
- `POST /runs/{id}/abort`:
- 404 if run doesn't exist
- 409 if run is not in "running" state (already finalized)
- Otherwise cancels the asyncio.Task; background's CancelledError
handler finalizes as "aborted"
- Defensive: if abort races with natural completion, marks as
aborted directly
- Engine lifespan teardown:
- run_registry.shutdown(timeout=5s) cancels all running tasks
- Surviving runs marked as "aborted" in DB
- Prevents orphan asyncio tasks at engine stop
- Engine lifespan startup:
- mark_stale_running_runs_as_failed() finds Run rows in "running"
state from crashed engines and marks them as "failed" with a
synthetic snapshot recording the reason
- Without this, restart leaves ghost runs forever in "running"
New deps wiring:
- get_run_registry / get_session_factory in api/deps.py
- App.state.run_registry exposed via lifespan
Tests (5 new in tests/smoke/test_run_lifecycle.py):
- POST /runs returns in <500ms even with 2s slow adapter (proves async)
- abort_running_run: triggers run with slow adapter, aborts mid-flight,
verifies final_status="aborted"
- abort_already_completed → 409
- abort_unknown_run → 404
- Stale-on-startup recovery: write a "running" Run, restart app,
verify it was reaped to "failed"
Updated existing test (test_runs_trigger.py):
- Replaced direct final_status="success" assertion with poll-until-
complete helper since POST is now async
Verified locally:
- ruff check + format clean
- mypy: 67 source files, 0 errors
- pytest: 123 passed (118 prior + 5 new)
- 1 pre-existing test updated for async semantics
Out of scope (separate issues):
- POST /runs/{id}/pause + resume — requires LangGraph SqliteSaver
checkpointing for state rollback
- POST /runs/{id}/nodes/{node}/retry — requires state snapshot semantics
- POST /runs/{id}/nodes/{node}/skip — requires manual graph traversal
- Run timeout (max duration before auto-abort)
- Concurrent run limits (single-user local doesn't need)
- Persistent task state across engine restarts (currently runs lost
when engine stops mid-pipeline)
Closes #25
Co-authored-by: Rafal Lagowski <rla@Rafals-MacBook-Pro.local>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(engine): pause/resume runs via LangGraph SqliteSaver (#28)
* feat(engine): pause/resume runs via LangGraph SqliteSaver checkpointing
Adds POST /runs/{id}/pause and POST /runs/{id}/resume backed by an
AsyncSqliteSaver wired into PipelineRunner. Pause cancels the background
task between node boundaries; the checkpoint preserves graph state so
resume picks up from the last completed node without re-running the
finished prefix.
- AsyncSqliteSaver lives in <db>.checkpoints.db, managed via the lifespan
AsyncExitStack so connections close cleanly on shutdown
- RunRegistry gains pause()/was_paused() so the background task can
distinguish a pause cancellation from an abort and finalize accordingly
- pause_run / resume_run repo helpers — paused runs keep ended_at=null
so they're not counted as completed
- abort now accepts paused runs as a terminal exit path
Closes #27
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(engine): address PR review — stale reads + duplicate aggregation
- abort/pause endpoints now expire the request session before re-reading
the run, so the response reflects the finalization performed in the
background task's separate session (expire_on_commit=False meant we
were returning stale RunORM data)
- Extract metric aggregation into _aggregate_run_metrics() using SQL
SUM, eliminating the in-memory log scan duplicated across finalize_run
and pause_run
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Rafal Lagowski <rla@Rafals-MacBook-Pro.local>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(dashboard): pause/resume/abort buttons on run detail page (#30)
* feat(dashboard): pause/resume/abort buttons on run detail page
Surfaces the engine's run action endpoints in the UI. Buttons render
conditionally on final_status:
- running → Pause + Abort
- paused → Resume + Abort
- terminal → no actions
Abort uses a native confirm prompt since it's a terminal, irreversible
action; pause and resume fire immediately. All three mutations
invalidate the run query so polling resumes/stops correctly based on
the new status.
Closes #29
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(dashboard): address PR review — typed mutation + readable errors
- useRunActionMutation is now generic <T>, preserving the Run return
type from the underlying client methods
- formatActionError extracts FastAPI's detail field from ApiError so
users see "Run is not running (final_status=success)" instead of
the generic "API error 409"
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Rafal Lagowski <rla@Rafals-MacBook-Pro.local>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(dashboard): trigger runs from the dashboard (#39)
* feat(dashboard): trigger runs from the dashboard
Surfaces POST /runs in the UI — users no longer need curl/CLI to start
a pipeline. Run buttons appear in two places:
- Pipelines list — quick-run on each row (default current version)
- Designer toolbar — only when editing a saved pipeline
Both open the same TriggerRunDialog, which lets the user:
- Pick a specific pipeline_version (defaults to current)
- Optionally supply initial_state as JSON (validated client-side)
- Surfaces FastAPI's detail field on errors via formatApiError()
On success the dialog closes and routes to /runs/{id} so the user
lands on the live run view immediately.
formatApiError was promoted from the run detail page to lib/api/client
so the trigger dialog can reuse it.
Closes #31
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(dashboard): address PR review on trigger-run dialog
- formatApiError now handles plain-string detail (non-JSON responses
with statusText) and {message} payloads from non-FastAPI servers
- usePipelineVersions accepts an `enabled` option; the trigger dialog
defers the fetch until it's opened, so the pipelines list page no
longer fires N versions queries on first render
- Dialog clears parseError + resets the trigger mutation when closing,
so reopening after a failed submit doesn't show stale errors
- Validate parsed pipeline_version with Number.isFinite() before
including it in the request — guards against non-numeric option values
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Rafal Lagowski <rla@Rafals-MacBook-Pro.local>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(runtimes): bash adapter — shell command execution (#40)
* feat(runtimes): bash adapter — shell command execution
Implements the bash runtime so pipelines can mix LLM steps with
deterministic shell tooling (pytest, git, build scripts). Was a stub
since F0; this fills it in.
- runtime_config.command (preferred) or <command> tag inside prompt_xml
- runtime_config.shell override (default /bin/bash); honors task.working_directory
- task.timeout_ms enforced via asyncio.wait_for; SIGKILL on overrun
- structured = { exit_code, stderr, command, shell, timed_out } so
PipelineState can branch on the result
- Optional runtime_config.env merged onto the subprocess environment
- README: documents the runtime + the v0.1 local-trust security model
(no sandbox; multi-user requires #38 + isolation layer)
Closes #32
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(runtimes): address PR review on bash adapter
- Drop unused DEFAULT_TIMEOUT_MS constant
- _resolve_shell() helper applies documented precedence consistently in
both healthcheck() and execute(): runtime_config.shell → DAP_BASH_SHELL
→ /bin/bash. Previously execute() ignored the env var
- Validate runtime_config.env strictly: non-dict or non-string entries
return a descriptive error instead of being silently dropped
- Subprocess starts in a new POSIX session (start_new_session=True) so
timeout/cancel can SIGKILL the entire process group, not just the shell
- asyncio.CancelledError now caught around communicate(): the engine's
abort/pause cancellation triggers a process-tree teardown before
re-raising, so subprocesses don't leak
- _make_structured() guarantees a consistent shape across success,
failure, timeout, and missing-command paths so downstream nodes can
reliably branch on result.structured
- Test fixture monkeypatches DAP_BASH_SHELL out so tests are hermetic
regardless of the developer's shell setup; new tests cover env-var
precedence, env validation, cancellation, and structured-shape stability
- README example wraps the await in asyncio.run(main()) — the previous
top-level await wasn't valid Python
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Rafal Lagowski <rla@Rafals-MacBook-Pro.local>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(engine): retry-node + skip-node endpoints (#41)
* feat(engine): retry-node + skip-node endpoints via LangGraph state rewind
Closes the per-run control loop started in #28 (pause/resume) and #26
(abort). A failed or paused run can now be recovered without restarting
from scratch:
- POST /runs/{id}/nodes/{node_id}/retry — rewinds to the checkpoint
where the named node was staged as next, branches a new tip via
graph.aupdate_state, and resumes; the node executes again. Useful
for transient failures (rate limits, flakes).
- POST /runs/{id}/nodes/{node_id}/skip — same rewind, but applies an
empty-values update with as_node=node_id so the node appears to have
completed; resume proceeds to its downstream successors. Useful when
a node is broken and the rest of the pipeline can still produce a
useful outcome.
Both require the run to be paused or failed (409 otherwise) and the
named node to exist in the pipeline version (404 otherwise). Reuses
the same RunRegistry slot so a fresh background task is the only thing
that runs concurrently for the run_id.
- PipelineRunner.rewind_and_run() — uses graph.aget_state_history to
locate the target checkpoint, then aupdate_state + ainvoke(None)
- CheckpointNotFoundError when no historical checkpoint had the target
node staged as next (e.g. graph never reached it)
- repo.revive_run() — accepts paused or failed (resume_run only handles
paused) so retry/skip can put a terminated run back into motion
Closes #33
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* style: ruff format
---------
Co-authored-by: Rafal Lagowski <rla@Rafals-MacBook-Pro.local>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore(dashboard): ESLint flat config + lint gate in CI (#42)
* chore(dashboard): ESLint flat config + lint gate in CI
Adds the ESLint setup that #29 / #31 already presumed existed.
- eslint.config.mjs (flat config) extending next/core-web-vitals +
next/typescript via FlatCompat; ignores .next, dist, and the
generated openapi types
- pnpm lint script wired into the Dashboard CI step between typecheck
and build, so PRs are gated on lint cleanliness
- eslint-config-next pinned to 15.x — 16.x has a circular-structure
crash through @eslint/eslintrc that blocked the run
- Existing fixes from the first lint pass:
- Replaced two empty marker interfaces with type aliases
(DesignerNode/DesignerEdge) — same shape, no rule trip
- Converted require("tailwindcss-animate") → ESM import in
tailwind.config.ts to satisfy no-require-imports
Prettier deferred — the next/typescript config already enforces the
style points that matter; adding Prettier would mean another config
plus a format pass across the codebase for low marginal value.
Closes #34
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(dashboard): pin new ESLint dev deps to exact versions
Matches the existing convention in this package.json (every other
devDep pinned). Caret ranges were a slip when running `pnpm add`.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Rafal Lagowski <rla@Rafals-MacBook-Pro.local>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* docs: README quickstart + architecture + runtimes guide (#43)
The previous top-level README mixed phase-by-phase status (F0..F12,
much of it stale), implementation details, endpoint listings, and
quickstart instructions in one ~310-line file. New contributors had no
clean entry point.
Replaces it with a tighter, accurate set:
- README.md — what DAP is, quickstart (clone → uv sync → engine →
dashboard → first agent/pipeline/run), repository layout, common
commands, the five architectural invariants, and links to the rest
- docs/architecture.md — components diagram, package responsibilities,
PipelineState schema (with the field categories), Run lifecycle with
pause/resume/retry/skip transitions, LangGraph checkpoint model and
how rewind_and_run uses it, two-database persistence layout
- docs/runtimes.md — the RuntimeAdapter Protocol explained, a working
skeleton, registration steps, the contract between adapter and
engine ("what the engine does for you" / "what you must do"),
healthcheck guidance, testing patterns
Also fixes the broken `../LOCAL_APP_PLAN.md` and `../DOCUMENTATION.md`
links in the old README (those files don't exist anywhere in the repo).
Closes #35
Co-authored-by: Rafal Lagowski <rla@Rafals-MacBook-Pro.local>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(engine): per-role output parsing for structured node results (#44)
* feat(engine): per-role output parsing for structured node results
Today the runner only writes adapter-supplied `RuntimeResult.structured`
fields into `PipelineState`, which is fine for telemetry-style runtimes
(bash exit_code, api-call usage) but doesn't help LLM agents whose
intentional answer lives in `RuntimeResult.output` as plain text.
This change adds a per-role contract for the four well-known roles
(`task_selector`, `test_author`, `implementer`, `verifier`):
- `dap_types.role_outputs` declares the field allow-list per role and
builds (cached) Pydantic validators that re-use the type annotations
from PipelineState. Single source of truth — no duplicated schemas.
- `dap_engine.execution.output_parser.parse_node_output(role, text)`:
1. extracts JSON from `<output>...</output>`, then a ```json fence,
then a bare `{...}` payload (canonical → permissive),
2. validates against the role schema (extra="forbid", reject foreign
keys, type-check via Pydantic),
3. returns a `ParseResult` with parsed fields + errors.
- `node_executor` now combines both paths on success: structured first
(telemetry), then per-role parsed output (intentional response wins
on conflict). Parse failures are logged and dropped — they don't
fail the run, so #33's retry-node / skip-node remain the recovery
primitives.
- Roles without a declared schema (custom or unknown) are skipped,
preserving the existing permissive-merge behaviour.
15 unit tests cover all four roles, both wrappers (XML tag and
markdown fence), plus the failure modes: foreign keys, type mismatches,
invalid Literal values, missing payload, malformed JSON, non-object
payloads. Integration test added in `test_pipeline_runner` proves
the parsed diff actually reaches state through the runner.
Closes #37
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(engine): missing-payload error names all accepted JSON formats
The parser also accepts a bare `{...}` JSON object as a fallback; the
error message only mentioned the XML tag and markdown fence, which
misled prompt authors troubleshooting a parse miss.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Rafal Lagowski <rla@Rafals-MacBook-Pro.local>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(dashboard): F8 — agent detail / edit / archive + version history (#45)
* feat(dashboard): F8 — agent detail / edit / archive + version history
Closes the dashboard CRUD loop for agents. Today the dashboard could
only list and create them; everything else needed curl.
- /agents/[id] — detail view with current-version metadata, prompt
template, runtime config, plus a collapsible version-history list.
Each previous version expands to show its full prompt + runtime
config so users can compare visually (a proper text diff view is a
separate follow-up — picking a diff library is its own decision).
- /agents/[id]/edit — pre-filled form that bumps the agent to a new
version on save. Role is locked because the engine treats it as
immutable per agent identity.
- Edit + Archive buttons on the /agents list and the detail header,
with a confirm prompt before archive (terminal-ish action — the
agent stays referenced by past pipeline versions).
Refactor:
- Extracted `<AgentForm>` from the create page so /new and /[id]/edit
share validation + Jinja-XML refinement + locked-fields support.
- Errors throughout use the existing `formatApiError` helper so
FastAPI `detail` payloads come through verbatim.
Closes #36
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(dashboard): address PR review on agent registry UI
- AgentUpdate now requires every full-replacement field except `name`
(runtime_config, input_schema, output_schema, constraints,
budget_limit_usd, timeout_ms). The engine applies Pydantic defaults
for omitted fields, so partial payloads silently reset stored
values; making the TS type strict forces callers to consciously
forward each one. The edit page already does this — type tightening
surfaces any future caller that would forget.
- AgentForm.handleSubmit now wraps the await in try/catch and swallows
rejection. The parent owns error display via `submitError`; without
the catch the rejected mutateAsync promise would also surface as
an unhandled rejection.
- /agents/[id] VersionHistory gates on the versions query's loading
and error states before rendering "No previous versions yet" — the
empty-array path was hit briefly during initial load and on errors,
with no error state shown.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Rafal Lagowski <rla@Rafals-MacBook-Pro.local>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix: CORS for dashboard :3000 + designer surface server errors inline (#70)
* fix: CORS for dashboard :3000 + designer surface server errors inline
Two unrelated bugs surfaced while actually using the system:
1. **CORS for the dev dashboard.** The CORS allowlist in `app.py` only
listed `localhost:7332` (a leftover port from the pre-pivot plan),
so the Next.js dev server on `localhost:3000` (the default) hit
"Failed to fetch" for every API call. Allowlist now covers both
:3000 and :7332.
2. **Designer mutations now surface 422 errors inline.** `handleValidate`
and `handleSave` in the pipeline designer awaited `mutateAsync`
without try/catch — server-side validation errors (422) bubbled
as unhandled rejections, popping the Next.js dev overlay and
leaving the user with no actionable message. Wrapping in try/catch
plus rendering `submitError` (formatted via the existing
`formatApiError` helper) in the toolbar means the actual FastAPI
`detail` text shows below the Save / Validate buttons.
The same pattern was already applied to `<AgentForm>` in #45 — this
copies it to the designer.
Discovered while walking through the seeded demo pipeline; no related
issue, just a usability blocker. ESLint, typecheck, build, ruff, mypy,
pytest (172) all clean.
* fix(dashboard): clear validationResult before re-validating
Without this, a previously successful validate result stayed on screen
when a follow-up validate (after pipeline edits) failed — the toolbar
kept showing "Pipeline is valid" and Save remained enabled despite the
new validation never landing. Now we reset to null at the start of
handleValidate so the UI is consistent with the latest attempt.
Addresses PR review on #70.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Rafal Lagowski <rla@Rafals-MacBook-Pro.local>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(runtimes): api-call → multi-provider (Anthropic + OpenAI + OpenAI-compat + Gemini) (#71)
* feat(runtimes): api-call → multi-provider (Anthropic + OpenAI + OpenAI-compat + Gemini)
Refactors the api-call adapter from Anthropic-only to a thin dispatcher
over per-provider modules. Adding a new provider = drop a new module
under `_providers/` and register it in `PROVIDERS`; api_call.py is
unchanged.
## Providers
- `anthropic` (default — backward compatible) — Anthropic SDK, full
Claude 4.x family with cache-write / cache-read multipliers.
- `openai` — OpenAI SDK with default base URL (gpt-5, o-series).
- `openai-compat` — OpenAI SDK with a per-agent `base_url` and
`api_key_env`. Covers GLM (z.ai), Together, OpenRouter, llama.cpp
servers and any Chat Completions-compatible endpoint.
- `gemini` — Google Gen AI SDK (Gemini 2.x / 3.x).
## Architecture
- `_providers/_base.py` — `ProviderResult` dataclass + `ProviderError`,
shared by all provider modules. Lives in its own file so providers
can import without going through `__init__.py` (which depends on the
providers to build the registry — circular smell otherwise).
- `_providers/__init__.py` — `PROVIDERS` registry + `get_provider()` /
`list_provider_ids()`. Re-exports `ProviderResult`/`ProviderError`.
- Each provider module exposes the same surface:
`ID`, `DEFAULT_ENV_VAR`, `validate_config`, `env_var_for`, `call`,
`healthcheck`. Provider-specific pricing tables live next to the
call site, not duplicated centrally.
- `api_call.py` is now a thin dispatcher: read `runtime_config.provider`,
look up the module, validate, call, normalise to RuntimeResult.
## Behaviour
- Healthcheck reports `available=true` if at least one provider is
configured, with `missing=[...]` listing the unconfigured ones —
gives operators a clear list of env vars to set.
- Cost tracking: full pricing for native providers (anthropic / openai /
gemini); `None` for `openai-compat` because we don't know third-party
prices.
- Default provider is `anthropic` so existing agents without an explicit
`runtime_config.provider` keep working unchanged.
## Tests
- `tests/smoke/test_api_call_adapter.py` updated for the dispatcher
(mock target moved to `_providers._anthropic.AsyncAnthropic`,
unsupported-provider test reframed for the new "Unknown provider"
error path) — 18 tests pass.
- New `tests/smoke/test_provider_openai.py` (10 tests) — native OpenAI
+ openai-compat covering GLM-style usage (custom env, base_url),
validation errors, cost calc.
- New `tests/smoke/test_provider_gemini.py` (7 tests) — happy path,
request-shape verification (XML as system_instruction), validation
errors, cost calc.
- All 190 smoke tests pass; ruff + mypy + format clean.
## Deps
Adds `openai>=1.55.0` and `google-genai>=0.3.0` to `dap-runtimes`.
Both are imported only inside their provider modules — if a user never
selects those providers, the SDKs are loaded lazily anyway.
Closes #50
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(runtimes): address PR review on multi-provider api-call
Three trafne komentarze z PR #71:
1. **Lazy-loaded registry.** ``_providers/__init__.py`` no longer
eagerly imports the provider modules — it stores ``ProviderInfo``
metadata (id, module_path, default_env_var, display_name) and
``get_provider()`` does ``importlib.import_module`` on demand. So
``import dap_runtimes`` no longer pulls anthropic + openai +
google-genai SDKs. Adapter healthcheck reads registry metadata
only — no SDK imports.
2. **OpenAI api_key_env restricted to compat.** Native ``provider=
"openai"`` no longer honours ``runtime_config.api_key_env`` in
``env_var_for()``: ``_make_client`` ignores the field for the
native path so honoring it would let validation pass while the
actual call falls back to ``OPENAI_API_KEY`` — silently
inconsistent. Now only the ``openai-compat`` path respects it.
3. **Gemini honors system_prompt.** ``_build_request_kwargs`` now
prepends ``runtime_config.system_prompt`` to ``prompt_xml`` before
building ``system_instruction``, matching Anthropic and OpenAI
behaviour. Previously dropped silently.
Tests:
- New ``test_provider_registry_holds_only_metadata`` — guards against
regressions where someone re-introduces eager SDK imports in the
registry. Checks the data shape (module_path is a string), so it's
hermetic and doesn't poke at sys.modules.
- All 191 smoke tests pass; ruff + mypy + format clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Rafal Lagowski <rla@Rafals-MacBook-Pro.local>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(dashboard): AgentForm — runtime-specific config fields (#72)
* feat(dashboard): AgentForm — runtime-specific config fields
Closes the JSON-blind UI gap from #57. Before this, the form collected
name/role/runtime_id/prompt_template only; runtime_config was invisible.
Result: a freshly-created api-call agent had `runtime_config={}`, hit
the engine, and failed with 422 "model_id is required" — user had no
way from the UI to know what was missing.
## Architecture
- `runtime-config-schemas.ts` — typed metadata describing each runtime's
expected runtime_config shape (api-call, bash, claude-code, gemini-cli,
codex, http, aider). Each field declares kind (text/number/boolean/
select/json/kv), required flag, default, optional `visible` predicate
for conditional fields. Single source of truth for what the form asks
for; mirrors the engine's per-adapter validate_config.
- `runtime-config-editor.tsx` — schema-driven component:
- Renders typed inputs per field (Input, Textarea, native select,
checkbox, key-value editor for env-var dicts).
- "Advanced (raw JSON)" toggle as escape hatch for power users and
runtimes without a schema (also the fallback for unknown runtimes).
- Surfaces required-field errors inline via validateRuntimeConfig().
- Conditional fields: api-call's `base_url` and `api_key_env` only
appear when provider="openai-compat"; `effort`/`prompt_cache`/
`enable_thinking` only for anthropic; `temperature` for openai/
openai-compat/gemini; `thinking_budget` for gemini.
- AgentForm wires runtime_config into the form state. Switching runtime
resets to the new runtime's defaults. On submit, pruneRuntimeConfig()
drops keys not in the schema so stale fields from a previous selection
don't leak into the payload.
## Behaviour
- New agent: select runtime → required fields are pre-filled with
declared defaults (provider="anthropic", max_tokens=4096, etc.).
- Edit agent: form pre-fills runtime_config from the loaded agent;
user edits land in the PUT payload directly. Previously the edit
page forwarded agent.runtime_config unchanged because the form
had no UI for it — that drop is now fixed.
- Required fields surface as inline errors before the engine sees them.
- Empty optional fields are stripped on submit (no `""` clutter in
runtime_config).
## End-to-end verified
- POST /agents with provider="anthropic" + effort/prompt_cache
- POST /agents with provider="openai-compat" + base_url + api_key_env
(the GLM/DeveloperJr shape)
Both round-trip cleanly. tsc, lint, build, ruff, mypy, pytest (191/191) all clean.
Closes #57
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(dashboard): address PR review on runtime config editor
Six trafne komentarze z PR #72 — wszystkie zaadresowane:
1. **Advanced JSON validity propagated up.** ``RuntimeConfigEditor`` now
takes ``onValidityChange`` so AgentForm can gate submit. The Advanced
editor reports ``false`` while ``parseError`` is set; submit button
is disabled with an inline message until JSON parses cleanly.
2. **JsonFieldInput owns its own text state.** Previously the catch
path stashed unparseable raw text into ``runtime_config`` and the
next render did ``JSON.stringify(string)``, escaping/quoting what
the user typed. Now per-field json fields keep a local ``text``
state, only write parsed objects up. Errors propagate to the form
via the same ``onValidityChange`` channel.
3. **KeyValueEditor as array-of-rows internally.** Renaming a key no
longer silently overwrites a colliding row — internal state is
``Array<{key, value}>``, conversion to dict happens at the boundary,
and duplicate keys get an inline warning instead of silent data
loss.
4. **Submit gates on JSON validity AND required fields.** AgentForm
now blocks submit when ``runtimeConfigJsonValid === false`` (any
parse error) in addition to the existing required-field check.
5. **Schemas claim updated for stub runtimes.** Header comment now
distinguishes implemented (api-call, bash) from stub (claude-code,
gemini-cli, codex, http, aider) runtimes. Stub runtimes' fields
are no longer marked ``required`` — the engine can't validate
them today and shouldn't block agent creation. Each stub schema
has a description noting which v0.4 issue will tighten it.
6. **Advanced flag resets on runtime change.** ``useEffect`` syncs
``advanced`` with ``!schema`` whenever ``runtime_id`` flips, so a
user who switches from an unknown runtime to a known one (or
vice-versa) doesn't get stuck in raw-JSON mode.
Verified end-to-end: tsc, lint, build clean. backend pytest + ruff +
mypy untouched (frontend-only PR).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Rafal Lagowski <rla@Rafals-MacBook-Pro.local>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(runtimes): claude-code adapter — Anthropic CLI subprocess (#73)
* feat(runtimes): claude-code adapter — Anthropic CLI subprocess
Replaces the F0 ClaudeCodeAdapter stub with a working implementation.
Pipelines can now use Claude Code in agentic mode (full tool use, file
mutations, MCP) instead of going through the SDK's single-shot api-call.
## Why both api-call (Anthropic) AND claude-code
| Need | Use |
| ---------------------------------------------- | ----------- |
| Single-shot LLM call, no tools, deterministic | api-call |
| Agentic loop with file edits, bash, MCP tools | claude-code |
The CLI is 10-100x more expensive per task but does work the SDK can't
(real coding agent loop). Pipelines compose: cheap api-call for
selection / planning, claude-code for implementation.
## Implementation
- `claude --print --output-format json --model {model_id} {extra_args}`
with the rendered XML piped to stdin.
- Parses structured JSON output: `result`, `usage` (input/output/cache
tokens), `total_cost_usd`, `session_id`, `num_turns` — all populate
RuntimeResult automatically.
- Process lifecycle copied verbatim from the bash adapter:
`start_new_session=True`, POSIX session-group kill on timeout/cancel,
`asyncio.shield(process.wait())` cleanup. Engine pause/abort tears
down the entire CLI tree (including MCP children) without leaks.
- Healthcheck: `shutil.which("claude")` + `claude --version` + checks
for ANTHROPIC_API_KEY (the CLI reads it directly).
- runtime_config: `model_id` (required), `binary_path`, `extra_args`.
extra_args validated as `list[str]`; non-string elements rejected
with a descriptive error before subprocess spawn.
## Failure paths covered
- Missing model_id / api key / binary → 422-style RuntimeResult.errors
- Subprocess spawn failure (FileNotFoundError, OSError)
- Timeout → process group SIGKILL'd, `timed_out=true` in structured
- CancelledError → tree teardown then re-raise
- Non-zero exit → stderr preview in error message
- Unparseable JSON → descriptive error
- `is_error=true` in payload → propagated as failure
## Tests
`tests/smoke/test_claude_code_adapter.py` — 17 tests, all paths above
+ stdin verification + extra_args plumbing + cache token accounting.
Subprocess mocked via `unittest.mock.patch` on `asyncio.create_subprocess_exec`;
real CLI gated behind DAP_E2E_CLAUDE_CODE=1 (separate file, not in CI).
## Schema
`runtime-config-schemas.ts` updated: claude-code's model_id is now
`required`. Header comment lists claude-code as implemented.
All 208 smoke tests pass; ruff + mypy + format clean.
Closes #51
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(runtimes): address PR review on claude-code adapter
Three trafne komentarze z PR #73:
1. **_read_cli_version doesn't leak on timeout.** When `claude --version`
hangs, the previous code caught TimeoutError and returned None
without killing the subprocess. Now: kill + asyncio.shield(wait)
before returning, so healthchecks don't accumulate child processes.
2. **Non-object JSON payload no longer crashes the adapter.** Valid
JSON that isn't a dict (e.g. `[1,2,3]` or `"oops"`) used to raise
AttributeError on the first `payload.get(...)`. Now: explicit
`isinstance(payload, dict)` check returns a descriptive
_failed(...) result with the actual type in the message.
3. **Invalid `usage` shape no longer crashes.** Same defence for
`usage` — verified as dict before .get(), and the `int(...)`
conversions wrapped in try/except for non-numeric token counts.
Both paths return descriptive _failed(...) results.
Tests added: non-object payload, non-dict usage, non-numeric token
counts. 211/211 smoke pass; ruff + mypy + format clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Rafal Lagowski <rla@Rafals-MacBook-Pro.local>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(runtimes): gemini-cli adapter — Google CLI subprocess (#74)
Replaces …
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
POST /runs/{id}/pause|resume|abort./runs/[id]rendered conditionally onfinal_status:running→ Pause + Abortpaused→ Resume + Abortwindow.confirm()(terminal, irreversible). Pause/resume fire immediately.useRunpicks up the new status and resumes/stops correctly.Test plan
Closes #29
🤖 Generated with Claude Code