diff --git a/examples/kineticos-continuation-fleet/ARCHITECTURE.md b/examples/kineticos-continuation-fleet/ARCHITECTURE.md new file mode 100644 index 0000000000..8db99a4565 --- /dev/null +++ b/examples/kineticos-continuation-fleet/ARCHITECTURE.md @@ -0,0 +1,154 @@ +# KineticOS — Architecture (review map) + +This document maps the execution plan (Phases 0–9) onto the actual files, so a +reviewer can navigate the scaffold quickly. The build is a **single Next.js 15 +app** (the proven shape for these hackathons) rather than the plan's literal +multi-service monorepo — but every plane and gate from the plan is present as a +module, and [`infra/render/render.yaml`](infra/render/render.yaml) shows how the +single app fans out into the plan's separate Render services for production. + +## Vision (v2) + +> CAD image of an assembly with a broken component → modified CAD file out, so +> production keeps running temporarily. + +Input is a CAD image (a render/screenshot of the assembly with a broken or +missing component visible in situ). Output is a `ContinuationCadOutput` — a v1 +OpenSCAD-style CAD file, a small BOM, and a textual operator runbook that +together let the line keep moving until the proper OEM replacement arrives. + +The system chooses between four continuation strategies: + +| Strategy | When | +|---|---| +| `substitute_component` | An off-the-shelf or community-cataloged part fits the surrounding assembly (Track A hit). | +| `printed_insert` | The default for unique geometry: a 3D-printable substitute that respects the assembly's mating interfaces. | +| `bridge_adapter` | A small printed adapter so an available in-stock standard fastener can stand in. | +| `simplified_geometry` | Strip non-load features so in-house tooling can produce the part today. | + +## The load-bearing split + +> **Render runs things. Superplane decides when and whether things run.** + +| Plane | Module | Responsibility | +|---|---|---| +| Perception | `src/agents/perception/**` | CAD image → broken-component class → undamaged intent → mating interfaces | +| Continuation Design | `src/agents/design/**` | strategy choice → sourced substitute OR generated insert/adapter | +| Printability | `src/agents/material-adapter.ts` | re-parameterize against locally-loaded stock; printability margin | +| Orchestration | `src/lib/superplane/client.ts` + `src/lib/gates/` + `src/worker/jobs.ts` | workflow state, gates, fan-in, audit | +| Per-Job Runtime | `src/lib/render/client.ts` | births/kills the dedicated ephemeral `cad-validator-{jobId}` service | +| CAD Output | `src/agents/fabrication.ts` | emits the v1 `ContinuationCadOutput` + runs canary/bulk validator | +| Edge / Machine | `src/lib/edge/client.ts` | simulated on-prem agent: inventory + validator telemetry | + +Both sponsor integrations are **clean stubs that run at zero credentials** and +upgrade to real APIs behind a flag (`SUPERPLANE_API_KEY`, `RENDER_API_KEY`). + +## How Render is used (4 ways) + +1. **Web service `kineticos`** — declared in [`render.yaml`](infra/render/render.yaml). +2. **Per-job ephemeral `cad-validator-{jobId}`** — created at runtime by + [`src/lib/render/client.ts → provisionRuntime`](src/lib/render/client.ts); + deleted on terminal status by `teardownRuntime`. The orphan-reaper cron + (commented in `render.yaml`) reconciles via `listEphemeralServices`. +3. **`/healthz`** — Render's healthcheck probe target + ([`src/app/healthz/route.ts`](src/app/healthz/route.ts)). +4. **Managed Postgres `kineticos-pg`** — `DATABASE_URL` injected from the + managed DB into the web service. + +## How Superplane is used (4 ways) + +1. **`startRun`** — a Superplane run is opened on job ingest; the run id and + canvas URL are stamped on `Job.audit.superplaneRunId`. +2. **`emit`** — every JobStatus transition is emitted as a step event, giving + the run record a live timeline of the pipeline. +3. **`recordGate`** — every gate decision (proceed / human_review / block) is + recorded into the run record alongside the local audit trail. +4. **Workflow gating** — the four gates (`composite_confidence`, + `continuation_strategy`, `printability`, `output_acceptance`) live as pure + policy functions in [`src/lib/gates/`](src/lib/gates/) and the worker + pauses the job (`status: needs_human`, `pendingGate` set) when one routes + to a human. +5. **The agent fleet (Canvas)** — [`infra/superplane/kineticos-fleet.canvas.yaml`](infra/superplane/kineticos-fleet.canvas.yaml) + is the entire pipeline as a 34-node SuperPlane Canvas: the six perception + agents fan out in parallel from the ingest webhook, a `merge` fuses them, the + two design tracks branch on a sourcing hit, and each phase ends in the + `http evaluate-gate → if → approval` gate pattern before the line resumes. + Every action node is an `http` executor hitting one agent endpoint — + granular [`/api/agents/[agent]`](src/app/api/agents) for the perception + sensors + design tracks, coarse [`/api/stages/*`](src/app/api/stages) for + phases 4 and 5–7. The in-process worker and the Canvas are two drivers of the + same agents + gates (`LOCAL_ORCHESTRATOR=1` selects the worker). + +## The spine + +[`src/lib/types.ts`](src/lib/types.ts) — the canonical `Job` object every stage +keys off (Phase 0.3), plus the Phase 2/3/4/8 output contracts, the gate types, +the new `ContinuationStrategy` enum, the v1 `ContinuationCadOutput` type, and +the **locked stage signatures** the worker imports and each stage module +implements. + +## The pipeline (worker drives it, upserting the Job after every step) + +`src/worker/jobs.ts` runs stages in order and evaluates a Superplane gate +between phases; a human-scoped gate pauses the job (`needs_human`) and resumes +on `resolveGate()`. + +| Phase | Stage | File | +|---|---|---| +| 2A | CAD image conditioning / admissibility | `src/agents/perception/conditioning.ts` | +| 2B | Broken-component localization *(exemplar)* | `src/agents/perception/classification.ts` | +| 2C | Reconstruct the **undamaged intent** | `src/agents/perception/reconstruction.ts` | +| 2D | Interface extraction (terminal scale chain) | `src/agents/perception/dimensioning.ts` | +| 2E | Material & surface inference | `src/agents/perception/material.ts` | +| 2F | Telemetry track + sensor fusion | `src/agents/perception/telemetry.ts` | +| **2.G** | **Composite confidence gate** | `src/lib/gates/index.ts` → `compositeConfidenceGate` | +| 3A | Substitute sourcing (Track A) | `src/agents/design/sourcing.ts` | +| 3B | Generative continuation CAD B1–B8 (Track B) | `src/agents/design/generative-cad.ts` | +| **3.G** | **Continuation strategy gate** | `src/lib/gates/index.ts` → `designAcceptanceGate` | +| 4 | Printability adaptation + toolpath | `src/agents/material-adapter.ts` | +| **4.3** | **Printability feasibility gate** | `src/lib/gates/index.ts` → `structuralGate` | +| 5 | Provision dedicated cad-validator service | `src/lib/render/client.ts` → `provisionRuntime` | +| 6/7 | Emit v1 ContinuationCadOutput + canary/bulk validator | `src/agents/fabrication.ts` | +| 8 | Output validation | `src/agents/fabrication.ts` | +| **8.1** | **Output acceptance gate** → COMPLETE | `src/lib/gates/index.ts` → `acceptanceGate` | +| 8.2 | Teardown dedicated runtime | `src/lib/render/client.ts` → `teardownRuntime` | +| 8.3 | Audit seal | `src/lib/audit.ts` + `Job.audit` | + +## Data & API + +- `src/lib/store.ts` — dual-layer job store: globalThis `Map` always-on, Postgres + write-through when `DATABASE_URL` is set. `src/lib/db/**` is the pg layer. +- `src/app/api/jobs/route.ts` — `POST` ingest (422 if neither `cadImageUris` + nor `telemetryUri`), `GET` list. +- `src/app/api/jobs/[id]/route.ts` — `GET` one (polling target). +- `src/app/api/jobs/[id]/gate/route.ts` — `POST` resolve a scoped human gate. +- `src/app/healthz/route.ts` — Render health check target. + +## UI (live polling, dark theme) + +`src/app/page.tsx` tab shell → `src/components/tabs/{IntakeTab,PipelineTab,GatesTab,AuditTab}.tsx`, +all built on `src/components/ui.tsx` primitives and `src/lib/usePolling.ts`. +The PipelineTab carries the new **Continuation CAD output (v1)** panel that +renders the OpenSCAD payload + BOM + runbook with a `data:` download link. + +## V1 CAD output — what's intentionally rough + +The v1 `ContinuationCadOutput` payload is a plain-text OpenSCAD script. It +captures the resolved mating interfaces (bore, plate footprint, tooth count) +but the non-load features are simplified for fast 3D printing — gear teeth are +square notches, not involute curves; brackets are flat plates with corner +holes; everything else is a bored block. A v2 swap to a real CAD-kernel +B-rep through the `cad-validator-{jobId}` service is a drop-in replacement of +the `buildOpenscadScript` helper in `src/agents/fabrication.ts` — the +`ContinuationCadOutput` contract (`format`, `filename`, `contents`, `bom`, +`runbook`) is the same, and every other plane already keys off that shape. + +## Not yet built (Phase 9 hardening — deliberately deferred for review) + +- Orphan-reaper cron logic (the primitive `render.listEphemeralServices()` + exists; the reconcile job/endpoint is a TODO in `render.yaml`). +- 2A→3A bounded reconstruction refinement loop (single-pass today). +- Real per-job mTLS / signing-key verification on the edge channel (key is + generated and passed; verification is simulated). +- Real CAD kernel for the v2 continuation output (drop-in behind + `buildOpenscadScript`). diff --git a/examples/kineticos-continuation-fleet/README.md b/examples/kineticos-continuation-fleet/README.md new file mode 100644 index 0000000000..9aceb6bbec --- /dev/null +++ b/examples/kineticos-continuation-fleet/README.md @@ -0,0 +1,140 @@ +# KineticOS + +**CAD image of an assembly with a broken component in → modified CAD file out, so production keeps running.** + +

+ + Open in SuperPlane + +

+ +> ☝️ Opens the **SuperPlane app** — the canvas with the full agent fleet (every +> perception, design, printability, and validation agent + the gates between +> them), hosted locally on **http://localhost:3001** (KineticOS keeps :3000). +> To bring it up from scratch — no Docker Desktop needed: +> +> ```bash +> # 1. a headless container runtime +> brew install colima docker && colima start --vm-type vz --vz-rosetta +> # 2. host the SuperPlane app on :3001 +> docker run -d --name superplane -p 3001:3000 \ +> -e BASE_URL=http://localhost:3001 -e ALLOWED_WS_ORIGINS=http://localhost:3001 \ +> -v spdata:/app/data ghcr.io/superplanehq/superplane-demo:stable +> # 3. point the fleet canvas at KineticOS and load it — host.docker.internal +> # reaches the host from inside the SuperPlane container +> infra/superplane/apply.sh http://host.docker.internal:3000 # or import the YAML in the UI +> ``` +> +> See [`infra/superplane/`](infra/superplane/) for the canvas + the full walkthrough. + +KineticOS ingests a CAD image of a mechanical assembly that has a broken or +missing component, locates the break, **reconstructs the intended undamaged +geometry** (never the cracked artifact), picks a **continuation strategy** — +an off-the-shelf substitute, a 3D-printable insert, a small bridge adapter, or +a simplified-geometry version — re-parameterizes the design against locally +loaded stock, then spins up a **dedicated, ephemeral cloud validator per job** +and hands back a **v1 CAD file** the operator can save, slice, and use to keep +the line running until the OEM replacement arrives. + +> The CAD output itself is **v1** — an OpenSCAD-style script that captures the +> mating interfaces and the chosen strategy. The surrounding system (gates, +> per-job ephemeral runtime, audit trail, multi-plane orchestration) is fit to +> the final vision so v1 can be swapped for a real CAD-kernel output later +> without disturbing anything else. + +Two load-bearing constraints, kept strictly separate: + +> **Render runs things. Superplane decides when and whether things run.** + +## Render — four ways + +1. **Web service** — `kineticos` hosts the Next.js app (UI + API + `/healthz`). + See [`infra/render/render.yaml`](infra/render/render.yaml). +2. **Per-job ephemeral service** — `cad-validator-{jobId}` is born and killed + by [`src/lib/render/client.ts`](src/lib/render/client.ts) for every job. + Receives the v1 CAD output, runs the canary→bulk validator passes, streams + progress frames back. No shared multi-tenant endpoint — one job, one service. +3. **Healthcheck probe** — Render polls [`/healthz`](src/app/healthz/route.ts) + to drive its readiness gate. +4. **Managed Postgres** — `kineticos-pg` backs the durable Job + audit store + when `DATABASE_URL` is set ([`src/lib/db/**`](src/lib/db/)). + +## Superplane — four ways + +1. **Workflow run record** — every Job starts a Superplane run; the canvas + URL is stamped on `Job.audit.superplaneRunId` + ([`src/lib/superplane/client.ts`](src/lib/superplane/client.ts) · + `startRun`). +2. **Step transitions** — every status change is emitted as a step event + (`emit`) so the run record is a live timeline of the pipeline. +3. **Gates** — four distinct gates (`composite_confidence` 2.G, + `continuation_strategy` 3.G, `printability` 4.3, `output_acceptance` 8.1) + live as pure policy functions in [`src/lib/gates/`](src/lib/gates/) and + recorded through `recordGate`. Human-scoped gates park the job in the + **Gates** tab; the rest pass through. +4. **Audit fan-in** — gate decisions are mirrored into the run record, giving + one immutable provenance trail across every plane — the liability record + the AuditTab renders. + +### The agent fleet (Canvas) + +[`infra/superplane/kineticos-fleet.canvas.yaml`](infra/superplane/kineticos-fleet.canvas.yaml) +is the **whole process as one SuperPlane Canvas** — 34 nodes that fan the six +perception agents out in parallel, fuse them, branch the two design tracks, and +gate between every phase (`http evaluate-gate → if → approval`) before the line +resumes production. Each action node is an `http` executor calling one KineticOS +agent at [`/api/agents/*`](src/app/api/agents) (roster: `GET /api/agents`); it +runs at zero credentials. See [`infra/superplane/README.md`](infra/superplane/README.md) +to load it, and `infra/superplane/smoke.sh` to rehearse a full run offline. + +## Runs at zero credentials + +Every integration is optional. With **no** environment variables the full +pipeline runs end-to-end on deterministic fallbacks: Claude → rule-based +stages, Superplane → an in-process control-plane model, Render → a simulated +provision/teardown lifecycle, the validator → simulated telemetry, Postgres → +an in-process `Map`. Each key upgrades exactly one plane from simulated to +real. + +## Quickstart + +```bash +npm install +npm run dev # http://localhost:3000 +``` + +Open the app → **Intake** tab → drop a CAD image of the broken assembly → add +an optional assembly context / failure note → **Ingest CAD image**, then watch +the **Pipeline** tab drive the job through perception → continuation design → +printability → provisioning → CAD output → validation, with Superplane gates +in between. The **Continuation CAD output (v1)** panel shows the emitted +OpenSCAD file with a download link. Low-confidence jobs pause in the +**Gates** tab; the full provenance trail is in **Audit**. + +Optional — turn planes real: + +```bash +cp .env.example .env.local +# ANTHROPIC_API_KEY → Claude for the LLM-bearing stages +# SUPERPLANE_API_KEY → the real Superplane control plane +# RENDER_API_KEY → real ephemeral per-job cad-validator provisioning +# DATABASE_URL → durable jobs + audit trail (npm run db:migrate && npm run db:seed) +``` + +## Architecture + +The single Next.js app maps the plan's four planes and every gate onto modules; +see **[ARCHITECTURE.md](ARCHITECTURE.md)** for the full Phase-0–9 → file map. +The canonical `Job` object in [`src/lib/types.ts`](src/lib/types.ts) is the +spine everything keys off — including the new `ContinuationStrategy` enum and +the `ContinuationCadOutput` v1 deliverable. +[`infra/render/render.yaml`](infra/render/render.yaml) shows the production +Render split. + +## Status + +Scaffold + architecture, aligned to the CAD-continuation vision. The pipeline, +gates, dual-layer store, Render/Superplane stub interfaces, live UI, and v1 +CAD output emitter run end-to-end. The v1 CAD output is intentionally a +placeholder (OpenSCAD script) — the surrounding system is built to accept a +real CAD-kernel STEP/STL output without changing any other module. diff --git a/examples/kineticos-continuation-fleet/SUBMISSION.md b/examples/kineticos-continuation-fleet/SUBMISSION.md new file mode 100644 index 0000000000..4a549a3a6b --- /dev/null +++ b/examples/kineticos-continuation-fleet/SUBMISSION.md @@ -0,0 +1,62 @@ +# KineticOS — a SuperPlane agent-fleet example + +A self-contained snapshot of **KineticOS**, a project built **on top of +SuperPlane**: a CAD image of a broken industrial machine comes in → a fleet of +agents looks at it, identifies the fault, reconstructs the intended part, +generates a new **3D-printable CAD file**, validates it, and **resumes +production** — with SuperPlane gating every phase. + +> **Heads-up for maintainers:** this is a *downstream application* example, not a +> change to the SuperPlane platform. It's contributed as an `examples/` folder so +> it adds nothing to your build/CI and touches none of your tree. If you'd prefer +> only the workflow template, the single file worth upstreaming is the Canvas +> below — it can be dropped into `templates/canvases/` with `metadata.isTemplate: +> true` per `docs/contributing/templates.md`. Happy to reshape this PR to just +> that if you'd rather. + +## The primary artifact — the Canvas + +[`infra/superplane/kineticos-fleet.canvas.yaml`](infra/superplane/kineticos-fleet.canvas.yaml) +is a 34-node Canvas exercising a lot of SuperPlane in one real workflow: + +- **Parallel fan-out + `merge`** — six perception agents run concurrently from + the ingest trigger, then a `merge` barrier fuses them. +- **The gate pattern (×4)** — each phase ends in `http` (evaluate-gate) → `if` + (`decision == "proceed"`) → `approval` (the human gate), with rejections + routed to a halt sink. +- **Conditional branching** — an `if` picks an off-the-shelf substitute vs. a + generated continuation part. +- **`http` executors** call the app's agent endpoints; **annotations** document + each section on-canvas. + +`infra/superplane/kineticos-fleet.local.canvas.yaml` is the same Canvas with +executor URLs pointed at `host.docker.internal:3000` for a locally-hosted +SuperPlane (container) calling the app (host). + +## The rest of the folder (the downstream app) + +- `src/app/api/agents/**` + `src/lib/contract.ts` — the Next.js HTTP executors + each Canvas node calls (one agent per endpoint). **Illustrative** — this is + the KineticOS app, not SuperPlane platform code; it isn't built by your CI. +- `infra/superplane/{README.md,apply.sh,smoke.sh}` — node→agent→endpoint map, + a canvas loader (host substitution + `superplane canvas create`), and an + offline end-to-end fleet rehearsal. +- `README.md` / `ARCHITECTURE.md` — the KineticOS project docs for context. + +## Run it + +```bash +# the app (executors) — runs at zero credentials +npm install && npm run dev # KineticOS on :3000 + +# SuperPlane locally (no Docker Desktop needed) +brew install colima docker && colima start --vm-type vz --vz-rosetta +docker run -d --name superplane -p 3001:3000 \ + -e BASE_URL=http://localhost:3001 -e ALLOWED_WS_ORIGINS=http://localhost:3001 \ + -v spdata:/app/data ghcr.io/superplanehq/superplane-demo:stable + +# load the fleet (host.docker.internal reaches the host from the container) +infra/superplane/apply.sh http://host.docker.internal:3000 # then import in the UI +``` + +Full project: https://github.com/sahielbose/KineticOS-Superplane-Hackathon diff --git a/examples/kineticos-continuation-fleet/infra/superplane/README.md b/examples/kineticos-continuation-fleet/infra/superplane/README.md new file mode 100644 index 0000000000..caed345f6c --- /dev/null +++ b/examples/kineticos-continuation-fleet/infra/superplane/README.md @@ -0,0 +1,175 @@ +# KineticOS — the SuperPlane agent fleet + +> **A CAD image of a broken industrial machine comes in → a new, 3D-printable +> CAD file goes out, so the line keeps running.** + +

+ + Open in SuperPlane + +

+ +> SuperPlane is hosted locally on **:3001** (KineticOS keeps :3000). Bring it up +> with Colima — no Docker Desktop needed — then load this fleet: +> ```bash +> brew install colima docker && colima start --vm-type vz --vz-rosetta +> docker run -d --name superplane -p 3001:3000 \ +> -e BASE_URL=http://localhost:3001 -e ALLOWED_WS_ORIGINS=http://localhost:3001 \ +> -v spdata:/app/data ghcr.io/superplanehq/superplane-demo:stable +> infra/superplane/apply.sh http://host.docker.internal:3000 # SuperPlane-in-container → KineticOS-on-host +> ``` + +This directory holds the **agent fleet as a SuperPlane Canvas** — +[`kineticos-fleet.canvas.yaml`](kineticos-fleet.canvas.yaml) — that orchestrates +the *exact* KineticOS process end to end: + +``` +look at the machine → identify what's wrong → reconstruct the intended part + → generate a new 3D-printable CAD file → prove it holds → resume production +``` + +SuperPlane is the **brain**: it decides *when and whether* each agent runs, fans +the perception sensors out in parallel, branches the two design tracks, and parks +the job at a **human gate** the moment confidence, the design trail, the +printability margin, or the output validation falls short. The compute itself +runs in the KineticOS Next.js app (and Render's per-job validator); SuperPlane +never executes domain logic — it routes. + +Every `TYPE_ACTION` node is an **`http` executor** that POSTs `{ "job_id": … }` +to one KineticOS endpoint. The whole fleet runs **at zero credentials**: each +agent has a deterministic fallback, so you can demo the full canvas offline. + +--- + +## The fleet at a glance + +`34 nodes` · `1 trigger` · `28 agent/gate/control nodes` · `5 doc widgets` · `40 edges` + +| # | Node | Component | Calls | What the agent does | +|---|------|-----------|-------|---------------------| +| — | Ingest: broken-machine CAD image | `webhook` | *(event source)* | Fires when `POST /api/jobs` posts to `SP_INGEST_WEBHOOK` | +| 2A | Conditioning | `http` | `/api/agents/conditioning` | Is the CAD image admissible (sharp, exposed, on-part)? | +| 2B | Classification | `http` | `/api/agents/classification` | **Which** component is broken (drives everything downstream) | +| 2C | Reconstruction | `http` | `/api/agents/reconstruction` | Rebuild the *intended undamaged* geometry — needs 2B | +| 2D | Dimensioning | `http` | `/api/agents/dimensioning` | Mating interfaces + a terminating scale chain | +| 2E | Material inference | `http` | `/api/agents/material-infer` | Material class + surface finish | +| 2F | Telemetry fusion | `http` | `/api/agents/telemetry` | Failure mode from telemetry + sensor-fusion agreement | +| — | Fuse the perception sensors | `merge` | — | Barrier: wait for all sensors | +| 2.x | Assemble PerceptionResult | `http` | `/api/agents/perception-assemble` | Compose the composite-confidence score | +| **2.G** | **Gate · composite confidence** | `http` + `if` + `approval` | `/api/evaluate-gate` | Proceed, or scope a human to the weak field | +| 3A | Sourcing | `http` | `/api/agents/sourcing` | Hunt an off-the-shelf / community substitute | +| 3B | Generative CAD | `http` | `/api/agents/generative-cad` | Synthesise the continuation insert via the **B1–B8** trail | +| **3.G** | **Gate · continuation strategy** | `http` + `if` + `approval` | `/api/evaluate-gate` | Generated + load-bearing → human review | +| 4 | Printability adaptation | `http` | `/api/stages/material` | Re-parameterize to locally-loaded stock | +| **4.3** | **Gate · printability feasibility** | `http` + `if` + `approval` | `/api/evaluate-gate` | Margin below duty load → block / relax | +| 5–7 | Provision + emit + validate | `http` | `/api/stages/fabricate-report` | Birth the ephemeral Render `cad-validator-{jobId}`, emit the **v1 CAD output**, run canary→bulk | +| **8.1** | **Gate · output acceptance** | `http` + `if` + `approval` | `/api/evaluate-gate` | Fan-in of dimensional + structural + anomaly checks | +| 8.3 | Seal run + resume production | `http` | `/api/agents/finalize` | Seal the audit trail; mark the line resumed | +| — | ✅ Line resumed / ⛔ Halted | `noop` | — | Terminal sinks | + +The fleet roster is also live at **`GET /api/agents`**. + +### The gate pattern (used ×4) + +Every phase ends in the same three-node shape: + +``` + http evaluate-gate ──► if (decision == "proceed") ──true──► next phase + │ + false + ▼ + approval (human gate) ──approved──► next phase + │ + rejected + ▼ + ⛔ Halt +``` + +The gate policy itself lives once, in +[`src/lib/gates/index.ts`](../../src/lib/gates/index.ts); `/api/evaluate-gate` +returns `{ decision, reason, scoped_field }`. When a gate routes to a human it +names the **exact** weak field (the component class, the reconstruction overlay, +one caliper reading) — never a generic "approve?". The operator resolves it +through the app's existing `POST /api/jobs/{id}/gate` endpoint / **Gates** tab. + +--- + +## Run it + +### 0. Start KineticOS (the executors) + +```bash +npm install && npm run dev # http://localhost:3000 — runs at zero credentials +``` + +Smoke-test the fleet endpoints directly (what the canvas nodes do, in order): + +```bash +infra/superplane/smoke.sh http://localhost:3000 +``` + +### 1. Point the canvas at your KineticOS base URL + +The YAML ships with `https://kineticos.onrender.com`. To retarget (e.g. local): + +```bash +# writes a substituted copy to /tmp and (optionally) applies it. +# SuperPlane runs in a container, so it reaches KineticOS (host :3000) via +# host.docker.internal — use plain localhost:3000 only if SP runs on the host. +infra/superplane/apply.sh http://host.docker.internal:3000 +``` + +### 2. Load the canvas into SuperPlane + +**CLI** (preferred — needs the `superplane` CLI authenticated to your org): + +```bash +superplane canvas create -f infra/superplane/kineticos-fleet.canvas.yaml +# or let apply.sh do the host-substitution + create in one step: +APPLY=1 infra/superplane/apply.sh https://your-kineticos.example.com +``` + +**UI:** open SuperPlane → **New canvas → Import YAML** → paste the (substituted) +file. The graph, gates, and annotations render exactly as laid out here. + +### 3. Wire the ingest event source + +1. In SuperPlane, create a **Webhook event source** and bind it to the + **Ingest: broken-machine CAD image** trigger node. Copy its URL + signing token. +2. In KineticOS, set: + + ```bash + SP_INGEST_WEBHOOK= + SP_WEBHOOK_TOKEN= + # leave LOCAL_ORCHESTRATOR unset so intake fires the webhook instead of the + # in-process worker (see src/app/api/jobs/route.ts) + ``` + +Now every `POST /api/jobs` (an operator dropping a CAD image in the **Intake** +tab) fires the canvas, and SuperPlane drives the whole fleet, calling back into +the agent endpoints and gating between phases. + +> **Note on the trigger node.** SuperPlane models inbound webhooks as *event +> sources* that a trigger node binds to in the UI. The node here uses +> `component: "webhook"` as a placeholder — if your SuperPlane build names the +> generic inbound trigger differently, set it on that one node; everything +> downstream is unchanged. + +--- + +## How this maps to the rest of KineticOS + +| Concern | Where | +|---|---| +| The agents (Claude + deterministic fallback) | [`src/agents/**`](../../src/agents) | +| The four gate policies | [`src/lib/gates/index.ts`](../../src/lib/gates/index.ts) | +| Granular per-agent executors | [`src/app/api/agents/[agent]/route.ts`](../../src/app/api/agents) | +| Coarse phase executors (4, 5–7) | [`src/app/api/stages/**`](../../src/app/api/stages) | +| Gate evaluator | [`src/app/api/evaluate-gate/route.ts`](../../src/app/api/evaluate-gate/route.ts) | +| Wire contract (snake_case) | [`src/lib/contract.ts`](../../src/lib/contract.ts) | +| In-process equivalent (offline / no canvas) | [`src/worker/jobs.ts`](../../src/worker/jobs.ts) | + +The in-process worker and this canvas are **two drivers of the same agents**: +set `LOCAL_ORCHESTRATOR=1` to drive the pipeline in-process (no SuperPlane); +leave it unset to let this fleet drive it. Same agents, same gates, same audit +trail either way. diff --git a/examples/kineticos-continuation-fleet/infra/superplane/apply.sh b/examples/kineticos-continuation-fleet/infra/superplane/apply.sh new file mode 100755 index 0000000000..2b4336d4be --- /dev/null +++ b/examples/kineticos-continuation-fleet/infra/superplane/apply.sh @@ -0,0 +1,50 @@ +#!/usr/bin/env bash +# Retarget the KineticOS fleet canvas at a KineticOS base URL and (optionally) +# load it into SuperPlane. +# +# infra/superplane/apply.sh [BASE_URL] +# +# BASE_URL KineticOS base the http nodes call (default: the prod Render URL). +# APPLY=1 also run `superplane canvas create -f` on the substituted file. +# +# Examples: +# infra/superplane/apply.sh http://localhost:3000 # just substitute +# APPLY=1 infra/superplane/apply.sh https://kos.example.com # substitute + create +set -euo pipefail + +DEFAULT_HOST="https://kineticos.onrender.com" +BASE_URL="${1:-$DEFAULT_HOST}" +BASE_URL="${BASE_URL%/}" # strip any trailing slash + +SRC="$(cd "$(dirname "$0")" && pwd)/kineticos-fleet.canvas.yaml" +OUT="${TMPDIR:-/tmp}/kineticos-fleet.canvas.yaml" + +if [[ ! -f "$SRC" ]]; then + echo "✗ canvas not found: $SRC" >&2 + exit 1 +fi + +# Replace the baked-in host with the target base URL. +sed "s#${DEFAULT_HOST}#${BASE_URL}#g" "$SRC" > "$OUT" +COUNT="$(grep -c "${BASE_URL}/api/" "$OUT" || true)" +echo "✓ wrote $OUT" +echo " → ${COUNT} http executor URLs now point at ${BASE_URL}" + +if [[ "${APPLY:-0}" == "1" ]]; then + if ! command -v superplane >/dev/null 2>&1; then + echo "✗ APPLY=1 but the 'superplane' CLI is not installed / on PATH." >&2 + echo " Install it, authenticate to your org, then re-run — or import $OUT in the UI." >&2 + exit 1 + fi + echo "→ superplane canvas create -f $OUT" + superplane canvas create -f "$OUT" + echo "✓ canvas created. Bind the ingest trigger to a Webhook event source," + echo " then set SP_INGEST_WEBHOOK + SP_WEBHOOK_TOKEN in KineticOS." +else + echo + echo "Next:" + echo " • CLI: superplane canvas create -f $OUT" + echo " • UI: New canvas → Import YAML → paste $OUT" + echo " • Then bind the ingest trigger to a Webhook event source and set" + echo " SP_INGEST_WEBHOOK + SP_WEBHOOK_TOKEN in KineticOS (see README.md)." +fi diff --git a/examples/kineticos-continuation-fleet/infra/superplane/kineticos-fleet.canvas.yaml b/examples/kineticos-continuation-fleet/infra/superplane/kineticos-fleet.canvas.yaml new file mode 100644 index 0000000000..173ee221a0 --- /dev/null +++ b/examples/kineticos-continuation-fleet/infra/superplane/kineticos-fleet.canvas.yaml @@ -0,0 +1,504 @@ +# ───────────────────────────────────────────────────────────────────────── +# KineticOS — the agent fleet, as a SuperPlane Canvas. +# +# THE EXACT PROCESS, end to end: +# a CAD image of an industrial machine with a broken component arrives → +# a fleet of perception agents LOOKS at it and IDENTIFIES what is wrong → +# a design fleet RECONSTRUCTS the intended geometry and GENERATES a new +# 3D-printable CAD file → printability + validation agents prove it will +# hold → and on acceptance the line RESUMES PRODUCTION on that v1 file +# until the OEM replacement arrives. +# +# SuperPlane is the BRAIN: it decides *when and whether* each agent runs, and +# it parks the job at a human gate whenever confidence, the design trail, the +# printability margin, or the output validation says so. Every TYPE_ACTION node +# below is an `http` executor that calls one KineticOS agent endpoint; the agent +# itself (Claude-backed, with a deterministic fallback) lives in the Next.js app. +# +# Replace the host `https://kineticos.onrender.com` with your KineticOS base URL +# (e.g. http://localhost:3000 for local dev) — infra/superplane/apply.sh does +# this substitution for you. The ingest trigger binds to the SuperPlane webhook +# event source that KineticOS fires as SP_INGEST_WEBHOOK on POST /api/jobs. +# ───────────────────────────────────────────────────────────────────────── +apiVersion: v1 +kind: Canvas +metadata: + name: "KineticOS — broken-machine → 3D-printable continuation fleet" + description: "Looks at a CAD image of a broken industrial machine, identifies the fault, generates a new 3D-printable CAD file, and resumes production — gated by SuperPlane at every phase." + isTemplate: false +spec: + nodes: + # ───────────────── ingest (event source) ───────────────── + - id: "ingest-trigger" + name: "Ingest: broken-machine CAD image" + type: "TYPE_TRIGGER" + configuration: {} + position: { x: 120, y: 760 } + component: "webhook" + isCollapsed: false + + # ───────────────── PERCEPTION FLEET — "look + identify what's wrong" ───────────────── + - id: "perc-conditioning" + name: "2A · Conditioning (image admissibility)" + type: "TYPE_ACTION" + configuration: + method: "POST" + url: "https://kineticos.onrender.com/api/agents/conditioning" + contentType: "application/json" + json: + job_id: "{{ root().data.job_id }}" + timeoutSeconds: 30 + successCodes: "2xx" + retry: { enabled: true, strategy: "exponential", maxAttempts: 5, intervalSeconds: 5 } + position: { x: 600, y: 160 } + component: "http" + isCollapsed: false + - id: "perc-classification" + name: "2B · Classification (locate the break)" + type: "TYPE_ACTION" + configuration: + method: "POST" + url: "https://kineticos.onrender.com/api/agents/classification" + contentType: "application/json" + json: + job_id: "{{ root().data.job_id }}" + timeoutSeconds: 30 + successCodes: "2xx" + retry: { enabled: true, strategy: "exponential", maxAttempts: 5, intervalSeconds: 5 } + position: { x: 600, y: 420 } + component: "http" + isCollapsed: false + - id: "perc-reconstruction" + name: "2C · Reconstruction (undamaged intent)" + type: "TYPE_ACTION" + configuration: + method: "POST" + url: "https://kineticos.onrender.com/api/agents/reconstruction" + contentType: "application/json" + json: + job_id: "{{ root().data.job_id }}" + timeoutSeconds: 30 + successCodes: "2xx" + retry: { enabled: true, strategy: "exponential", maxAttempts: 5, intervalSeconds: 5 } + position: { x: 1040, y: 420 } + component: "http" + isCollapsed: false + - id: "perc-dimensioning" + name: "2D · Dimensioning (interfaces + scale)" + type: "TYPE_ACTION" + configuration: + method: "POST" + url: "https://kineticos.onrender.com/api/agents/dimensioning" + contentType: "application/json" + json: + job_id: "{{ root().data.job_id }}" + timeoutSeconds: 30 + successCodes: "2xx" + retry: { enabled: true, strategy: "exponential", maxAttempts: 5, intervalSeconds: 5 } + position: { x: 600, y: 920 } + component: "http" + isCollapsed: false + - id: "perc-material" + name: "2E · Material inference" + type: "TYPE_ACTION" + configuration: + method: "POST" + url: "https://kineticos.onrender.com/api/agents/material-infer" + contentType: "application/json" + json: + job_id: "{{ root().data.job_id }}" + timeoutSeconds: 30 + successCodes: "2xx" + retry: { enabled: true, strategy: "exponential", maxAttempts: 5, intervalSeconds: 5 } + position: { x: 600, y: 1160 } + component: "http" + isCollapsed: false + - id: "perc-telemetry" + name: "2F · Telemetry fusion" + type: "TYPE_ACTION" + configuration: + method: "POST" + url: "https://kineticos.onrender.com/api/agents/telemetry" + contentType: "application/json" + json: + job_id: "{{ root().data.job_id }}" + timeoutSeconds: 30 + successCodes: "2xx" + retry: { enabled: true, strategy: "exponential", maxAttempts: 5, intervalSeconds: 5 } + position: { x: 600, y: 1400 } + component: "http" + isCollapsed: false + - id: "perc-merge" + name: "Fuse the perception sensors" + type: "TYPE_ACTION" + configuration: + enableStopIf: false + enableTimeout: true + executionTimeout: { unit: "minutes", value: 2 } + position: { x: 1480, y: 780 } + component: "merge" + isCollapsed: false + - id: "perc-assemble" + name: "Assemble PerceptionResult" + type: "TYPE_ACTION" + configuration: + method: "POST" + url: "https://kineticos.onrender.com/api/agents/perception-assemble" + contentType: "application/json" + json: + job_id: "{{ root().data.job_id }}" + timeoutSeconds: 30 + successCodes: "2xx" + retry: { enabled: true, strategy: "exponential", maxAttempts: 5, intervalSeconds: 5 } + position: { x: 1920, y: 780 } + component: "http" + isCollapsed: false + + # ───────────────── GATE 2.G · composite confidence ───────────────── + - id: "gate-2g" + name: "Gate 2.G · composite confidence" + type: "TYPE_ACTION" + configuration: + method: "POST" + url: "https://kineticos.onrender.com/api/evaluate-gate" + contentType: "application/json" + json: + job_id: "{{ root().data.job_id }}" + gate: "composite_confidence" + timeoutSeconds: 30 + successCodes: "2xx" + retry: { enabled: true, strategy: "fixed", maxAttempts: 3, intervalSeconds: 5 } + position: { x: 2360, y: 780 } + component: "http" + isCollapsed: false + - id: "if-2g" + name: "2.G clears?" + type: "TYPE_ACTION" + configuration: + expression: 'previous().data.body.decision == "proceed"' + position: { x: 2800, y: 780 } + component: "if" + isCollapsed: false + - id: "approve-2g" + name: "Operator review · confirm the broken component" + type: "TYPE_ACTION" + configuration: + items: + - type: "anyone" + position: { x: 2800, y: 1080 } + component: "approval" + isCollapsed: false + + # ───────────────── DESIGN FLEET — "make a new 3D-printable CAD file" ───────────────── + - id: "design-sourcing" + name: "3A · Sourcing (off-the-shelf substitute)" + type: "TYPE_ACTION" + configuration: + method: "POST" + url: "https://kineticos.onrender.com/api/agents/sourcing" + contentType: "application/json" + json: + job_id: "{{ root().data.job_id }}" + timeoutSeconds: 30 + successCodes: "2xx" + retry: { enabled: true, strategy: "exponential", maxAttempts: 5, intervalSeconds: 5 } + position: { x: 3240, y: 780 } + component: "http" + isCollapsed: false + - id: "if-sourced" + name: "Substitute found?" + type: "TYPE_ACTION" + configuration: + expression: "previous().data.body.matched == true" + position: { x: 3680, y: 780 } + component: "if" + isCollapsed: false + - id: "design-generative" + name: "3B · Generative CAD (B1–B8)" + type: "TYPE_ACTION" + configuration: + method: "POST" + url: "https://kineticos.onrender.com/api/agents/generative-cad" + contentType: "application/json" + json: + job_id: "{{ root().data.job_id }}" + timeoutSeconds: 45 + successCodes: "2xx" + retry: { enabled: true, strategy: "exponential", maxAttempts: 5, intervalSeconds: 5 } + position: { x: 3680, y: 1060 } + component: "http" + isCollapsed: false + + # ───────────────── GATE 3.G · continuation strategy ───────────────── + - id: "gate-3g" + name: "Gate 3.G · continuation strategy" + type: "TYPE_ACTION" + configuration: + method: "POST" + url: "https://kineticos.onrender.com/api/evaluate-gate" + contentType: "application/json" + json: + job_id: "{{ root().data.job_id }}" + gate: "continuation_strategy" + timeoutSeconds: 30 + successCodes: "2xx" + retry: { enabled: true, strategy: "fixed", maxAttempts: 3, intervalSeconds: 5 } + position: { x: 4120, y: 880 } + component: "http" + isCollapsed: false + - id: "if-3g" + name: "3.G clears?" + type: "TYPE_ACTION" + configuration: + expression: 'previous().data.body.decision == "proceed"' + position: { x: 4560, y: 880 } + component: "if" + isCollapsed: false + - id: "approve-3g" + name: "Operator review · the B1–B8 design trail" + type: "TYPE_ACTION" + configuration: + items: + - type: "anyone" + position: { x: 4560, y: 1180 } + component: "approval" + isCollapsed: false + + # ───────────────── PRINTABILITY + GATE 4.3 ───────────────── + - id: "phase4-material" + name: "4 · Printability adaptation (local stock)" + type: "TYPE_ACTION" + configuration: + method: "POST" + url: "https://kineticos.onrender.com/api/stages/material" + contentType: "application/json" + json: + job_id: "{{ root().data.job_id }}" + timeoutSeconds: 30 + successCodes: "2xx" + retry: { enabled: true, strategy: "exponential", maxAttempts: 5, intervalSeconds: 5 } + position: { x: 5000, y: 880 } + component: "http" + isCollapsed: false + - id: "gate-43" + name: "Gate 4.3 · printability feasibility" + type: "TYPE_ACTION" + configuration: + method: "POST" + url: "https://kineticos.onrender.com/api/evaluate-gate" + contentType: "application/json" + json: + job_id: "{{ root().data.job_id }}" + gate: "printability" + timeoutSeconds: 30 + successCodes: "2xx" + retry: { enabled: true, strategy: "fixed", maxAttempts: 3, intervalSeconds: 5 } + position: { x: 5440, y: 880 } + component: "http" + isCollapsed: false + - id: "if-43" + name: "4.3 clears?" + type: "TYPE_ACTION" + configuration: + expression: 'previous().data.body.decision == "proceed"' + position: { x: 5880, y: 880 } + component: "if" + isCollapsed: false + - id: "approve-43" + name: "Operator review · relax constraints or abort" + type: "TYPE_ACTION" + configuration: + items: + - type: "anyone" + position: { x: 5880, y: 1180 } + component: "approval" + isCollapsed: false + + # ───────────────── PROVISION + EMIT + VALIDATE (Render) + GATE 8.1 ───────────────── + - id: "phase5-fabricate" + name: "5–7 · Provision validator + emit CAD + validate" + type: "TYPE_ACTION" + configuration: + method: "POST" + url: "https://kineticos.onrender.com/api/stages/fabricate-report" + contentType: "application/json" + json: + job_id: "{{ root().data.job_id }}" + timeoutSeconds: 120 + successCodes: "2xx" + retry: { enabled: true, strategy: "exponential", maxAttempts: 4, intervalSeconds: 10 } + position: { x: 6320, y: 880 } + component: "http" + isCollapsed: false + - id: "gate-81" + name: "Gate 8.1 · output acceptance" + type: "TYPE_ACTION" + configuration: + method: "POST" + url: "https://kineticos.onrender.com/api/evaluate-gate" + contentType: "application/json" + json: + job_id: "{{ root().data.job_id }}" + gate: "output_acceptance" + timeoutSeconds: 30 + successCodes: "2xx" + retry: { enabled: true, strategy: "fixed", maxAttempts: 3, intervalSeconds: 5 } + position: { x: 6760, y: 880 } + component: "http" + isCollapsed: false + - id: "if-81" + name: "8.1 accepted?" + type: "TYPE_ACTION" + configuration: + expression: 'previous().data.body.decision == "proceed"' + position: { x: 7200, y: 880 } + component: "if" + isCollapsed: false + - id: "approve-81" + name: "Operator review · the v1 continuation output" + type: "TYPE_ACTION" + configuration: + items: + - type: "anyone" + position: { x: 7200, y: 1180 } + component: "approval" + isCollapsed: false + + # ───────────────── RESUME PRODUCTION ───────────────── + - id: "finalize" + name: "8.3 · Seal run + resume production" + type: "TYPE_ACTION" + configuration: + method: "POST" + url: "https://kineticos.onrender.com/api/agents/finalize" + contentType: "application/json" + json: + job_id: "{{ root().data.job_id }}" + timeoutSeconds: 30 + successCodes: "2xx" + retry: { enabled: true, strategy: "fixed", maxAttempts: 3, intervalSeconds: 5 } + position: { x: 7640, y: 880 } + component: "http" + isCollapsed: false + - id: "notify-resumed" + name: "✅ Line resumed on v1 continuation" + type: "TYPE_ACTION" + configuration: {} + position: { x: 8080, y: 880 } + component: "noop" + isCollapsed: false + - id: "halt-aborted" + name: "⛔ Halted — operator aborted" + type: "TYPE_ACTION" + configuration: {} + position: { x: 5000, y: 1560 } + component: "noop" + isCollapsed: false + + # ───────────────── annotations (documentation widgets) ───────────────── + - id: "anno-overview" + name: "overview" + type: "TYPE_WIDGET" + configuration: + color: "blue" + width: 470 + height: 360 + text: "# KineticOS agent fleet\n\n**A CAD image of a broken industrial machine comes in → a modified, 3D-printable CAD file goes out, so the line keeps running.**\n\nSuperPlane is the brain: it decides *when and whether* each agent runs and parks the job at a human gate when confidence is low. Render runs the compute (the per-job `cad-validator-{jobId}` service).\n\nEvery action node is an `http` executor calling one KineticOS agent at `/api/agents/*`. Runs end-to-end at **zero credentials** — each agent has a deterministic fallback." + position: { x: 110, y: 200 } + component: "annotation" + isCollapsed: false + - id: "anno-perception" + name: "anno-perception" + type: "TYPE_WIDGET" + configuration: + color: "yellow" + width: 520 + height: 250 + text: "## 1 · Look & identify what is wrong\n\nSix perception agents fan out **in parallel** from the ingest event:\n\n- **2A Conditioning** — is the image even usable?\n- **2B Classification** — *which* component is broken (drives everything downstream).\n- **2C Reconstruction** — rebuild the *intended, undamaged* shape (never the cracked artifact). Needs 2B.\n- **2D Dimensioning** — the mating interfaces + a terminating scale chain.\n- **2E Material** · **2F Telemetry** — material/finish + sensor-fusion cross-check.\n\nThe **Merge** node waits for all sensors, then **Assemble** fuses them into one composite-confidence score." + position: { x: 600, y: -220 } + component: "annotation" + isCollapsed: false + - id: "anno-gates" + name: "anno-gates" + type: "TYPE_WIDGET" + configuration: + color: "red" + width: 470 + height: 230 + text: "## The gate pattern (×4)\n\nEvery phase ends in the same shape:\n\n`http evaluate-gate` → **If `decision == \"proceed\"`** → next phase.\n\nOtherwise the job parks at an **Approval** node — the human gate. SuperPlane scopes it to the *exact* weak field (the class, the overlay, one caliper reading). **Approved** resumes the flow; **Rejected** routes to **Halt**.\n\nGates: 2.G confidence · 3.G strategy · 4.3 printability · 8.1 output." + position: { x: 2360, y: 400 } + component: "annotation" + isCollapsed: false + - id: "anno-design" + name: "anno-design" + type: "TYPE_WIDGET" + configuration: + color: "green" + width: 520 + height: 230 + text: "## 2 · Make a new 3D-printable CAD file\n\n**3A Sourcing** first hunts an off-the-shelf / community substitute that drops in. If none matches, **3B Generative CAD** synthesises a continuation insert through the **B1–B8** trail (parameters → feature graph → B-rep → constraints → validation → DFM → functional check → STEP/STL export).\n\nGate **3.G** sends any *generated, load-bearing* part to a human before release." + position: { x: 3240, y: 420 } + component: "annotation" + isCollapsed: false + - id: "anno-resume" + name: "anno-resume" + type: "TYPE_WIDGET" + configuration: + color: "green" + width: 470 + height: 220 + text: "## 3 · Prove it, then resume production\n\n**Phase 4** re-parameterizes the design to locally-loaded stock; **4.3** blocks if the printability margin can't meet the duty load.\n\n**Phases 5–7** spin up a dedicated, ephemeral Render `cad-validator-{jobId}` service, emit the **v1 CAD output** (OpenSCAD + BOM + runbook), and run a canary→bulk validation. **8.1** is the fan-in acceptance gate.\n\nOn acceptance, **Finalize** seals the immutable audit trail and the **line resumes** on the v1 file until the OEM part arrives." + position: { x: 6760, y: 440 } + component: "annotation" + isCollapsed: false + + edges: + # ingest → perception sensors (parallel fan-out) + - { sourceId: "ingest-trigger", targetId: "perc-conditioning", channel: "default" } + - { sourceId: "ingest-trigger", targetId: "perc-classification", channel: "default" } + - { sourceId: "ingest-trigger", targetId: "perc-dimensioning", channel: "default" } + - { sourceId: "ingest-trigger", targetId: "perc-material", channel: "default" } + - { sourceId: "ingest-trigger", targetId: "perc-telemetry", channel: "default" } + # 2B → 2C (reconstruction needs the identified part) + - { sourceId: "perc-classification", targetId: "perc-reconstruction", channel: "success" } + # sensors → merge (barrier) + - { sourceId: "perc-conditioning", targetId: "perc-merge", channel: "success" } + - { sourceId: "perc-reconstruction", targetId: "perc-merge", channel: "success" } + - { sourceId: "perc-dimensioning", targetId: "perc-merge", channel: "success" } + - { sourceId: "perc-material", targetId: "perc-merge", channel: "success" } + - { sourceId: "perc-telemetry", targetId: "perc-merge", channel: "success" } + # fuse → assemble → gate 2.G + - { sourceId: "perc-merge", targetId: "perc-assemble", channel: "success" } + - { sourceId: "perc-assemble", targetId: "gate-2g", channel: "success" } + - { sourceId: "gate-2g", targetId: "if-2g", channel: "success" } + - { sourceId: "if-2g", targetId: "design-sourcing", channel: "true" } + - { sourceId: "if-2g", targetId: "approve-2g", channel: "false" } + - { sourceId: "approve-2g", targetId: "design-sourcing", channel: "approved" } + - { sourceId: "approve-2g", targetId: "halt-aborted", channel: "rejected" } + # design: 3A sourcing → (matched? 3.G : 3B generative → 3.G) + - { sourceId: "design-sourcing", targetId: "if-sourced", channel: "success" } + - { sourceId: "if-sourced", targetId: "gate-3g", channel: "true" } + - { sourceId: "if-sourced", targetId: "design-generative", channel: "false" } + - { sourceId: "design-generative", targetId: "gate-3g", channel: "success" } + # gate 3.G + - { sourceId: "gate-3g", targetId: "if-3g", channel: "success" } + - { sourceId: "if-3g", targetId: "phase4-material", channel: "true" } + - { sourceId: "if-3g", targetId: "approve-3g", channel: "false" } + - { sourceId: "approve-3g", targetId: "phase4-material", channel: "approved" } + - { sourceId: "approve-3g", targetId: "halt-aborted", channel: "rejected" } + # printability + gate 4.3 + - { sourceId: "phase4-material", targetId: "gate-43", channel: "success" } + - { sourceId: "gate-43", targetId: "if-43", channel: "success" } + - { sourceId: "if-43", targetId: "phase5-fabricate", channel: "true" } + - { sourceId: "if-43", targetId: "approve-43", channel: "false" } + - { sourceId: "approve-43", targetId: "phase5-fabricate", channel: "approved" } + - { sourceId: "approve-43", targetId: "halt-aborted", channel: "rejected" } + # provision + emit + validate + gate 8.1 + - { sourceId: "phase5-fabricate", targetId: "gate-81", channel: "success" } + - { sourceId: "gate-81", targetId: "if-81", channel: "success" } + - { sourceId: "if-81", targetId: "finalize", channel: "true" } + - { sourceId: "if-81", targetId: "approve-81", channel: "false" } + - { sourceId: "approve-81", targetId: "finalize", channel: "approved" } + - { sourceId: "approve-81", targetId: "halt-aborted", channel: "rejected" } + # resume production + - { sourceId: "finalize", targetId: "notify-resumed", channel: "success" } diff --git a/examples/kineticos-continuation-fleet/infra/superplane/kineticos-fleet.local.canvas.yaml b/examples/kineticos-continuation-fleet/infra/superplane/kineticos-fleet.local.canvas.yaml new file mode 100644 index 0000000000..3e97c1d952 --- /dev/null +++ b/examples/kineticos-continuation-fleet/infra/superplane/kineticos-fleet.local.canvas.yaml @@ -0,0 +1,504 @@ +# ───────────────────────────────────────────────────────────────────────── +# KineticOS — the agent fleet, as a SuperPlane Canvas. +# +# THE EXACT PROCESS, end to end: +# a CAD image of an industrial machine with a broken component arrives → +# a fleet of perception agents LOOKS at it and IDENTIFIES what is wrong → +# a design fleet RECONSTRUCTS the intended geometry and GENERATES a new +# 3D-printable CAD file → printability + validation agents prove it will +# hold → and on acceptance the line RESUMES PRODUCTION on that v1 file +# until the OEM replacement arrives. +# +# SuperPlane is the BRAIN: it decides *when and whether* each agent runs, and +# it parks the job at a human gate whenever confidence, the design trail, the +# printability margin, or the output validation says so. Every TYPE_ACTION node +# below is an `http` executor that calls one KineticOS agent endpoint; the agent +# itself (Claude-backed, with a deterministic fallback) lives in the Next.js app. +# +# Replace the host `http://host.docker.internal:3000` with your KineticOS base URL +# (e.g. http://localhost:3000 for local dev) — infra/superplane/apply.sh does +# this substitution for you. The ingest trigger binds to the SuperPlane webhook +# event source that KineticOS fires as SP_INGEST_WEBHOOK on POST /api/jobs. +# ───────────────────────────────────────────────────────────────────────── +apiVersion: v1 +kind: Canvas +metadata: + name: "KineticOS — broken-machine → 3D-printable continuation fleet" + description: "Looks at a CAD image of a broken industrial machine, identifies the fault, generates a new 3D-printable CAD file, and resumes production — gated by SuperPlane at every phase." + isTemplate: false +spec: + nodes: + # ───────────────── ingest (event source) ───────────────── + - id: "ingest-trigger" + name: "Ingest: broken-machine CAD image" + type: "TYPE_TRIGGER" + configuration: {} + position: { x: 120, y: 760 } + component: "webhook" + isCollapsed: false + + # ───────────────── PERCEPTION FLEET — "look + identify what's wrong" ───────────────── + - id: "perc-conditioning" + name: "2A · Conditioning (image admissibility)" + type: "TYPE_ACTION" + configuration: + method: "POST" + url: "http://host.docker.internal:3000/api/agents/conditioning" + contentType: "application/json" + json: + job_id: "{{ root().data.job_id }}" + timeoutSeconds: 30 + successCodes: "2xx" + retry: { enabled: true, strategy: "exponential", maxAttempts: 5, intervalSeconds: 5 } + position: { x: 600, y: 160 } + component: "http" + isCollapsed: false + - id: "perc-classification" + name: "2B · Classification (locate the break)" + type: "TYPE_ACTION" + configuration: + method: "POST" + url: "http://host.docker.internal:3000/api/agents/classification" + contentType: "application/json" + json: + job_id: "{{ root().data.job_id }}" + timeoutSeconds: 30 + successCodes: "2xx" + retry: { enabled: true, strategy: "exponential", maxAttempts: 5, intervalSeconds: 5 } + position: { x: 600, y: 420 } + component: "http" + isCollapsed: false + - id: "perc-reconstruction" + name: "2C · Reconstruction (undamaged intent)" + type: "TYPE_ACTION" + configuration: + method: "POST" + url: "http://host.docker.internal:3000/api/agents/reconstruction" + contentType: "application/json" + json: + job_id: "{{ root().data.job_id }}" + timeoutSeconds: 30 + successCodes: "2xx" + retry: { enabled: true, strategy: "exponential", maxAttempts: 5, intervalSeconds: 5 } + position: { x: 1040, y: 420 } + component: "http" + isCollapsed: false + - id: "perc-dimensioning" + name: "2D · Dimensioning (interfaces + scale)" + type: "TYPE_ACTION" + configuration: + method: "POST" + url: "http://host.docker.internal:3000/api/agents/dimensioning" + contentType: "application/json" + json: + job_id: "{{ root().data.job_id }}" + timeoutSeconds: 30 + successCodes: "2xx" + retry: { enabled: true, strategy: "exponential", maxAttempts: 5, intervalSeconds: 5 } + position: { x: 600, y: 920 } + component: "http" + isCollapsed: false + - id: "perc-material" + name: "2E · Material inference" + type: "TYPE_ACTION" + configuration: + method: "POST" + url: "http://host.docker.internal:3000/api/agents/material-infer" + contentType: "application/json" + json: + job_id: "{{ root().data.job_id }}" + timeoutSeconds: 30 + successCodes: "2xx" + retry: { enabled: true, strategy: "exponential", maxAttempts: 5, intervalSeconds: 5 } + position: { x: 600, y: 1160 } + component: "http" + isCollapsed: false + - id: "perc-telemetry" + name: "2F · Telemetry fusion" + type: "TYPE_ACTION" + configuration: + method: "POST" + url: "http://host.docker.internal:3000/api/agents/telemetry" + contentType: "application/json" + json: + job_id: "{{ root().data.job_id }}" + timeoutSeconds: 30 + successCodes: "2xx" + retry: { enabled: true, strategy: "exponential", maxAttempts: 5, intervalSeconds: 5 } + position: { x: 600, y: 1400 } + component: "http" + isCollapsed: false + - id: "perc-merge" + name: "Fuse the perception sensors" + type: "TYPE_ACTION" + configuration: + enableStopIf: false + enableTimeout: true + executionTimeout: { unit: "minutes", value: 2 } + position: { x: 1480, y: 780 } + component: "merge" + isCollapsed: false + - id: "perc-assemble" + name: "Assemble PerceptionResult" + type: "TYPE_ACTION" + configuration: + method: "POST" + url: "http://host.docker.internal:3000/api/agents/perception-assemble" + contentType: "application/json" + json: + job_id: "{{ root().data.job_id }}" + timeoutSeconds: 30 + successCodes: "2xx" + retry: { enabled: true, strategy: "exponential", maxAttempts: 5, intervalSeconds: 5 } + position: { x: 1920, y: 780 } + component: "http" + isCollapsed: false + + # ───────────────── GATE 2.G · composite confidence ───────────────── + - id: "gate-2g" + name: "Gate 2.G · composite confidence" + type: "TYPE_ACTION" + configuration: + method: "POST" + url: "http://host.docker.internal:3000/api/evaluate-gate" + contentType: "application/json" + json: + job_id: "{{ root().data.job_id }}" + gate: "composite_confidence" + timeoutSeconds: 30 + successCodes: "2xx" + retry: { enabled: true, strategy: "fixed", maxAttempts: 3, intervalSeconds: 5 } + position: { x: 2360, y: 780 } + component: "http" + isCollapsed: false + - id: "if-2g" + name: "2.G clears?" + type: "TYPE_ACTION" + configuration: + expression: 'previous().data.body.decision == "proceed"' + position: { x: 2800, y: 780 } + component: "if" + isCollapsed: false + - id: "approve-2g" + name: "Operator review · confirm the broken component" + type: "TYPE_ACTION" + configuration: + items: + - type: "anyone" + position: { x: 2800, y: 1080 } + component: "approval" + isCollapsed: false + + # ───────────────── DESIGN FLEET — "make a new 3D-printable CAD file" ───────────────── + - id: "design-sourcing" + name: "3A · Sourcing (off-the-shelf substitute)" + type: "TYPE_ACTION" + configuration: + method: "POST" + url: "http://host.docker.internal:3000/api/agents/sourcing" + contentType: "application/json" + json: + job_id: "{{ root().data.job_id }}" + timeoutSeconds: 30 + successCodes: "2xx" + retry: { enabled: true, strategy: "exponential", maxAttempts: 5, intervalSeconds: 5 } + position: { x: 3240, y: 780 } + component: "http" + isCollapsed: false + - id: "if-sourced" + name: "Substitute found?" + type: "TYPE_ACTION" + configuration: + expression: "previous().data.body.matched == true" + position: { x: 3680, y: 780 } + component: "if" + isCollapsed: false + - id: "design-generative" + name: "3B · Generative CAD (B1–B8)" + type: "TYPE_ACTION" + configuration: + method: "POST" + url: "http://host.docker.internal:3000/api/agents/generative-cad" + contentType: "application/json" + json: + job_id: "{{ root().data.job_id }}" + timeoutSeconds: 45 + successCodes: "2xx" + retry: { enabled: true, strategy: "exponential", maxAttempts: 5, intervalSeconds: 5 } + position: { x: 3680, y: 1060 } + component: "http" + isCollapsed: false + + # ───────────────── GATE 3.G · continuation strategy ───────────────── + - id: "gate-3g" + name: "Gate 3.G · continuation strategy" + type: "TYPE_ACTION" + configuration: + method: "POST" + url: "http://host.docker.internal:3000/api/evaluate-gate" + contentType: "application/json" + json: + job_id: "{{ root().data.job_id }}" + gate: "continuation_strategy" + timeoutSeconds: 30 + successCodes: "2xx" + retry: { enabled: true, strategy: "fixed", maxAttempts: 3, intervalSeconds: 5 } + position: { x: 4120, y: 880 } + component: "http" + isCollapsed: false + - id: "if-3g" + name: "3.G clears?" + type: "TYPE_ACTION" + configuration: + expression: 'previous().data.body.decision == "proceed"' + position: { x: 4560, y: 880 } + component: "if" + isCollapsed: false + - id: "approve-3g" + name: "Operator review · the B1–B8 design trail" + type: "TYPE_ACTION" + configuration: + items: + - type: "anyone" + position: { x: 4560, y: 1180 } + component: "approval" + isCollapsed: false + + # ───────────────── PRINTABILITY + GATE 4.3 ───────────────── + - id: "phase4-material" + name: "4 · Printability adaptation (local stock)" + type: "TYPE_ACTION" + configuration: + method: "POST" + url: "http://host.docker.internal:3000/api/stages/material" + contentType: "application/json" + json: + job_id: "{{ root().data.job_id }}" + timeoutSeconds: 30 + successCodes: "2xx" + retry: { enabled: true, strategy: "exponential", maxAttempts: 5, intervalSeconds: 5 } + position: { x: 5000, y: 880 } + component: "http" + isCollapsed: false + - id: "gate-43" + name: "Gate 4.3 · printability feasibility" + type: "TYPE_ACTION" + configuration: + method: "POST" + url: "http://host.docker.internal:3000/api/evaluate-gate" + contentType: "application/json" + json: + job_id: "{{ root().data.job_id }}" + gate: "printability" + timeoutSeconds: 30 + successCodes: "2xx" + retry: { enabled: true, strategy: "fixed", maxAttempts: 3, intervalSeconds: 5 } + position: { x: 5440, y: 880 } + component: "http" + isCollapsed: false + - id: "if-43" + name: "4.3 clears?" + type: "TYPE_ACTION" + configuration: + expression: 'previous().data.body.decision == "proceed"' + position: { x: 5880, y: 880 } + component: "if" + isCollapsed: false + - id: "approve-43" + name: "Operator review · relax constraints or abort" + type: "TYPE_ACTION" + configuration: + items: + - type: "anyone" + position: { x: 5880, y: 1180 } + component: "approval" + isCollapsed: false + + # ───────────────── PROVISION + EMIT + VALIDATE (Render) + GATE 8.1 ───────────────── + - id: "phase5-fabricate" + name: "5–7 · Provision validator + emit CAD + validate" + type: "TYPE_ACTION" + configuration: + method: "POST" + url: "http://host.docker.internal:3000/api/stages/fabricate-report" + contentType: "application/json" + json: + job_id: "{{ root().data.job_id }}" + timeoutSeconds: 120 + successCodes: "2xx" + retry: { enabled: true, strategy: "exponential", maxAttempts: 4, intervalSeconds: 10 } + position: { x: 6320, y: 880 } + component: "http" + isCollapsed: false + - id: "gate-81" + name: "Gate 8.1 · output acceptance" + type: "TYPE_ACTION" + configuration: + method: "POST" + url: "http://host.docker.internal:3000/api/evaluate-gate" + contentType: "application/json" + json: + job_id: "{{ root().data.job_id }}" + gate: "output_acceptance" + timeoutSeconds: 30 + successCodes: "2xx" + retry: { enabled: true, strategy: "fixed", maxAttempts: 3, intervalSeconds: 5 } + position: { x: 6760, y: 880 } + component: "http" + isCollapsed: false + - id: "if-81" + name: "8.1 accepted?" + type: "TYPE_ACTION" + configuration: + expression: 'previous().data.body.decision == "proceed"' + position: { x: 7200, y: 880 } + component: "if" + isCollapsed: false + - id: "approve-81" + name: "Operator review · the v1 continuation output" + type: "TYPE_ACTION" + configuration: + items: + - type: "anyone" + position: { x: 7200, y: 1180 } + component: "approval" + isCollapsed: false + + # ───────────────── RESUME PRODUCTION ───────────────── + - id: "finalize" + name: "8.3 · Seal run + resume production" + type: "TYPE_ACTION" + configuration: + method: "POST" + url: "http://host.docker.internal:3000/api/agents/finalize" + contentType: "application/json" + json: + job_id: "{{ root().data.job_id }}" + timeoutSeconds: 30 + successCodes: "2xx" + retry: { enabled: true, strategy: "fixed", maxAttempts: 3, intervalSeconds: 5 } + position: { x: 7640, y: 880 } + component: "http" + isCollapsed: false + - id: "notify-resumed" + name: "✅ Line resumed on v1 continuation" + type: "TYPE_ACTION" + configuration: {} + position: { x: 8080, y: 880 } + component: "noop" + isCollapsed: false + - id: "halt-aborted" + name: "⛔ Halted — operator aborted" + type: "TYPE_ACTION" + configuration: {} + position: { x: 5000, y: 1560 } + component: "noop" + isCollapsed: false + + # ───────────────── annotations (documentation widgets) ───────────────── + - id: "anno-overview" + name: "overview" + type: "TYPE_WIDGET" + configuration: + color: "blue" + width: 470 + height: 360 + text: "# KineticOS agent fleet\n\n**A CAD image of a broken industrial machine comes in → a modified, 3D-printable CAD file goes out, so the line keeps running.**\n\nSuperPlane is the brain: it decides *when and whether* each agent runs and parks the job at a human gate when confidence is low. Render runs the compute (the per-job `cad-validator-{jobId}` service).\n\nEvery action node is an `http` executor calling one KineticOS agent at `/api/agents/*`. Runs end-to-end at **zero credentials** — each agent has a deterministic fallback." + position: { x: 110, y: 200 } + component: "annotation" + isCollapsed: false + - id: "anno-perception" + name: "anno-perception" + type: "TYPE_WIDGET" + configuration: + color: "yellow" + width: 520 + height: 250 + text: "## 1 · Look & identify what is wrong\n\nSix perception agents fan out **in parallel** from the ingest event:\n\n- **2A Conditioning** — is the image even usable?\n- **2B Classification** — *which* component is broken (drives everything downstream).\n- **2C Reconstruction** — rebuild the *intended, undamaged* shape (never the cracked artifact). Needs 2B.\n- **2D Dimensioning** — the mating interfaces + a terminating scale chain.\n- **2E Material** · **2F Telemetry** — material/finish + sensor-fusion cross-check.\n\nThe **Merge** node waits for all sensors, then **Assemble** fuses them into one composite-confidence score." + position: { x: 600, y: -220 } + component: "annotation" + isCollapsed: false + - id: "anno-gates" + name: "anno-gates" + type: "TYPE_WIDGET" + configuration: + color: "red" + width: 470 + height: 230 + text: "## The gate pattern (×4)\n\nEvery phase ends in the same shape:\n\n`http evaluate-gate` → **If `decision == \"proceed\"`** → next phase.\n\nOtherwise the job parks at an **Approval** node — the human gate. SuperPlane scopes it to the *exact* weak field (the class, the overlay, one caliper reading). **Approved** resumes the flow; **Rejected** routes to **Halt**.\n\nGates: 2.G confidence · 3.G strategy · 4.3 printability · 8.1 output." + position: { x: 2360, y: 400 } + component: "annotation" + isCollapsed: false + - id: "anno-design" + name: "anno-design" + type: "TYPE_WIDGET" + configuration: + color: "green" + width: 520 + height: 230 + text: "## 2 · Make a new 3D-printable CAD file\n\n**3A Sourcing** first hunts an off-the-shelf / community substitute that drops in. If none matches, **3B Generative CAD** synthesises a continuation insert through the **B1–B8** trail (parameters → feature graph → B-rep → constraints → validation → DFM → functional check → STEP/STL export).\n\nGate **3.G** sends any *generated, load-bearing* part to a human before release." + position: { x: 3240, y: 420 } + component: "annotation" + isCollapsed: false + - id: "anno-resume" + name: "anno-resume" + type: "TYPE_WIDGET" + configuration: + color: "green" + width: 470 + height: 220 + text: "## 3 · Prove it, then resume production\n\n**Phase 4** re-parameterizes the design to locally-loaded stock; **4.3** blocks if the printability margin can't meet the duty load.\n\n**Phases 5–7** spin up a dedicated, ephemeral Render `cad-validator-{jobId}` service, emit the **v1 CAD output** (OpenSCAD + BOM + runbook), and run a canary→bulk validation. **8.1** is the fan-in acceptance gate.\n\nOn acceptance, **Finalize** seals the immutable audit trail and the **line resumes** on the v1 file until the OEM part arrives." + position: { x: 6760, y: 440 } + component: "annotation" + isCollapsed: false + + edges: + # ingest → perception sensors (parallel fan-out) + - { sourceId: "ingest-trigger", targetId: "perc-conditioning", channel: "default" } + - { sourceId: "ingest-trigger", targetId: "perc-classification", channel: "default" } + - { sourceId: "ingest-trigger", targetId: "perc-dimensioning", channel: "default" } + - { sourceId: "ingest-trigger", targetId: "perc-material", channel: "default" } + - { sourceId: "ingest-trigger", targetId: "perc-telemetry", channel: "default" } + # 2B → 2C (reconstruction needs the identified part) + - { sourceId: "perc-classification", targetId: "perc-reconstruction", channel: "success" } + # sensors → merge (barrier) + - { sourceId: "perc-conditioning", targetId: "perc-merge", channel: "success" } + - { sourceId: "perc-reconstruction", targetId: "perc-merge", channel: "success" } + - { sourceId: "perc-dimensioning", targetId: "perc-merge", channel: "success" } + - { sourceId: "perc-material", targetId: "perc-merge", channel: "success" } + - { sourceId: "perc-telemetry", targetId: "perc-merge", channel: "success" } + # fuse → assemble → gate 2.G + - { sourceId: "perc-merge", targetId: "perc-assemble", channel: "success" } + - { sourceId: "perc-assemble", targetId: "gate-2g", channel: "success" } + - { sourceId: "gate-2g", targetId: "if-2g", channel: "success" } + - { sourceId: "if-2g", targetId: "design-sourcing", channel: "true" } + - { sourceId: "if-2g", targetId: "approve-2g", channel: "false" } + - { sourceId: "approve-2g", targetId: "design-sourcing", channel: "approved" } + - { sourceId: "approve-2g", targetId: "halt-aborted", channel: "rejected" } + # design: 3A sourcing → (matched? 3.G : 3B generative → 3.G) + - { sourceId: "design-sourcing", targetId: "if-sourced", channel: "success" } + - { sourceId: "if-sourced", targetId: "gate-3g", channel: "true" } + - { sourceId: "if-sourced", targetId: "design-generative", channel: "false" } + - { sourceId: "design-generative", targetId: "gate-3g", channel: "success" } + # gate 3.G + - { sourceId: "gate-3g", targetId: "if-3g", channel: "success" } + - { sourceId: "if-3g", targetId: "phase4-material", channel: "true" } + - { sourceId: "if-3g", targetId: "approve-3g", channel: "false" } + - { sourceId: "approve-3g", targetId: "phase4-material", channel: "approved" } + - { sourceId: "approve-3g", targetId: "halt-aborted", channel: "rejected" } + # printability + gate 4.3 + - { sourceId: "phase4-material", targetId: "gate-43", channel: "success" } + - { sourceId: "gate-43", targetId: "if-43", channel: "success" } + - { sourceId: "if-43", targetId: "phase5-fabricate", channel: "true" } + - { sourceId: "if-43", targetId: "approve-43", channel: "false" } + - { sourceId: "approve-43", targetId: "phase5-fabricate", channel: "approved" } + - { sourceId: "approve-43", targetId: "halt-aborted", channel: "rejected" } + # provision + emit + validate + gate 8.1 + - { sourceId: "phase5-fabricate", targetId: "gate-81", channel: "success" } + - { sourceId: "gate-81", targetId: "if-81", channel: "success" } + - { sourceId: "if-81", targetId: "finalize", channel: "true" } + - { sourceId: "if-81", targetId: "approve-81", channel: "false" } + - { sourceId: "approve-81", targetId: "finalize", channel: "approved" } + - { sourceId: "approve-81", targetId: "halt-aborted", channel: "rejected" } + # resume production + - { sourceId: "finalize", targetId: "notify-resumed", channel: "success" } diff --git a/examples/kineticos-continuation-fleet/infra/superplane/smoke.sh b/examples/kineticos-continuation-fleet/infra/superplane/smoke.sh new file mode 100755 index 0000000000..f183bf9fba --- /dev/null +++ b/examples/kineticos-continuation-fleet/infra/superplane/smoke.sh @@ -0,0 +1,52 @@ +#!/usr/bin/env bash +# Drive the KineticOS agent fleet end-to-end over HTTP, in the EXACT order the +# SuperPlane canvas does — a faithful offline rehearsal of a canvas run. Runs at +# zero credentials (every agent has a deterministic fallback). +# +# infra/superplane/smoke.sh [APP_BASE_URL] (default http://localhost:3000) +# +# Start the app first: npm run dev +set -euo pipefail +APP="${1:-http://localhost:3000}" + +post() { # post + curl -fsS -X POST "$APP$1" -H 'content-type: application/json' -d "$2" +} +field() { python3 -c 'import sys,json;d=json.load(sys.stdin);print(d.get(sys.argv[1],""))' "$1"; } +show() { python3 -m json.tool; } + +echo "▸ ingest a broken-gearbox CAD image" +JOB=$(post /api/jobs '{"cadImageUris":["s3://demo/gearbox-stage2-cad.png"],"telemetryUri":"s3://demo/gearbox-vibration.csv","assemblyContext":"gearbox stage 2 — driven idler","failureNote":"sheared tooth on idler gear"}' | field jobId) +[ -n "$JOB" ] || { echo "✗ no jobId — is the app running at $APP?"; exit 1; } +echo " job = $JOB" +B="{\"job_id\":\"$JOB\"}" + +echo "▸ perception fleet (2A–2F fan out, then fuse)" +for a in conditioning classification dimensioning material-infer telemetry; do + printf ' %-16s ' "$a"; post "/api/agents/$a" "$B" | python3 -c 'import sys,json;print(json.load(sys.stdin))' +done +printf ' %-16s ' reconstruction; post /api/agents/reconstruction "$B" | python3 -c 'import sys,json;print(json.load(sys.stdin))' +echo "▸ assemble PerceptionResult"; post /api/agents/perception-assemble "$B" | show + +echo "▸ Gate 2.G · composite confidence" +post /api/evaluate-gate "{\"job_id\":\"$JOB\",\"gate\":\"composite_confidence\"}" | show + +echo "▸ design fleet (3A sourcing → else 3B generative)" +MATCHED=$(post /api/agents/sourcing "$B" | field matched) +echo " sourcing matched = $MATCHED" +if [ "$MATCHED" != "True" ] && [ "$MATCHED" != "true" ]; then + echo " → 3B generative continuation CAD (B1–B8)" + post /api/agents/generative-cad "$B" | show +fi + +echo "▸ Gate 3.G · continuation strategy" +post /api/evaluate-gate "{\"job_id\":\"$JOB\",\"gate\":\"continuation_strategy\"}" | show + +echo "▸ Phase 4 · printability adaptation"; post /api/stages/material "$B" | show +echo "▸ Gate 4.3 · printability"; post /api/evaluate-gate "{\"job_id\":\"$JOB\",\"gate\":\"printability\"}" | show + +echo "▸ Phases 5–7 · provision validator + emit CAD + validate"; post /api/stages/fabricate-report "$B" | show +echo "▸ Gate 8.1 · output acceptance"; post /api/evaluate-gate "{\"job_id\":\"$JOB\",\"gate\":\"output_acceptance\"}" | show + +echo "▸ 8.3 · seal run + resume production"; post /api/agents/finalize "$B" | show +echo "✅ fleet complete — production resumed on the v1 continuation (job $JOB)" diff --git a/examples/kineticos-continuation-fleet/src/app/api/agents/[agent]/route.ts b/examples/kineticos-continuation-fleet/src/app/api/agents/[agent]/route.ts new file mode 100644 index 0000000000..c4f861171b --- /dev/null +++ b/examples/kineticos-continuation-fleet/src/app/api/agents/[agent]/route.ts @@ -0,0 +1,328 @@ +// ───────────────────────────────────────────────────────────────────────── +// THE AGENT FLEET — one agent per call. POST /api/agents/{agent} +// +// The coarse /api/stages/* endpoints run a whole phase per call. These expose +// ONE agent each, so a SuperPlane Canvas can drive the *exact process* at full +// granularity: fan the six perception sensors out in parallel, fuse them, branch +// the two design tracks, and gate between every phase. Each handler: +// 1. loads the Job from the store (shared with the worker + coarse endpoints), +// 2. runs exactly ONE agent module from src/agents/** (no domain logic here), +// 3. persists its slice — perception sensors stash into an in-process scratch +// keyed by jobId; `perception-assemble` composes the PerceptionResult from +// it exactly as the worker does and writes job.perception, +// 4. returns small flat JSON the Canvas routes on (see src/lib/contract.ts). +// +// Runs at zero credentials: every agent has a deterministic fallback, so the +// whole fleet executes end-to-end offline. See infra/superplane/ for the Canvas +// that wires these into nodes, gates, and the resume-production terminal. +// ───────────────────────────────────────────────────────────────────────── + +import { NextResponse } from "next/server"; +import type { + Dimensions, + IdentifiedPart, + ImageAdmissibility, + Job, + MaterialClass, + PerceptionResult, + ReconstructedGeometry, + ScaleSource, +} from "@/lib/types"; +import type { + AgentName, + ClassificationAgentResponse, + ConditioningAgentResponse, + DimensioningAgentResponse, + FinalizeAgentResponse, + GenerativeCadAgentResponse, + MaterialInferAgentResponse, + PerceptionAssembleResponse, + ReconstructionAgentResponse, + SourcingAgentResponse, + TelemetryAgentResponse, +} from "@/lib/contract"; +import { appendAudit, upsertJob } from "@/lib/store"; +import { createAuditEntry } from "@/lib/audit"; +import { runConditioning } from "@/agents/perception/conditioning"; +import { runClassification } from "@/agents/perception/classification"; +import { runReconstruction } from "@/agents/perception/reconstruction"; +import { runDimensioning } from "@/agents/perception/dimensioning"; +import { runMaterialInference } from "@/agents/perception/material"; +import { runTelemetry } from "@/agents/perception/telemetry"; +import { runSourcing } from "@/agents/design/sourcing"; +import { runGenerativeCad } from "@/agents/design/generative-cad"; +import { compositeConfidence, errResponse, isLoadBearing, resolveJob } from "../../stages/_lib"; + +export const runtime = "nodejs"; +export const dynamic = "force-dynamic"; + +// ───────────────────────── perception scratch (in-process) ───────────────────────── +// The six perception sensors run as independent nodes, so their outputs are +// stashed here keyed by jobId until `perception-assemble` composes them into the +// canonical PerceptionResult. Pinned to globalThis so every Next.js route bundle +// shares one map (the store/worker/superplane pattern). Cleared on assemble. +interface PerceptionScratch { + admissibility?: ImageAdmissibility[]; + identifiedPart?: IdentifiedPart; + reconstructedGeometry?: ReconstructedGeometry; + dimensions?: Dimensions; + scaleSource?: ScaleSource | null; + dimensionalConfidence?: number; + inferredMaterialClass?: MaterialClass; + surfaceFinish?: string; + failureMode?: string | null; + sensorFusionAgreement?: number | null; +} +const g = globalThis as unknown as { + __kineticosScratch?: Map; +}; +const scratchStore: Map = + g.__kineticosScratch ?? (g.__kineticosScratch = new Map()); + +function scratch(jobId: string): PerceptionScratch { + let s = scratchStore.get(jobId); + if (!s) { + s = {}; + scratchStore.set(jobId, s); + } + return s; +} + +// Ordered for the GET discovery view + the Canvas layout. +const AGENT_ORDER: AgentName[] = [ + "conditioning", + "classification", + "reconstruction", + "dimensioning", + "material-infer", + "telemetry", + "perception-assemble", + "sourcing", + "generative-cad", + "finalize", +]; + +// ───────────────────────── the dispatch ───────────────────────── +type Handler = (job: Job) => Promise; + +const HANDLERS: Record = { + // 2A — image admissibility + async conditioning(job) { + const admissibility = (await runConditioning(job)).data; + scratch(job.jobId).admissibility = admissibility; + const body: ConditioningAgentResponse = { + job_id: job.jobId, + admissible: admissibility.filter((a) => a.admissible).length, + images: admissibility.length, + }; + return NextResponse.json(body); + }, + + // 2B — broken-component localization (drives every later stage) + async classification(job) { + const identified = (await runClassification(job)).data; + scratch(job.jobId).identifiedPart = identified; + const body: ClassificationAgentResponse = { + job_id: job.jobId, + part_class: identified.partClass, + conf_class: identified.confClass, + ambiguous_class: identified.ambiguousClass, + additional_parts: identified.additionalParts?.length ?? 0, + load_bearing: isLoadBearing(identified.partClass), + }; + return NextResponse.json(body); + }, + + // 2C — reconstruct the undamaged intent (needs 2B's identified part) + async reconstruction(job) { + const identified = scratch(job.jobId).identifiedPart; + if (!identified) { + return errResponse(409, "classification (2B) must run before reconstruction (2C)"); + } + const reconstructed = (await runReconstruction(job, identified)).data; + scratch(job.jobId).reconstructedGeometry = reconstructed; + const body: ReconstructionAgentResponse = { + job_id: job.jobId, + method: reconstructed.method, + reconstruction_confidence: reconstructed.reconstructionConfidence, + failure_class: reconstructed.failureClass, + has_geometry: reconstructed.uri !== null, + }; + return NextResponse.json(body); + }, + + // 2D — mating interfaces + terminal scale chain + async dimensioning(job) { + const dim = (await runDimensioning(job)).data; + const s = scratch(job.jobId); + s.dimensions = dim.dimensions; + s.scaleSource = dim.scaleSource; + s.dimensionalConfidence = dim.dimensionalConfidence; + const body: DimensioningAgentResponse = { + job_id: job.jobId, + scale_source: dim.scaleSource, + dimensional_confidence: dim.dimensionalConfidence, + parameters: Object.keys(dim.dimensions).length, + }; + return NextResponse.json(body); + }, + + // 2E — material + surface inference + async "material-infer"(job) { + const material = (await runMaterialInference(job)).data; + const s = scratch(job.jobId); + s.inferredMaterialClass = material.inferredMaterialClass; + s.surfaceFinish = material.surfaceFinish; + const body: MaterialInferAgentResponse = { + job_id: job.jobId, + inferred_material_class: material.inferredMaterialClass, + surface_finish: material.surfaceFinish, + }; + return NextResponse.json(body); + }, + + // 2F — telemetry track + sensor fusion + async telemetry(job) { + const tele = (await runTelemetry(job)).data; + const s = scratch(job.jobId); + s.failureMode = tele.failureMode; + s.sensorFusionAgreement = tele.sensorFusionAgreement; + const body: TelemetryAgentResponse = { + job_id: job.jobId, + failure_mode: tele.failureMode, + sensor_fusion_agreement: tele.sensorFusionAgreement, + }; + return NextResponse.json(body); + }, + + // 2.x — fuse the six sensors into the canonical PerceptionResult (worker parity) + async "perception-assemble"(job) { + const s = scratch(job.jobId); + if (!s.identifiedPart || !s.reconstructedGeometry || !s.dimensions || !s.admissibility) { + return errResponse( + 409, + "run conditioning, classification, reconstruction and dimensioning before perception-assemble", + ); + } + const perception: PerceptionResult = { + admissibility: s.admissibility, + identifiedPart: s.identifiedPart, + reconstructedGeometry: s.reconstructedGeometry, + dimensions: s.dimensions, + scaleSource: s.scaleSource ?? null, + dimensionalConfidence: s.dimensionalConfidence ?? 0, + inferredMaterialClass: s.inferredMaterialClass ?? "unknown", + surfaceFinish: s.surfaceFinish ?? "unknown", + failureMode: s.failureMode ?? null, + sensorFusionAgreement: s.sensorFusionAgreement ?? null, + confTotal: compositeConfidence( + s.identifiedPart.confClass, + s.dimensionalConfidence ?? 0, + s.reconstructedGeometry.reconstructionConfidence, + s.sensorFusionAgreement ?? null, + ), + }; + job.perception = perception; + await upsertJob(job); + scratchStore.delete(job.jobId); + await safeAudit(job.jobId, "perception-fleet", "assemble", `composite confidence ${perception.confTotal.toFixed(2)}`); + + const body: PerceptionAssembleResponse = { + job_id: job.jobId, + conf_total: perception.confTotal, + conf_class: perception.identifiedPart.confClass, + dimensional_confidence: perception.dimensionalConfidence, + reconstruction_confidence: perception.reconstructedGeometry.reconstructionConfidence, + sensor_fusion_agreement: perception.sensorFusionAgreement, + ambiguous_class: perception.identifiedPart.ambiguousClass, + scale_source: perception.scaleSource, + load_bearing: isLoadBearing(perception.identifiedPart.partClass), + }; + return NextResponse.json(body); + }, + + // 3A — off-the-shelf substitute federation (null → fall through to 3B) + async sourcing(job) { + if (!job.perception) return errResponse(409, "perception must run before sourcing (3A)"); + const blueprint = (await runSourcing(job)).data; + if (blueprint) { + job.blueprint = blueprint; + await upsertJob(job); + } + const body: SourcingAgentResponse = { + job_id: job.jobId, + matched: blueprint !== null, + source_type: blueprint?.sourceType ?? null, + match_score: blueprint?.matchScore ?? null, + strategy: blueprint?.strategy ?? null, + }; + return NextResponse.json(body); + }, + + // 3B — generative continuation CAD, B1..B8 (the new 3D-printable file) + async "generative-cad"(job) { + if (!job.perception) return errResponse(409, "perception must run before generative-cad (3B)"); + const blueprint = (await runGenerativeCad(job)).data; + job.blueprint = blueprint; + await upsertJob(job); + const body: GenerativeCadAgentResponse = { + job_id: job.jobId, + source_type: "generated", + strategy: blueprint.strategy, + load_bearing: isLoadBearing(job.perception.identifiedPart.partClass), + design_trail: blueprint.designTrail, + }; + return NextResponse.json(body); + }, + + // 8.3 — seal the run; the line is resumed on the accepted v1 continuation + async finalize(job) { + job.status = "complete"; + job.pendingGate = null; + job.auditTrail.push( + createAuditEntry( + "fleet", + "complete", + "continuation accepted — audit sealed, production resumed on the v1 CAD output", + ), + ); + await upsertJob(job); + const body: FinalizeAgentResponse = { + job_id: job.jobId, + status: job.status, + resumed: true, + }; + return NextResponse.json(body); + }, +}; + +async function safeAudit(jobId: string, actor: string, action: string, detail: string): Promise { + try { + await appendAudit(jobId, createAuditEntry(actor, action, detail)); + } catch { + // never fail the fleet on an audit write + } +} + +// ───────────────────────── route handlers ───────────────────────── +export async function POST( + req: Request, + { params }: { params: Promise<{ agent: string }> }, +) { + const { agent } = await params; + const handler = HANDLERS[agent as AgentName]; + if (!handler) { + return errResponse(404, `unknown agent "${agent}" — one of: ${AGENT_ORDER.join(", ")}`); + } + + let body: { job_id?: unknown }; + try { + body = (await req.json()) as { job_id?: unknown }; + } catch { + return errResponse(400, "invalid JSON body"); + } + const resolved = await resolveJob(body.job_id); + if ("error" in resolved) return resolved.error; + + return handler(resolved.job); +} diff --git a/examples/kineticos-continuation-fleet/src/app/api/agents/route.ts b/examples/kineticos-continuation-fleet/src/app/api/agents/route.ts new file mode 100644 index 0000000000..300364553f --- /dev/null +++ b/examples/kineticos-continuation-fleet/src/app/api/agents/route.ts @@ -0,0 +1,26 @@ +// GET /api/agents — the fleet roster: every agent the Canvas can call, in +// execution order, with the phase it implements. Discovery endpoint for the +// SuperPlane Canvas builder + the docs in infra/superplane/. + +import { NextResponse } from "next/server"; +import type { AgentName } from "@/lib/contract"; + +export const runtime = "nodejs"; +export const dynamic = "force-dynamic"; + +const FLEET: { agent: AgentName; phase: string; label: string }[] = [ + { agent: "conditioning", phase: "2A", label: "CAD image admissibility" }, + { agent: "classification", phase: "2B", label: "broken-component localization" }, + { agent: "reconstruction", phase: "2C", label: "reconstruct the undamaged intent" }, + { agent: "dimensioning", phase: "2D", label: "mating interfaces + scale chain" }, + { agent: "material-infer", phase: "2E", label: "material + surface inference" }, + { agent: "telemetry", phase: "2F", label: "telemetry track + sensor fusion" }, + { agent: "perception-assemble", phase: "2.x", label: "fuse sensors → PerceptionResult" }, + { agent: "sourcing", phase: "3A", label: "off-the-shelf substitute federation" }, + { agent: "generative-cad", phase: "3B", label: "generative continuation CAD (B1–B8)" }, + { agent: "finalize", phase: "8.3", label: "seal the run, resume production" }, +]; + +export async function GET() { + return NextResponse.json({ fleet: FLEET, count: FLEET.length }); +} diff --git a/examples/kineticos-continuation-fleet/src/lib/contract.ts b/examples/kineticos-continuation-fleet/src/lib/contract.ts new file mode 100644 index 0000000000..221c8c3904 --- /dev/null +++ b/examples/kineticos-continuation-fleet/src/lib/contract.ts @@ -0,0 +1,176 @@ +// ───────────────────────────────────────────────────────────────────────── +// THE SHARED CONTRACT (types only) — the A↔B seam. +// +// The pivot divides at one seam: the SuperPlane App (Workstream A) calls this +// Next app (Workstream B) over HTTP, and this app fires the Canvas webhook. +// This file is the single source of truth for the wire shapes both sides agree +// on. Person A imports the request/response types to build Canvas nodes; the +// stage endpoints under src/app/api/stages/** + src/app/api/evaluate-gate +// implement them verbatim. +// +// Wire fields are snake_case (so Canvas `if` expressions stay simple and flat); +// the internal Job slices in ./types are camelCase. Each endpoint maps one to +// the other. This module re-uses the canonical enums from ./types so the wire +// contract can never silently drift from the domain model. +// ───────────────────────────────────────────────────────────────────────── + +import type { + BlueprintSourceType, + DesignDecision, + GateDecisionType, + GateName, + MachineTarget, + ScaleSource, +} from "./types"; + +// ───────────────────────── stage endpoints (B implements, A calls) ───────────────────────── +// Every stage endpoint takes exactly this, loads the Job from the store, runs +// the existing agent module(s), persists the Job slice, and returns flat JSON. +export interface StageRequest { + job_id: string; +} + +/** POST /api/stages/perception → the composite-confidence routing fields. */ +export interface PerceptionStageResponse { + job_id: string; + conf_total: number; + conf_class: number; + dimensional_confidence: number; + reconstruction_confidence: number; + sensor_fusion_agreement: number | null; // null when no telemetry present + ambiguous_class: boolean; + scale_source: ScaleSource | null; + load_bearing: boolean; +} + +/** POST /api/stages/design → the continuation-strategy routing fields. */ +export interface DesignStageResponse { + job_id: string; + source_type: BlueprintSourceType; // "sourced" | "generated" + match_score: number | null; // Track A only; null for a generated continuation + cad_uri_step: string | null; + cad_uri_stl: string | null; + load_bearing: boolean; + // Additive (beyond the flat wire contract): the B1..B8 design trail. The + // Canvas routes only on the flat fields and ignores this; the UI/audit render + // it. null on a sourced (Track A) substitute, which has no synthesis trail. + design_trail: DesignDecision[] | null; +} + +/** POST /api/stages/material → printability + structural-feasibility fields. */ +export interface MaterialStageResponse { + job_id: string; + machine_target: MachineTarget | null; // "fdm" | "sla" | "cnc" + toolpath_uri: string | null; + structural_ok: boolean; + margin_pct: number; +} + +/** POST /api/stages/fabricate-report → output-validation (QA) fields. */ +export interface FabricateReportResponse { + job_id: string; + dimensional_pass: boolean | null; + structural_pass: boolean | null; + in_process_anomalies: number; +} + +// ───────────────────────── gate evaluation (optional convenience) ───────────────────────── +// A can route purely on the confidence numbers with Canvas `if` nodes, OR call +// this to reuse src/lib/gates/index.ts policy verbatim. +export interface EvaluateGateRequest { + job_id: string; + gate: GateName; // composite_confidence | continuation_strategy | printability | output_acceptance +} + +export interface EvaluateGateResponse { + decision: GateDecisionType; // "proceed" | "human_review" | "block" + reason: string; + scoped_field: string | null; // the precise field a human must resolve, if any +} + +// ───────────────────────── granular agent fleet (B implements, A calls) ───────────────────────── +// The coarse stage endpoints above run a whole phase per call. The agent-fleet +// endpoints under /api/agents/[agent] expose ONE agent per call, so a SuperPlane +// Canvas can fan the perception sensors out in parallel, branch the two design +// tracks, and render the fleet at full granularity. Each takes { job_id }, runs +// exactly one agent, persists its slice (perception sensors persist to an +// in-process scratch the `perception-assemble` agent then composes), and returns +// a small flat result the Canvas routes on. See infra/superplane/. +export type AgentName = + | "conditioning" // 2A image admissibility + | "classification" // 2B broken-component localization + | "reconstruction" // 2C undamaged-intent reconstruction (needs 2B) + | "dimensioning" // 2D mating interfaces + scale chain + | "material-infer" // 2E material + surface inference + | "telemetry" // 2F telemetry track + sensor fusion + | "perception-assemble" // 2.x compose PerceptionResult from the sensors + | "sourcing" // 3A off-the-shelf substitute federation + | "generative-cad" // 3B generative continuation CAD (B1–B8) + | "finalize"; // 8.3 seal the run, mark the line resumed + +export interface ConditioningAgentResponse { + job_id: string; + admissible: number; // count of admissible images + images: number; // total images assessed +} +export interface ClassificationAgentResponse { + job_id: string; + part_class: string; + conf_class: number; + ambiguous_class: boolean; + additional_parts: number; // other broken components in the same image + load_bearing: boolean; +} +export interface ReconstructionAgentResponse { + job_id: string; + method: string; + reconstruction_confidence: number; + failure_class: string | null; + has_geometry: boolean; // false → confidence below gate → human overlay +} +export interface DimensioningAgentResponse { + job_id: string; + scale_source: ScaleSource | null; + dimensional_confidence: number; + parameters: number; // count of resolved dimensional parameters +} +export interface MaterialInferAgentResponse { + job_id: string; + inferred_material_class: string; + surface_finish: string; +} +export interface TelemetryAgentResponse { + job_id: string; + failure_mode: string | null; + sensor_fusion_agreement: number | null; +} +/** perception-assemble returns the SAME routing shape as the coarse endpoint. */ +export type PerceptionAssembleResponse = PerceptionStageResponse; +export interface SourcingAgentResponse { + job_id: string; + matched: boolean; // false → fall through to the generative track (3B) + source_type: BlueprintSourceType | null; + match_score: number | null; + strategy: string | null; +} +export interface GenerativeCadAgentResponse { + job_id: string; + source_type: "generated"; + strategy: string; + load_bearing: boolean; + design_trail: DesignDecision[] | null; // B1..B8 +} +export interface FinalizeAgentResponse { + job_id: string; + status: string; // "complete" + resumed: boolean; // production resumed on the v1 continuation +} + +// ───────────────────────── intake → Canvas webhook (A defines, B fires) ───────────────────────── +// POST /api/jobs creates the Job, then POSTs this to SP_INGEST_WEBHOOK with the +// header `X-Webhook-Token: `. (Firing is Workstream B2.) +export interface IngestWebhookPayload { + job_id: string; + image_uris: string[]; + telemetry_uri: string | null; +}