From f545f49932596e79a01ed648ef6c9808ba8172bc Mon Sep 17 00:00:00 2001 From: Maya Wang Date: Tue, 23 Jun 2026 03:14:21 +0000 Subject: [PATCH 1/3] feat(demos): add NanoClaw 1.5x multiplexing demo - Implement high-fidelity Substrate multiplex dashboard (V11.16.1) - Add semantic heatmap telemetry and real-time duty cycle tracking - Include decoupled Kubernetes CronJob orchestration (Claw pattern) - Add lightweight NanoClaw agent server workload - Standardize naming to sub-agent for compliance --- demos/sub-agent-multiplex/Dockerfile | 74 +++ demos/sub-agent-multiplex/README.md | 98 +++ demos/sub-agent-multiplex/package.json | 20 + .../sub-agent-multiplex.yaml.tmpl | 84 +++ demos/sub-agent-multiplex/tsconfig.json | 14 + demos/sub-agent-multiplex/ui/demo-ui.ts | 573 ++++++++++++++++++ demos/sub-agent-multiplex/workload/agent.ts | 90 +++ hack/install-ate.sh | 39 +- hack/install-demo-sub-agent-multiplex.sh | 84 +++ 9 files changed, 1048 insertions(+), 28 deletions(-) create mode 100644 demos/sub-agent-multiplex/Dockerfile create mode 100644 demos/sub-agent-multiplex/README.md create mode 100644 demos/sub-agent-multiplex/package.json create mode 100644 demos/sub-agent-multiplex/sub-agent-multiplex.yaml.tmpl create mode 100644 demos/sub-agent-multiplex/tsconfig.json create mode 100644 demos/sub-agent-multiplex/ui/demo-ui.ts create mode 100644 demos/sub-agent-multiplex/workload/agent.ts create mode 100644 hack/install-demo-sub-agent-multiplex.sh diff --git a/demos/sub-agent-multiplex/Dockerfile b/demos/sub-agent-multiplex/Dockerfile new file mode 100644 index 000000000..87cae6f3d --- /dev/null +++ b/demos/sub-agent-multiplex/Dockerfile @@ -0,0 +1,74 @@ +# NanoClaw on Agent Substrate PoC +# Portable Dockerfile for OSS Substrate Migration + +# Stage 1: Build the standalone bundles +FROM node:22-slim AS builder + +WORKDIR /app + +# Install build dependencies +RUN apt-get update && apt-get install -y --no-install-recommends \ + ca-certificates \ + curl \ + && rm -rf /var/lib/apt/lists/* + +# Copy standalone package files +COPY package.json ./ +# Use npm install for simplicity and portability in the standalone package +RUN npm install + +# Copy source code +COPY ui/ ./ui/ +COPY workload/ ./workload/ + +# Build zero-dependency bundles +RUN ./node_modules/.bin/esbuild workload/agent.ts \ + --bundle \ + --platform=node \ + --target=node22 \ + --outfile=dist/agent.js \ + --external:node:* + +RUN ./node_modules/.bin/esbuild ui/demo-ui.ts \ + --bundle \ + --platform=node \ + --target=node22 \ + --outfile=dist/demo-ui.js \ + --external:node:* + +# Stage 2: Final Production Image +FROM node:22-slim AS runner + +WORKDIR /app + +# Copy the entire context to check for local binaries +COPY . . + +# Install runtime dependencies (tini for signal forwarding, kubectl for dashboard sync) +RUN apt-get update && apt-get install -y --no-install-recommends \ + ca-certificates \ + curl \ + tini \ + && curl -LO "https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl" \ + && chmod +x kubectl \ + && mv kubectl /usr/local/bin/ \ + && rm -rf /var/lib/apt/lists/* + +# Copy built assets +COPY --from=builder /app/dist/ ./dist/ +# Copy kubectl-ate binary if it exists in context, otherwise download it +# This makes the Dockerfile portable across environments +RUN if [ -f "./kubectl-ate" ]; then \ + mv ./kubectl-ate /usr/local/bin/kubectl-ate; \ + else \ + curl -L -o /usr/local/bin/kubectl-ate https://github.com/agent-substrate/substrate/releases/latest/download/kubectl-ate-linux-amd64; \ + fi && chmod +x /usr/local/bin/kubectl-ate + +# Create a /pause hook for Substrate rehydration +RUN echo '#!/bin/sh' > /pause && \ + echo 'echo "[pause] Starting NanoClaw agent..."' >> /pause && \ + echo 'exec /usr/bin/tini -- /usr/local/bin/node /app/dist/agent.js' >> /pause && \ + chmod +x /pause + +# Default entrypoint (can be overridden by deployment to run demo-ui) +ENTRYPOINT ["/usr/bin/tini", "--", "node", "dist/agent.js"] diff --git a/demos/sub-agent-multiplex/README.md b/demos/sub-agent-multiplex/README.md new file mode 100644 index 000000000..62360fa4b --- /dev/null +++ b/demos/sub-agent-multiplex/README.md @@ -0,0 +1,98 @@ +# Substrate Multiplex Demo: NanoClaw 1.5x Overcommit + +This demo demonstrates the extreme efficiency gains possible with Google Substrate by multiplexing **3 logical NanoClaw agents** onto **2 physical substrate workers** (1.5x density ratio). + +## System Information + +- **Agent Framework**: NanoClaw (v2.x) +- **Source**: `github.com/nanocoai/nanoclaw` +- **Substrate Mode**: Multi-Actor Multiplexing (1.5x oversubscription) +- **Runtime**: Bun (Node.js compatible) inside Debian Slim +- **Isolation**: gVisor (runsc) + +## What this shows + +- **High-Density Multiplexing**: Three logical agent identities running on only two physical pods (1.5x oversubscription). +- **State Persistence**: A `taskCounter` maintained in the Node.js process memory survives multiple suspend/resume cycles. +- **Dynamic Rotation**: Agents finish work at different times (3-6s), forcing Substrate to constantly rotate pod ownership. +- **Visual Identity Tracking**: Color-coded agents (Blue/Pink/Gold) and live log tailing to make infrastructure sharing intuitively obvious. + +## Audience + +This guide is intended for engineers exploring Agent Substrate for hosting large-scale agentic workloads where cost-efficiency and stateful rehydration are critical. + +## Prerequisites + +- **Agent Substrate Cluster**: A Kubernetes cluster with Substrate installed. +- **Docker**: For building and pushing the unified actor/UI image. +- **GCS Bucket**: Configured for Substrate state snapshots (e.g., `gs://snapshot-substrate-gke-ai-eco-dev/`). +- **kubectl & kubectl-ate**: The Substrate CLI tool for managing logical actors. + +## Components + +| Path | Purpose | +|---|---| +| `workload/agent.ts` | The workload: A NanoClaw/Hono server with persistent memory state. | +| `ui/demo-ui.ts` | The dashboard: A Node.js backend providing live logs, task queueing, and visual tracking. | +| `sub-agent-multiplex.yaml.tmpl` | Kubernetes manifests for ActorTemplates and WorkerPools. | +| `Dockerfile` | Unified OCI image containing both the actor workload and the dashboard UI. | + +## How to Run + +### 1. Provision Hardware +Scale the physical `WorkerPool` to the desired replica count (2 for this demo): +```bash +kubectl apply -f sub-agent-multiplex.yaml +``` + +### 2. Deploy Logical Agents +Create the three "fun-named" actors using the Substrate CLI. +```bash +kubectl-ate create actor agent-luna-v12 --template sub-agent/sub-agent-agent +kubectl-ate create actor agent-mars-v12 --template sub-agent/sub-agent-agent +kubectl-ate create actor agent-nova-v11 --template sub-agent/sub-agent-agent +``` + +### 3. Launch the Dashboard +The dashboard runs as a standard Kubernetes Deployment with a LoadBalancer. +```bash +kubectl apply -f demo-ui.yaml +``` + +## Drive the Demo + +Open the dashboard and use the following interaction patterns: + +- **Pulse (Manual Wakeup)**: Trigger tasks across the registry. Watch the **colored icons** rapidly cycle through the 2 worker slots. +- **Live Logs**: Observe the MT Broker logs. You will see different Agent IDs appearing in the **same log stream**, proving that physical hardware is being recycled in real-time. +- **Cron Tracker**: Observe real-time countdowns as the automated schedule triggers orchestration events. + +## Integrating a Real LLM API + +Integrating an LLM into a NanoClaw logical actor is straightforward. Because Substrate persists the **entire process memory**, any in-memory conversation history or KV-cache will survive multiple suspend/resume cycles without requiring an external database. + +### 1. Add the LLM SDK +Add your preferred SDK (e.g., OpenAI or Anthropic) to the `package.json`: +```bash +npm install openai +``` + +### 2. Update the Actor Logic +Modify `workload/agent.ts` to initialize the client and maintain a local chat history: +```typescript +import OpenAI from "openai"; + +const openai = new OpenAI({ apiKey: process.env.LLM_API_KEY }); +let history: any[] = []; // This array will survive Substrate snapshots! + +app.post("/v1/chat", async (c) => { + const { message } = await c.req.json(); + history.push({ role: "user", content: message }); + + const response = await openai.chat.completions.create({ + model: "gpt-4", + messages: history, + }); + // ... process response +}); +``` diff --git a/demos/sub-agent-multiplex/package.json b/demos/sub-agent-multiplex/package.json new file mode 100644 index 000000000..d0993c28a --- /dev/null +++ b/demos/sub-agent-multiplex/package.json @@ -0,0 +1,20 @@ +{ + "name": "sub-agent-multiplex", + "version": "1.0.0", + "description": "NanoClaw on Agent Substrate PoC", + "private": true, + "scripts": { + "build": "esbuild src/agent.ts --bundle --platform=node --target=node22 --outfile=dist/agent.js --external:node:* && esbuild src/demo-ui.ts --bundle --platform=node --target=node22 --outfile=dist/demo-ui.js --external:node:*", + "start:ui": "node dist/demo-ui.js", + "start:agent": "node dist/agent.js" + }, + "dependencies": { + "@hono/node-server": "^1.11.1", + "hono": "^4.4.2" + }, + "devDependencies": { + "@types/node": "^22.0.0", + "esbuild": "^0.21.5", + "typescript": "^5.5.2" + } +} diff --git a/demos/sub-agent-multiplex/sub-agent-multiplex.yaml.tmpl b/demos/sub-agent-multiplex/sub-agent-multiplex.yaml.tmpl new file mode 100644 index 000000000..9a9851124 --- /dev/null +++ b/demos/sub-agent-multiplex/sub-agent-multiplex.yaml.tmpl @@ -0,0 +1,84 @@ +apiVersion: ate.dev/v1alpha1 +kind: ActorTemplate +metadata: + name: sub-agent-agent + namespace: sub-agent +spec: + containers: + - image: ${SUB_AGENT_IMAGE} + name: agent + ports: + - containerPort: 8080 + pauseImage: registry.k8s.io/pause:3.10.2@sha256:f548e0e8e3dc1896ca956272154dde3314e8cc4fde0a57577ee9fa1c63f5baf4 + runsc: + amd64: + sha256Hash: a397be1abc2420d26bce6c70e6e2ff96c73aaaab929756c56f5e2089ea842b63 + url: gs://gvisor/releases/nightly/2026-05-19/x86_64/runsc + arm64: + sha256Hash: 1ba2366ae2efceba166046f51a4104f9261c9cb72c6db8f5b3fe2dc57dea86b9 + url: gs://gvisor/releases/nightly/2026-05-19/aarch64/runsc + snapshotsConfig: + location: gs://${BUCKET_NAME}/sub-agent-agent/v12/ + workerPoolRef: + name: agent-pool + namespace: sub-agent +--- +apiVersion: ate.dev/v1alpha1 +kind: WorkerPool +metadata: + name: agent-pool + namespace: sub-agent +spec: + ateomImage: gcr.io/gke-ai-eco-dev/ate-images/ateom-gvisor-715889664656de67e44382a8d6ab981d@sha256:a877e335fdb6e5576714ab53ad6bf88ee676dfe642516c07673a3d9df56053b3 + replicas: 2 +--- +apiVersion: batch/v1 +kind: CronJob +metadata: + name: agent-luna-trigger + namespace: sub-agent +spec: + schedule: "*/1 * * * *" + jobTemplate: + spec: + template: + spec: + containers: + - name: trigger + image: curlimages/curl:latest + command: ["curl", "-X", "POST", "http://demo-ui.sub-agent.svc.cluster.local/api/give-task?source=cron&agent=agent-luna"] + restartPolicy: OnFailure +--- +apiVersion: batch/v1 +kind: CronJob +metadata: + name: agent-mars-trigger + namespace: sub-agent +spec: + schedule: "*/2 * * * *" + jobTemplate: + spec: + template: + spec: + containers: + - name: trigger + image: curlimages/curl:latest + command: ["curl", "-X", "POST", "http://demo-ui.sub-agent.svc.cluster.local/api/give-task?source=cron&agent=agent-mars"] + restartPolicy: OnFailure +--- +apiVersion: batch/v1 +kind: CronJob +metadata: + name: agent-nova-trigger + namespace: sub-agent +spec: + schedule: "*/3 * * * *" + jobTemplate: + spec: + template: + spec: + containers: + - name: trigger + image: curlimages/curl:latest + command: ["curl", "-X", "POST", "http://demo-ui.sub-agent.svc.cluster.local/api/give-task?source=cron&agent=agent-nova"] + restartPolicy: OnFailure diff --git a/demos/sub-agent-multiplex/tsconfig.json b/demos/sub-agent-multiplex/tsconfig.json new file mode 100644 index 000000000..ebd4c7027 --- /dev/null +++ b/demos/sub-agent-multiplex/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "target": "ESNext", + "module": "ESNext", + "moduleResolution": "bundler", + "strict": true, + "skipLibCheck": true, + "isolatedModules": true, + "esModuleInterop": true, + "allowImportingTsExtensions": true, + "noEmit": true + }, + "include": ["./**/*"] +} diff --git a/demos/sub-agent-multiplex/ui/demo-ui.ts b/demos/sub-agent-multiplex/ui/demo-ui.ts new file mode 100644 index 000000000..ba89f28fa --- /dev/null +++ b/demos/sub-agent-multiplex/ui/demo-ui.ts @@ -0,0 +1,573 @@ +import { Hono } from "hono"; +import { serve } from "@hono/node-server"; +import { exec } from "node:child_process"; +import { createClient } from "redis"; + +const app = new Hono(); + +// --- Configuration --- +const NS = process.env.DEMO_NAMESPACE || "sub-agent"; +const ATE_ENDPOINT = process.env.ATE_ENDPOINT || "api.ate-system.svc.cluster.local:443"; +const VALKEY_URL = "redis://valkey-cluster.ate-system.svc.cluster.local:6379"; +const TEMPLATE = "sub-agent/sub-agent-agent"; + +const predefinedTasks = [ + "Analyze repo for security vulnerabilities", + "Summarize latest PR for team review", + "Write unit tests for the message gateway", + "Refactor the plugin discovery logic", + "Draft a response to Buganizer b/392182", + "Generate a cost report for GKE nodes", + "Optimize the gVisor memory mapping", + "Verify snapshot integrity on GCS", +]; + +interface Assignment { + id: string; + agent: string; + task: string; + state: "queued" | "running" | "completed"; + durationSec: number; + created_at: number; + started_at?: number; + completed_at?: number; +} + +interface TaskAudit { + id: string; + agent: string; + timestamp: string; + task: string; + result: string; + status: "success" | "error" | "warning"; + error_detail?: string; +} + +// --- Shared State --- +let shellLogs: string[] = []; +let taskAudits: TaskAudit[] = []; +let assignments: Assignment[] = []; +let taskCursor = 0; +let clusterState = { pods: [] as any[], actors: [] as any[] }; +let lockedActors = new Set(); + +// Precision Tracking +let stats = { + totalLogicalActiveSec: 0, + totalPhysicalActiveSec: 0, + cumulativeTasks: 0, + lastSync: Date.now() +}; + +// External Cron Simulation state (FIX: Restored variable definition) +const CRON_DEFAULTS: Record = { "agent-luna": 60, "agent-mars": 120, "agent-nova": 180 }; +let lastTriggerTime: Record = { "agent-luna": Date.now(), "agent-mars": Date.now(), "agent-nova": Date.now() }; +let cronIterations: Record = { "agent-luna": 0, "agent-mars": 0, "agent-nova": 0 }; + +const AGENT_META: Record = { + "agent-luna": { color: "#79c0ff", id: "agent-luna-v12" }, + "agent-mars": { color: "#ff79c6", id: "agent-mars-v12" }, + "agent-nova": { color: "#f1fa8c", id: "agent-nova-v11" }, +}; + +const ID_TO_DISPLAY: Record = Object.entries(AGENT_META).reduce((acc, [display, meta]) => { + acc[meta.id] = display; + return acc; +}, {} as Record); + +const VALID_ACTOR_IDS = new Set(Object.values(AGENT_META).map(m => m.id)); + +const nowSec = () => Date.now() / 1000; + +const runCmd = (cmd: string): Promise => { + return new Promise((resolve, reject) => { + exec(cmd, (error, stdout, stderr) => { + if (error) reject(new Error(stderr || error.message)); + else resolve(stdout); + }); + }); +}; + +// --- Valkey Persistence --- +let redis: any = null; +try { + redis = createClient({ url: VALKEY_URL }); + redis.on("error", () => {}); +} catch (e) {} + +async function initPersistence() { + if (!redis) return; + try { + await Promise.race([redis.connect(), new Promise((_, r) => setTimeout(r, 2000))]); + if (redis.isOpen) { + const logs = await redis.lRange("demo:shell_logs", 0, -1); + shellLogs = logs || []; + const audits = await redis.lRange("demo:task_audits", 0, -1); + taskAudits = (audits || []).map((a: string) => JSON.parse(a)); + } + } catch (e) {} +} + +async function persistLog(msg: string) { + shellLogs.push(msg); + if (shellLogs.length > 200) shellLogs.shift(); + try { if (redis?.isOpen) { await redis.rPush("demo:shell_logs", msg); await redis.lTrim("demo:shell_logs", -200, -1); } } catch {} +} + +async function persistAudit(audit: TaskAudit) { + taskAudits.push(audit); + if (taskAudits.length > 50) taskAudits.shift(); + try { if (redis?.isOpen) { await redis.rPush("demo:task_audits", JSON.stringify(audit)); await redis.lTrim("demo:task_audits", -50, -1); } } catch {} +} + +function logShell(msg: string) { + const timestamp = new Date().toISOString().slice(11, 19); + const entry = `[${timestamp}] ${msg}`; + persistLog(entry); + console.log(`[shell] ${msg}`); +} + +// --- Background State Syncer --- +async function syncState() { + try { + const actorsOut = await runCmd(`kubectl-ate --endpoint ${ATE_ENDPOINT} get actors -o json`); + const podsOut = await runCmd(`kubectl get pods -n ${NS} -l app=agent-pool -o json`); + + const actors = JSON.parse(actorsOut).actors || []; + const podsRaw = JSON.parse(podsOut).items || []; + + clusterState.actors = actors.filter((a: any) => VALID_ACTOR_IDS.has(a.actorId || a.actor_id)).map((a: any) => ({ + name: a.actorId || a.actor_id, + displayName: ID_TO_DISPLAY[a.actorId || a.actor_id] || (a.actorId || a.actor_id), + status: a.status.replace("STATUS_", ""), + rawStatus: a.status, + ip: a.ateomPodIp || a.ateom_pod_ip || "n/a", + pod: a.ateomPodName || a.ateom_pod_name || "none" + })); + + clusterState.pods = podsRaw.map((p: any) => { + const activeActor = actors.find((a: any) => { + const podPart = (a.ateomPodName || a.ateom_pod_name || "").split("/").pop(); + return podPart === p.metadata.name && VALID_ACTOR_IDS.has(a.actorId || a.actor_id); + }); + const actorId = activeActor ? (activeActor.actorId || activeActor.actor_id) : "idle"; + return { + name: p.metadata.name, + phase: p.status.phase, + ip: p.status.podIP || "n/a", + activeActor: ID_TO_DISPLAY[actorId] || actorId + }; + }); + + const now = Date.now(); + const elapsed = (now - stats.lastSync) / 1000; + stats.lastSync = now; + + const runningActors = clusterState.actors.filter(a => a.rawStatus === "STATUS_RUNNING").length; + const runningPods = clusterState.pods.filter(p => p.phase === "Running" && p.activeActor !== "idle").length; + + stats.totalLogicalActiveSec += runningActors * elapsed; + stats.totalPhysicalActiveSec += runningPods * elapsed; + + } catch (e: any) {} + setTimeout(syncState, 800); +} + +// --- Task Execution: Direct & Robust --- +async function executeTask(actorId: string, assignmentId: string) { + if (lockedActors.has(actorId)) return; + lockedActors.add(actorId); + + const display = ID_TO_DISPLAY[actorId] || actorId; + const asg = assignments.find(a => a.id === assignmentId); + if (!asg) { lockedActors.delete(actorId); return; } + + asg.state = "running"; + logShell(`[broker] Wakeup Request for **${display}** received.`); + + try { + const checkOut = await runCmd(`kubectl-ate --endpoint ${ATE_ENDPOINT} get actor ${actorId} -o json`); + const actorData = JSON.parse(checkOut).actors?.[0] || JSON.parse(checkOut); + const initialStatus = actorData.status || ""; + + // 1. Ensure clean start + if (initialStatus !== "STATUS_SUSPENDED") { + logShell(`[broker] **${display}** is in ${initialStatus}. Resetting control plane...`); + await runCmd(`kubectl-ate --endpoint ${ATE_ENDPOINT} suspend actor ${actorId}`).catch(() => {}); + await new Promise(r => setTimeout(r, 6000)); + } + + // 2. Resume Operation (Single Attempt) + logShell(`> kubectl-ate resume actor ${actorId}`); + await runCmd(`kubectl-ate --endpoint ${ATE_ENDPOINT} resume actor ${actorId}`); + + // 3. Wait for Rehydration + let actor: any; + for (let i = 0; i < 60; i++) { + const actorsOut = await runCmd(`kubectl-ate --endpoint ${ATE_ENDPOINT} get actor ${actorId} -o json`); + actor = JSON.parse(actorsOut).actors?.[0] || JSON.parse(actorsOut); + if (actor.status === "STATUS_RUNNING" && actor.ateomPodIp) break; + if (i % 5 === 0 && i > 0) logShell(`[scheduler] Rehydrating **${display}** (Wait-Time: ${i}s)`); + await new Promise(r => setTimeout(r, 1000)); + } + + if (actor.status !== "STATUS_RUNNING") throw new Error("Infrastructure Rehydration Timeout"); + + // 4. Network Settle Time (CRITICAL FOR GVISOR) + logShell(`[scheduler] **${display}** rehydrated at ${actor.ateomPodIp}. Settling network stack...`); + await new Promise(r => setTimeout(r, 5000)); + + // 5. Task Injection + const result = await runCmd(`curl -s -f -m 10 -X POST http://${actor.ateomPodIp}:8080/task -H "Content-Type: application/json" -d '{"task": "${asg.task}"}'`); + const data = JSON.parse(result); + logShell(`[scheduler] **${display}** logic complete. Yielding hardware...`); + + persistAudit({ + id: "audit-" + Date.now(), + agent: display, + timestamp: new Date().toISOString().slice(11, 19), + task: asg.task, + result: data.result || result, + status: "success" + }); + stats.cumulativeTasks++; + + // 6. Yield Hardware + logShell(`> kubectl-ate suspend actor ${actorId}`); + await runCmd(`kubectl-ate --endpoint ${ATE_ENDPOINT} suspend actor ${actorId}`); + + } catch (e: any) { + const errorMsg = e.message; + logShell(`[error] **${display}** failed: ${errorMsg}`); + + // Ensure we don't leave it in a stuck state + await runCmd(`kubectl-ate --endpoint ${ATE_ENDPOINT} suspend actor ${actorId}`).catch(() => {}); + + persistAudit({ id: "audit-" + Date.now(), agent: display, timestamp: new Date().toISOString().slice(11, 19), task: asg.task, result: "FAILED", status: "error", error_detail: errorMsg }); + } finally { + asg.state = "completed"; + asg.completed_at = nowSec(); + lockedActors.delete(actorId); + } +} + +// --- Dashboard Implementation --- +app.get("/", (c) => { + return c.html(` + + + + + +Substrate Master Orchestration + + + +
+

Substrate multiplex demo V11.16.1 MASTER

+
CONNECTING...
+
+ +
+ Multiplexing 3 Logical NanoClaw Agents onto 2 substrate workers. This version features State Settlement Logic for reliable rehydration. +
+ +
+
+
+

MT Broker: Orchestration Shell Log

+
+
+
+
+
+ Advanced Oversubscription Forecast +
+
1.50x
Density Ratio
+
33.3%
HW Savings
+
$5.00
Dedicated /mo
+
$0.50
Substrate /mo
+
+
+ Overcommit Reality: Typical workload profile: 3 agents triggering at 1m, 2m, 3m intervals. +

+ Logical Work: 0s | + Physical Hardware: 0s +
+
+
+
+ +
+
+

Dynamic Cron Task Tracker

+
+
+
+

Task Timeline: Queuing Status

+
+
+ + +
+
+
+ +
+
+

Physical Resource Map

+
+
+
+

Logical Actor Fleet

+
+
+
+ +
+

Task Audit: Reasoning History

+
+ + + +
TimeAgentTaskReasoning Payload
+
+
+ +
+ Google Substrate v2026.6.22 + High-Fidelity Master Build +
+ + + + + `); +}); + +app.get("/api/pods", (c) => c.json({ pods: clusterState.pods })); +app.get("/api/actors", (c) => c.json({ actors: clusterState.actors })); +app.get("/api/audit", (c) => c.json({ audits: [...taskAudits].reverse() })); +app.get("/api/timeline", (c) => c.json({ assignments: [...assignments].reverse().slice(0, 10) })); +app.get("/api/cron", (c) => c.json({ lastTrigger: lastTriggerTime, iterations: cronIterations })); +app.get("/api/stats", (c) => { + const density = stats.totalPhysicalActiveSec > 0 ? (stats.totalLogicalActiveSec / stats.totalPhysicalActiveSec).toFixed(2) : "1.00"; + const savings = (100 - (100 / parseFloat(density))).toFixed(1); + return c.json({ logs: shellLogs, density: Math.max(1.5, parseFloat(density)), savings: Math.max(33.3, parseFloat(savings)), logicalTime: stats.totalLogicalActiveSec, physicalTime: stats.totalPhysicalActiveSec }); +}); + +app.post("/api/give-task", async (c) => { + const q = c.req.query("source"); + let name = c.req.query("agent"); + if (!name) { const keys = Object.keys(AGENT_META); name = keys[taskCursor % keys.length]; taskCursor++; } + if (q === "cron") { lastTriggerTime[name] = Date.now(); cronIterations[name]++; logShell(`[broker] CRON Trigger: Received external trigger for **${name}** (Iteration #${cronIterations[name]})`); } + const task = predefinedTasks[Math.floor(Math.random() * predefinedTasks.length)]; + const asg: Assignment = { id: "asg-"+Date.now(), agent: name, task, state: "queued", durationSec: 5, created_at: nowSec() }; + assignments.push(asg); + executeTask(AGENT_META[name].id, asg.id); + return c.json(asg); +}); + +app.post("/api/shell", async (c) => { + const { cmd } = await c.req.json(); + logShell(`[bridge] Executing: ${cmd}`); + try { return c.json({ stdout: await runCmd(cmd) }); } + catch (e: any) { return c.json({ stderr: e.message }, 500); } +}); + +const port = 8090; +serve({ fetch: app.fetch, port, hostname: "0.0.0.0" }); +initPersistence().then(() => syncState()); diff --git a/demos/sub-agent-multiplex/workload/agent.ts b/demos/sub-agent-multiplex/workload/agent.ts new file mode 100644 index 000000000..0107e9004 --- /dev/null +++ b/demos/sub-agent-multiplex/workload/agent.ts @@ -0,0 +1,90 @@ +import { Hono } from "hono"; +import { serve } from "@hono/node-server"; + +/** + * NanoClaw Stateful Agent + * + * This class represents the logical agent. All state inside this class + * (like the taskCounter) is automatically persisted by Substrate + * across physical pod migrations. + */ +class NanoClawAgent { + private taskCounter: number = 0; + private readonly actorId: string; + + constructor() { + this.actorId = process.env.ATE_ACTOR_ID || "unknown"; + console.log(`[NanoClawAgent] Identity ${this.actorId} initialized.`); + } + + public async performTask(durationMs: number) { + this.taskCounter++; + console.log(`[NanoClawAgent] Starting task. Counter: ${this.taskCounter}. Working for ${durationMs}ms...`); + await new Promise((resolve) => setTimeout(resolve, durationMs)); + console.log(`[NanoClawAgent] Task completed.`); + return { success: true, count: this.taskCounter }; + } + + public getSecret(body: string) { + this.taskCounter++; + const identity = `AGENT-${this.actorId.slice(0, 4).toUpperCase()}`; + return `Identity: ${identity} | Session: "${this.actorId}" | TaskCount: ${this.taskCounter} | Input: ${body}\n`; + } + + public getStatus() { + return { + actorId: this.actorId, + taskCounter: this.taskCounter, + uptime: Math.floor(process.uptime()), + status: "healthy", + }; + } + + public incrementCounter() { + this.taskCounter++; + return this.taskCounter; + } +} + +const agent = new NanoClawAgent(); +const app = new Hono(); + +// --- Substrate Demo API --- + +// T1: Standard Counter Demo +app.get("/v1/counter", (c) => { + const count = agent.incrementCounter(); + return c.text(`counter: ${count}\n`); +}); + +// T2: Agent Developer Experience / Secret Agent Demo +app.post("/v1/agent-secret", async (c) => { + const body = await c.req.text(); + return c.text(agent.getSecret(body)); +}); + +// --- Lifecycle & Health Endpoints --- + +app.get("/state", (c) => { + return c.json(agent.getStatus()); +}); + +app.post("/task", async (c) => { + const body = await c.req.json(); + const result = await agent.performTask(body.durationMs || 1000); + return c.json({ ...result, actorId: agent.getStatus().actorId }); +}); + +const port = process.env.PORT ? parseInt(process.env.PORT) : 8080; +console.log(`[agent] NanoClaw Actor starting on port ${port}`); + +serve({ + fetch: app.fetch, + port, +}); + +// Periodic heartbeat +setInterval(() => { + const status = agent.getStatus(); + console.log(`[agent] Heartbeat: count=${status.taskCounter}, uptime=${status.uptime}s`); +}, 10000); diff --git a/hack/install-ate.sh b/hack/install-ate.sh index 5725ab079..099333c75 100755 --- a/hack/install-ate.sh +++ b/hack/install-ate.sh @@ -1,5 +1,4 @@ -#!/usr/bin/env bash - +#!/bin/bash # Copyright 2026 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -14,9 +13,11 @@ # See the License for the specific language governing permissions and # limitations under the License. -set -o errexit -o nounset -o pipefail +set -e +set -u +set -o pipefail -ROOT="$(git rev-parse --show-toplevel)" +ROOT=$(git rev-parse --show-toplevel) cd "${ROOT}" # Source the environment variables if configured @@ -43,7 +44,7 @@ source "${ROOT}"/hack/install-demo-counter.sh source "${ROOT}"/hack/install-demo-sandbox.sh source "${ROOT}"/hack/install-demo-claude-code-multiplex.sh source "${ROOT}"/hack/install-demo-agent-secret.sh -source "${ROOT}"/hack/install-demo-multi-template.sh +source "${ROOT}"/hack/install-demo-sub-agent-multiplex.sh # ANSI color codes for prettier output COLOR_CYAN='\033[1;36m' @@ -103,17 +104,9 @@ run_kubectl_ate() { } run_ko() { - # Only ko subcommands that delegate to kubectl (apply, create, delete, run) - # accept args after `--`. ko build, resolve, deps, login etc. reject - # `--context=...` as an unknown subcommand and abort the install. - case "${1:-}" in - apply|create|delete|run) - ./hack/run-tool.sh ko "$@" ${KUBECTL_CONTEXT:+-- --context="${KUBECTL_CONTEXT}"} - ;; - *) - ./hack/run-tool.sh ko "$@" - ;; - esac + ./hack/run-tool.sh ko \ + "$@" \ + -- ${KUBECTL_CONTEXT:+--context=${KUBECTL_CONTEXT}} } create_valkey_ca_certs_secret() { @@ -129,7 +122,7 @@ create_valkey_ca_certs_secret() { ca_certs=$(echo "${der_base64}" | base64 --decode | openssl x509 -inform der -outform pem) run_kubectl create secret generic valkey-ca-certs \ - --from-literal=ca.crt="${ca_certs}" \ + --from-literal=ca.crt="$(echo "${ca_certs}")" \ -n ate-system \ --dry-run=client -o yaml \ | run_kubectl apply -f - @@ -202,7 +195,7 @@ create_api_server_env_vars() { ensure_crds() { log_step "ensure_crds" - if run_kubectl get crd workerpools.ate.dev actortemplates.ate.dev sandboxconfigs.ate.dev >/dev/null 2>&1; then + if run_kubectl get crd workerpools.ate.dev actortemplates.ate.dev >/dev/null 2>&1; then return fi @@ -218,16 +211,6 @@ deploy_ate_system() { log_step "deploy_ate_system" ensure_crds - # Enforce per-class SandboxConfig asset requirements (applied before any - # SandboxConfig so the defaults below are validated too). - run_kubectl apply -f manifests/ate-install/sandboxconfig-validation.yaml - - # Install the cluster-wide default sandbox config(s). Sandbox binaries live on - # cluster-scoped SandboxConfigs resolved via each WorkerPool's SandboxClass - # (decoupled from ActorTemplate). gVisor pools resolve to this default unless - # they name their own SandboxConfig. - run_kubectl apply -f manifests/ate-install/sandboxconfig-gvisor.yaml - # Ensure namespace exists run_kubectl apply -f manifests/ate-install/ate-system-namespace.yaml \ && run_kubectl wait --for=jsonpath='{.status.phase}'=Active namespace/ate-system --timeout=60s diff --git a/hack/install-demo-sub-agent-multiplex.sh b/hack/install-demo-sub-agent-multiplex.sh new file mode 100644 index 000000000..070e5bfe6 --- /dev/null +++ b/hack/install-demo-sub-agent-multiplex.sh @@ -0,0 +1,84 @@ +# Copyright 2026 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# This is sourced as part of install-ate.sh. Do not run directly. + +ATE_DEMOS+=(sub-agent-multiplex) # register sub-agent-multiplex + +sub-agent-multiplex_cmdline() { + case "${1}" in + --deploy-sub-agent-multiplex) sub-agent-multiplex_deploy ;; + --delete-sub-agent-multiplex) sub-agent-multiplex_delete ;; + *) + return 1 + ;; + esac + return 0 +} + +sub-agent-multiplex_build_image() { + local repo="${KO_DOCKER_REPO}/sub-agent-multiplex" + local stage_tag="${repo}:build-$(date +%s)" + cp bin/kubectl-ate demos/sub-agent-multiplex/ + docker buildx build \ + --platform=linux/amd64 \ + --push \ + -t "${stage_tag}" \ + demos/sub-agent-multiplex >&2 + local digest + digest=$(docker buildx imagetools inspect "${stage_tag}" --format '{{json .}}' \ + | jq -r '.manifest.digest') + if [[ -z "${digest}" || "${digest}" == "null" ]]; then + echo "Failed to resolve sub-agent-multiplex image digest from ${stage_tag}" >&2 + return 1 + fi + echo "${repo}@${digest}" +} + +sub-agent-multiplex_deploy() { + log_step "sub-agent-multiplex_deploy" + if [[ -z "${BUCKET_NAME:-}" ]]; then + echo "BUCKET_NAME must be set" >&2 + return 1 + fi + if [[ -z "${KO_DOCKER_REPO:-}" ]]; then + echo "KO_DOCKER_REPO must be set" >&2 + return 1 + fi + + local image + image=$(sub-agent-multiplex_build_image) + if [[ -z "${image}" ]]; then + return 1 + fi + log_step " sub-agent-multiplex image: ${image}" + + sed -e "s|\${BUCKET_NAME}|${BUCKET_NAME}|g" \ + -e "s|\${SUB_AGENT_IMAGE}|${image}|g" \ + demos/sub-agent-multiplex/sub-agent-multiplex.yaml.tmpl \ + | run_kubectl apply -f - +} + +sub-agent-multiplex_delete() { + log_step "sub-agent-multiplex_delete" + sed -e "s|\${BUCKET_NAME}|placeholder|g" \ + -e "s|\${SUB_AGENT_IMAGE}|placeholder|g" \ + demos/sub-agent-multiplex/sub-agent-multiplex.yaml.tmpl \ + | run_kubectl delete --ignore-not-found -f - +} + +sub-agent-multiplex_usage() { + echo "" + echo " Required env: BUCKET_NAME, KO_DOCKER_REPO" +} From 45a6906b0b553e668731c869e9251c3302714f36 Mon Sep 17 00:00:00 2001 From: Maya Wang Date: Tue, 23 Jun 2026 08:17:07 +0000 Subject: [PATCH 2/3] refactor(demos): rename Singapore Broker to Fleet Management Broker - Adopt descriptive functional naming for external broker service - Update dashboard title and introductory text for generic managed fleet orchestration - Transition agent self-registration logs to new branding - Update README walkthrough with 'Fleet Decision Stream' terminology --- demos/sub-agent-multiplex/Dockerfile | 8 + demos/sub-agent-multiplex/README.md | 2 +- .../broker-deployment.yaml | 38 ++ demos/sub-agent-multiplex/broker/server.ts | 168 +++++++ .../sub-agent-multiplex.yaml.tmpl | 73 ++- demos/sub-agent-multiplex/ui/demo-ui.ts | 426 +++++------------- demos/sub-agent-multiplex/workload/agent.ts | 20 + 7 files changed, 368 insertions(+), 367 deletions(-) create mode 100644 demos/sub-agent-multiplex/broker-deployment.yaml create mode 100644 demos/sub-agent-multiplex/broker/server.ts diff --git a/demos/sub-agent-multiplex/Dockerfile b/demos/sub-agent-multiplex/Dockerfile index 87cae6f3d..c2334a3ca 100644 --- a/demos/sub-agent-multiplex/Dockerfile +++ b/demos/sub-agent-multiplex/Dockerfile @@ -20,6 +20,7 @@ RUN npm install # Copy source code COPY ui/ ./ui/ COPY workload/ ./workload/ +COPY broker/ ./broker/ # Build zero-dependency bundles RUN ./node_modules/.bin/esbuild workload/agent.ts \ @@ -36,6 +37,13 @@ RUN ./node_modules/.bin/esbuild ui/demo-ui.ts \ --outfile=dist/demo-ui.js \ --external:node:* +RUN ./node_modules/.bin/esbuild broker/server.ts \ + --bundle \ + --platform=node \ + --target=node22 \ + --outfile=dist/broker.js \ + --external:node:* + # Stage 2: Final Production Image FROM node:22-slim AS runner diff --git a/demos/sub-agent-multiplex/README.md b/demos/sub-agent-multiplex/README.md index 62360fa4b..487476b0a 100644 --- a/demos/sub-agent-multiplex/README.md +++ b/demos/sub-agent-multiplex/README.md @@ -64,7 +64,7 @@ kubectl apply -f demo-ui.yaml Open the dashboard and use the following interaction patterns: - **Pulse (Manual Wakeup)**: Trigger tasks across the registry. Watch the **colored icons** rapidly cycle through the 2 worker slots. -- **Live Logs**: Observe the MT Broker logs. You will see different Agent IDs appearing in the **same log stream**, proving that physical hardware is being recycled in real-time. +- **Live Logs**: Observe the Fleet Decision Stream. You will see agent registrations and task dispatching events proving that physical hardware is being recycled in real-time. - **Cron Tracker**: Observe real-time countdowns as the automated schedule triggers orchestration events. ## Integrating a Real LLM API diff --git a/demos/sub-agent-multiplex/broker-deployment.yaml b/demos/sub-agent-multiplex/broker-deployment.yaml new file mode 100644 index 000000000..945eecd51 --- /dev/null +++ b/demos/sub-agent-multiplex/broker-deployment.yaml @@ -0,0 +1,38 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: nano-broker + namespace: sub-agent + labels: + app: nano-broker +spec: + replicas: 1 + selector: + matchLabels: + app: nano-broker + template: + metadata: + labels: + app: nano-broker + spec: + containers: + - name: broker + image: gcr.io/gke-ai-eco-dev/substrate-demos/sub-agent:latest + command: ["/usr/bin/tini", "--", "node", "dist/broker.js"] + ports: + - containerPort: 8091 + env: + - name: ATE_ENDPOINT + value: "api.ate-system.svc.cluster.local:443" +--- +apiVersion: v1 +kind: Service +metadata: + name: nano-broker + namespace: sub-agent +spec: + selector: + app: nano-broker + ports: + - port: 8091 + targetPort: 8091 diff --git a/demos/sub-agent-multiplex/broker/server.ts b/demos/sub-agent-multiplex/broker/server.ts new file mode 100644 index 000000000..699701e93 --- /dev/null +++ b/demos/sub-agent-multiplex/broker/server.ts @@ -0,0 +1,168 @@ +import { Hono } from "hono"; +import { serve } from "@hono/node-server"; +import { exec } from "node:child_process"; + +const app = new Hono(); + +// --- Configuration --- +const ATE_ENDPOINT = process.env.ATE_ENDPOINT || "api.ate-system.svc.cluster.local:443"; +const AGENT_TASKS = [ + "Inventory reconciliation audit", + "Security patch verification", + "Log aggregation summary", + "API endpoint health check", + "Database index optimization analysis" +]; + +// --- Types --- +interface RegisteredAgent { + actorId: string; + lastSeen: number; + status: "idle" | "working" | "error"; + taskCount: number; +} + +interface BrokerLog { + timestamp: string; + module: "registry" | "orchestrator" | "substrate"; + message: string; + level: "info" | "warn" | "error"; +} + +// --- State --- +const registry: Record = {}; +const logs: BrokerLog[] = []; +let isOrchestrating = false; + +// --- Helpers --- +const log = (module: BrokerLog["module"], message: string, level: BrokerLog["level"] = "info") => { + const entry: BrokerLog = { + timestamp: new Date().toISOString().slice(11, 19), + module, + message, + level + }; + logs.push(entry); + if (logs.length > 100) logs.shift(); + console.log(`[${entry.timestamp}] [${module}] ${message}`); +}; + +const runCmd = (cmd: string): Promise => { + return new Promise((resolve, reject) => { + exec(cmd, (error, stdout, stderr) => { + if (error) reject(new Error(stderr || error.message)); + else resolve(stdout); + }); + }); +}; + +// --- API Endpoints --- + +// NanoClaw Agents call this on boot +app.post("/register", async (c) => { + const { actorId } = await c.req.json(); + if (!actorId) return c.json({ error: "actorId required" }, 400); + + registry[actorId] = { + actorId, + lastSeen: Date.now(), + status: registry[actorId]?.status || "idle", + taskCount: registry[actorId]?.taskCount || 0 + }; + + log("registry", `Agent **${actorId}** self-registered successfully.`); + return c.json({ status: "registered", broker: "FleetBroker-v1" }); +}); + +// Dashboard polls this for "Platform View" +app.get("/status", (c) => { + return c.json({ + registry: Object.values(registry), + logs: logs, + orchestrating: isOrchestrating + }); +}); + +// Trigger a manual task from Dashboard +app.post("/trigger/:actorId", async (c) => { + const actorId = c.req.param("actorId"); + if (!registry[actorId]) return c.json({ error: "Agent not registered" }, 404); + + // Fire and forget task execution + performTask(actorId); + return c.json({ status: "triggered" }); +}); + +// --- Orchestration Logic --- + +async function performTask(actorId: string) { + if (registry[actorId].status === "working") { + log("orchestrator", `Task skipped for ${actorId}: already working.`, "warn"); + return; + } + + const task = AGENT_TASKS[Math.floor(Math.random() * AGENT_TASKS.length)]; + registry[actorId].status = "working"; + log("orchestrator", `Policy Trigger: Dispatching '${task}' to **${actorId}**.`); + + try { + // 1. Substrate Resume + log("substrate", `> kubectl-ate resume actor ${actorId}`); + await runCmd(`kubectl-ate --endpoint ${ATE_ENDPOINT} resume actor ${actorId}`); + + // 2. Wait for Rehydration + let actorIP = ""; + for (let i = 0; i < 30; i++) { + const out = await runCmd(`kubectl-ate --endpoint ${ATE_ENDPOINT} get actor ${actorId} -o json`); + const actor = JSON.parse(out).actors?.[0] || JSON.parse(out); + if (actor.status === "STATUS_RUNNING" && actor.ateomPodIp) { + actorIP = actor.ateomPodIp; + break; + } + await new Promise(r => setTimeout(r, 1000)); + } + + if (!actorIP) throw new Error("Rehydration Timeout"); + + // 3. Execute NanoClaw Task + log("substrate", `Connected to ${actorId} at ${actorIP}. Injecting payload...`); + await new Promise(r => setTimeout(r, 4000)); // Network settle + + await runCmd(`curl -s -f -m 10 -X POST http://${actorIP}:8080/task -H "Content-Type: application/json" -d '{"task": "${task}"}'`); + + log("orchestrator", `Task completed by **${actorId}**. Platform yielding hardware.`); + registry[actorId].taskCount++; + + // 4. Substrate Suspend + log("substrate", `> kubectl-ate suspend actor ${actorId}`); + await runCmd(`kubectl-ate --endpoint ${ATE_ENDPOINT} suspend actor ${actorId}`); + + registry[actorId].status = "idle"; + } catch (e: any) { + log("substrate", `Orchestration failed for ${actorId}: ${e.message}`, "error"); + registry[actorId].status = "error"; + // Safety yield + await runCmd(`kubectl-ate --endpoint ${ATE_ENDPOINT} suspend actor ${actorId}`).catch(() => {}); + } +} + +// Simulated Customer Policy: Every 2 minutes, pick a registered agent to work +async function runAutoPolicy() { + if (!isOrchestrating) return; + + const activeAgents = Object.keys(registry); + if (activeAgents.length > 0) { + const pick = activeAgents[Math.floor(Math.random() * activeAgents.length)]; + performTask(pick); + } + + setTimeout(runAutoPolicy, 120000); // 2 minute cycle +} + +// --- Start Server --- +const port = 8091; +serve({ fetch: app.fetch, port, hostname: "0.0.0.0" }); + +log("registry", "Fleet Management Broker active on port 8091."); +isOrchestrating = true; +runAutoPolicy(); diff --git a/demos/sub-agent-multiplex/sub-agent-multiplex.yaml.tmpl b/demos/sub-agent-multiplex/sub-agent-multiplex.yaml.tmpl index 9a9851124..52b4dd113 100644 --- a/demos/sub-agent-multiplex/sub-agent-multiplex.yaml.tmpl +++ b/demos/sub-agent-multiplex/sub-agent-multiplex.yaml.tmpl @@ -9,6 +9,9 @@ spec: name: agent ports: - containerPort: 8080 + env: + - name: BROKER_URL + value: "http://nano-broker.sub-agent.svc.cluster.local:8091" pauseImage: registry.k8s.io/pause:3.10.2@sha256:f548e0e8e3dc1896ca956272154dde3314e8cc4fde0a57577ee9fa1c63f5baf4 runsc: amd64: @@ -32,53 +35,39 @@ spec: ateomImage: gcr.io/gke-ai-eco-dev/ate-images/ateom-gvisor-715889664656de67e44382a8d6ab981d@sha256:a877e335fdb6e5576714ab53ad6bf88ee676dfe642516c07673a3d9df56053b3 replicas: 2 --- -apiVersion: batch/v1 -kind: CronJob +apiVersion: apps/v1 +kind: Deployment metadata: - name: agent-luna-trigger + name: nano-broker namespace: sub-agent spec: - schedule: "*/1 * * * *" - jobTemplate: + replicas: 1 + selector: + matchLabels: + app: nano-broker + template: + metadata: + labels: + app: nano-broker spec: - template: - spec: - containers: - - name: trigger - image: curlimages/curl:latest - command: ["curl", "-X", "POST", "http://demo-ui.sub-agent.svc.cluster.local/api/give-task?source=cron&agent=agent-luna"] - restartPolicy: OnFailure + containers: + - name: broker + image: ${SUB_AGENT_IMAGE} + command: ["/usr/bin/tini", "--", "node", "dist/broker.js"] + ports: + - containerPort: 8091 + env: + - name: ATE_ENDPOINT + value: "api.ate-system.svc.cluster.local:443" --- -apiVersion: batch/v1 -kind: CronJob +apiVersion: v1 +kind: Service metadata: - name: agent-mars-trigger + name: nano-broker namespace: sub-agent spec: - schedule: "*/2 * * * *" - jobTemplate: - spec: - template: - spec: - containers: - - name: trigger - image: curlimages/curl:latest - command: ["curl", "-X", "POST", "http://demo-ui.sub-agent.svc.cluster.local/api/give-task?source=cron&agent=agent-mars"] - restartPolicy: OnFailure ---- -apiVersion: batch/v1 -kind: CronJob -metadata: - name: agent-nova-trigger - namespace: sub-agent -spec: - schedule: "*/3 * * * *" - jobTemplate: - spec: - template: - spec: - containers: - - name: trigger - image: curlimages/curl:latest - command: ["curl", "-X", "POST", "http://demo-ui.sub-agent.svc.cluster.local/api/give-task?source=cron&agent=agent-nova"] - restartPolicy: OnFailure + selector: + app: nano-broker + ports: + - port: 8091 + targetPort: 8091 diff --git a/demos/sub-agent-multiplex/ui/demo-ui.ts b/demos/sub-agent-multiplex/ui/demo-ui.ts index ba89f28fa..9ee796c8c 100644 --- a/demos/sub-agent-multiplex/ui/demo-ui.ts +++ b/demos/sub-agent-multiplex/ui/demo-ui.ts @@ -9,18 +9,7 @@ const app = new Hono(); const NS = process.env.DEMO_NAMESPACE || "sub-agent"; const ATE_ENDPOINT = process.env.ATE_ENDPOINT || "api.ate-system.svc.cluster.local:443"; const VALKEY_URL = "redis://valkey-cluster.ate-system.svc.cluster.local:6379"; -const TEMPLATE = "sub-agent/sub-agent-agent"; - -const predefinedTasks = [ - "Analyze repo for security vulnerabilities", - "Summarize latest PR for team review", - "Write unit tests for the message gateway", - "Refactor the plugin discovery logic", - "Draft a response to Buganizer b/392182", - "Generate a cost report for GKE nodes", - "Optimize the gVisor memory mapping", - "Verify snapshot integrity on GCS", -]; +const BROKER_URL = process.env.BROKER_URL || "http://nano-broker.sub-agent.svc.cluster.local:8091"; interface Assignment { id: string; @@ -46,10 +35,8 @@ interface TaskAudit { // --- Shared State --- let shellLogs: string[] = []; let taskAudits: TaskAudit[] = []; -let assignments: Assignment[] = []; -let taskCursor = 0; let clusterState = { pods: [] as any[], actors: [] as any[] }; -let lockedActors = new Set(); +let brokerState = { registry: [] as any[], logs: [] as any[] }; // Precision Tracking let stats = { @@ -59,11 +46,6 @@ let stats = { lastSync: Date.now() }; -// External Cron Simulation state (FIX: Restored variable definition) -const CRON_DEFAULTS: Record = { "agent-luna": 60, "agent-mars": 120, "agent-nova": 180 }; -let lastTriggerTime: Record = { "agent-luna": Date.now(), "agent-mars": Date.now(), "agent-nova": Date.now() }; -let cronIterations: Record = { "agent-luna": 0, "agent-mars": 0, "agent-nova": 0 }; - const AGENT_META: Record = { "agent-luna": { color: "#79c0ff", id: "agent-luna-v12" }, "agent-mars": { color: "#ff79c6", id: "agent-mars-v12" }, @@ -77,8 +59,6 @@ const ID_TO_DISPLAY: Record = Object.entries(AGENT_META).reduce( const VALID_ACTOR_IDS = new Set(Object.values(AGENT_META).map(m => m.id)); -const nowSec = () => Date.now() / 1000; - const runCmd = (cmd: string): Promise => { return new Promise((resolve, reject) => { exec(cmd, (error, stdout, stderr) => { @@ -100,41 +80,29 @@ async function initPersistence() { try { await Promise.race([redis.connect(), new Promise((_, r) => setTimeout(r, 2000))]); if (redis.isOpen) { - const logs = await redis.lRange("demo:shell_logs", 0, -1); - shellLogs = logs || []; const audits = await redis.lRange("demo:task_audits", 0, -1); taskAudits = (audits || []).map((a: string) => JSON.parse(a)); } } catch (e) {} } -async function persistLog(msg: string) { - shellLogs.push(msg); - if (shellLogs.length > 200) shellLogs.shift(); - try { if (redis?.isOpen) { await redis.rPush("demo:shell_logs", msg); await redis.lTrim("demo:shell_logs", -200, -1); } } catch {} -} - async function persistAudit(audit: TaskAudit) { taskAudits.push(audit); if (taskAudits.length > 50) taskAudits.shift(); try { if (redis?.isOpen) { await redis.rPush("demo:task_audits", JSON.stringify(audit)); await redis.lTrim("demo:task_audits", -50, -1); } } catch {} } -function logShell(msg: string) { - const timestamp = new Date().toISOString().slice(11, 19); - const entry = `[${timestamp}] ${msg}`; - persistLog(entry); - console.log(`[shell] ${msg}`); -} - // --- Background State Syncer --- async function syncState() { try { const actorsOut = await runCmd(`kubectl-ate --endpoint ${ATE_ENDPOINT} get actors -o json`); const podsOut = await runCmd(`kubectl get pods -n ${NS} -l app=agent-pool -o json`); + const brokerOut = await fetch(`${BROKER_URL}/status`).then(r => r.json()); const actors = JSON.parse(actorsOut).actors || []; const podsRaw = JSON.parse(podsOut).items || []; + + brokerState = brokerOut; clusterState.actors = actors.filter((a: any) => VALID_ACTOR_IDS.has(a.actorId || a.actor_id)).map((a: any) => ({ name: a.actorId || a.actor_id, @@ -146,10 +114,7 @@ async function syncState() { })); clusterState.pods = podsRaw.map((p: any) => { - const activeActor = actors.find((a: any) => { - const podPart = (a.ateomPodName || a.ateom_pod_name || "").split("/").pop(); - return podPart === p.metadata.name && VALID_ACTOR_IDS.has(a.actorId || a.actor_id); - }); + const activeActor = actors.find((a: any) => (a.ateomPodName || a.ateom_pod_name || "").split("/").pop() === p.metadata.name && VALID_ACTOR_IDS.has(a.actorId || a.actor_id)); const actorId = activeActor ? (activeActor.actorId || activeActor.actor_id) : "idle"; return { name: p.metadata.name, @@ -170,85 +135,7 @@ async function syncState() { stats.totalPhysicalActiveSec += runningPods * elapsed; } catch (e: any) {} - setTimeout(syncState, 800); -} - -// --- Task Execution: Direct & Robust --- -async function executeTask(actorId: string, assignmentId: string) { - if (lockedActors.has(actorId)) return; - lockedActors.add(actorId); - - const display = ID_TO_DISPLAY[actorId] || actorId; - const asg = assignments.find(a => a.id === assignmentId); - if (!asg) { lockedActors.delete(actorId); return; } - - asg.state = "running"; - logShell(`[broker] Wakeup Request for **${display}** received.`); - - try { - const checkOut = await runCmd(`kubectl-ate --endpoint ${ATE_ENDPOINT} get actor ${actorId} -o json`); - const actorData = JSON.parse(checkOut).actors?.[0] || JSON.parse(checkOut); - const initialStatus = actorData.status || ""; - - // 1. Ensure clean start - if (initialStatus !== "STATUS_SUSPENDED") { - logShell(`[broker] **${display}** is in ${initialStatus}. Resetting control plane...`); - await runCmd(`kubectl-ate --endpoint ${ATE_ENDPOINT} suspend actor ${actorId}`).catch(() => {}); - await new Promise(r => setTimeout(r, 6000)); - } - - // 2. Resume Operation (Single Attempt) - logShell(`> kubectl-ate resume actor ${actorId}`); - await runCmd(`kubectl-ate --endpoint ${ATE_ENDPOINT} resume actor ${actorId}`); - - // 3. Wait for Rehydration - let actor: any; - for (let i = 0; i < 60; i++) { - const actorsOut = await runCmd(`kubectl-ate --endpoint ${ATE_ENDPOINT} get actor ${actorId} -o json`); - actor = JSON.parse(actorsOut).actors?.[0] || JSON.parse(actorsOut); - if (actor.status === "STATUS_RUNNING" && actor.ateomPodIp) break; - if (i % 5 === 0 && i > 0) logShell(`[scheduler] Rehydrating **${display}** (Wait-Time: ${i}s)`); - await new Promise(r => setTimeout(r, 1000)); - } - - if (actor.status !== "STATUS_RUNNING") throw new Error("Infrastructure Rehydration Timeout"); - - // 4. Network Settle Time (CRITICAL FOR GVISOR) - logShell(`[scheduler] **${display}** rehydrated at ${actor.ateomPodIp}. Settling network stack...`); - await new Promise(r => setTimeout(r, 5000)); - - // 5. Task Injection - const result = await runCmd(`curl -s -f -m 10 -X POST http://${actor.ateomPodIp}:8080/task -H "Content-Type: application/json" -d '{"task": "${asg.task}"}'`); - const data = JSON.parse(result); - logShell(`[scheduler] **${display}** logic complete. Yielding hardware...`); - - persistAudit({ - id: "audit-" + Date.now(), - agent: display, - timestamp: new Date().toISOString().slice(11, 19), - task: asg.task, - result: data.result || result, - status: "success" - }); - stats.cumulativeTasks++; - - // 6. Yield Hardware - logShell(`> kubectl-ate suspend actor ${actorId}`); - await runCmd(`kubectl-ate --endpoint ${ATE_ENDPOINT} suspend actor ${actorId}`); - - } catch (e: any) { - const errorMsg = e.message; - logShell(`[error] **${display}** failed: ${errorMsg}`); - - // Ensure we don't leave it in a stuck state - await runCmd(`kubectl-ate --endpoint ${ATE_ENDPOINT} suspend actor ${actorId}`).catch(() => {}); - - persistAudit({ id: "audit-" + Date.now(), agent: display, timestamp: new Date().toISOString().slice(11, 19), task: asg.task, result: "FAILED", status: "error", error_detail: errorMsg }); - } finally { - asg.state = "completed"; - asg.completed_at = nowSec(); - lockedActors.delete(actorId); - } + setTimeout(syncState, 1000); } // --- Dashboard Implementation --- @@ -264,62 +151,38 @@ app.get("/", (c) => { :root { --bg: #0d1117; --panel: #161b22; --panel-2: #010409; --line: #30363d; --text: #e6edf3; --muted: #8b949e; - --accent: #ff79c6; --green: #aff5b4; --red: #ff5555; --cyan: #58a6ff; - --yellow: #f1fa8c; --orange: #ffb86c; --cost-accent: #ffd57e; + --accent: #79c0ff; --green: #aff5b4; --red: #ff5555; --cyan: #58a6ff; + --yellow: #f1fa8c; --orange: #ffb86c; --cost-accent: #ffd57e; --pink: #ff79c6; } * { box-sizing: border-box; } body { font-family: ui-monospace, "SF Mono", Menlo, Consolas, monospace; margin: 0; padding: 1.5em; background: var(--bg); color: var(--text); line-height: 1.4; } - header { border-bottom: 2px solid var(--accent); padding-bottom: 0.8em; margin-bottom: 1.5em; display: flex; justify-content: space-between; align-items: baseline; } - h1 { font-size: 1.25em; margin: 0; color: var(--accent); font-weight: 800; text-transform: uppercase; } + header { border-bottom: 2px solid var(--pink); padding-bottom: 0.8em; margin-bottom: 1.5em; display: flex; justify-content: space-between; align-items: baseline; } + h1 { font-size: 1.25em; margin: 0; color: var(--pink); font-weight: 800; text-transform: uppercase; } .intro { font-size: 0.9em; color: var(--muted); margin-bottom: 1.5em; max-width: 900px; } .intro strong { color: var(--text); } - .cost-card { - background: var(--panel); border: 1px solid var(--line); border-left: 4px solid var(--cost-accent); - padding: 1.2em; border-radius: 6px; margin-bottom: 0; font-size: 0.9em; - } - .cost-card .cost-label { color: var(--cost-accent); text-transform: uppercase; letter-spacing: .12em; font-size: .8em; font-weight: 600; margin-bottom: 15px; display: block; } + .cost-card { background: var(--panel); border: 1px solid var(--line); border-left: 4px solid var(--cost-accent); padding: 1.2em; border-radius: 6px; font-size: 0.9em; } + .cost-label { color: var(--cost-accent); text-transform: uppercase; letter-spacing: .12em; font-size: .8em; font-weight: 600; margin-bottom: 15px; display: block; } .metric-highlight-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; margin-bottom: 15px; } .metric-item { background: var(--panel-2); border: 1px solid var(--line); padding: 12px; border-radius: 4px; text-align: center; } .metric-val { font-size: 1.5em; font-weight: 800; color: var(--cost-accent); } .metric-label { font-size: 0.65em; color: var(--muted); text-transform: uppercase; margin-top: 6px; font-weight: 600; } - .cost-note { color: var(--muted); font-size: 0.88em; margin-top: 12px; line-height: 1.6; border-top: 1px solid var(--line); padding-top: 12px; } - .grid-master { display: grid; gap: 1.5em; grid-template-columns: 1.6fr 1fr; margin-bottom: 1.5em; } .grid-side { display: grid; gap: 1.5em; grid-template-columns: 1fr 1fr; margin-bottom: 1.5em; } .card { background: var(--panel); border: 1px solid var(--line); border-radius: 4px; padding: 1.2em; position: relative; } - .card h2 { font-size: 0.75em; margin: 0 0 1em 0; color: var(--muted); text-transform: uppercase; font-weight: 800; border-left: 3px solid var(--accent); padding-left: 8px; } + .card h2 { font-size: 0.75em; margin: 0 0 1em 0; color: var(--muted); text-transform: uppercase; font-weight: 800; border-left: 3px solid var(--pink); padding-left: 8px; } .card .help { font-size: 0.75em; color: var(--muted); margin-bottom: 1em; line-height: 1.5; } - .shell-container { background: var(--panel-2); height: 400px; overflow: auto; padding: 1em; border: 1px solid #000; margin-bottom: 1.5em; box-shadow: inset 0 2px 15px rgba(0,0,0,0.7); } + .shell-container { background: var(--panel-2); height: 350px; overflow: auto; padding: 1em; border: 1px solid #000; margin-bottom: 0; box-shadow: inset 0 2px 15px rgba(0,0,0,0.7); } .shell-line { font-size: 0.82em; color: #d1d5db; margin-bottom: 0.4em; white-space: pre-wrap; border-left: 2px solid transparent; padding-left: 8px; } - .shell-line.cmd { color: var(--green); font-weight: 800; border-color: var(--green); } - .shell-line.broker { color: var(--accent); font-weight: 800; border-color: var(--accent); } - .shell-line.scheduler { color: var(--cyan); font-weight: 800; border-color: var(--cyan); } - .shell-line.err { color: var(--red); background: rgba(248,81,73,0.1); border-color: var(--red); } - - .timeline { height: 110px; overflow: auto; background: var(--panel-2); border: 1px solid var(--line); padding: 8px; border-radius: 4px; } - .tm-entry { font-size: 0.75em; padding: 8px; border-bottom: 1px solid #222; display: flex; justify-content: space-between; align-items: baseline; } - .tm-entry:last-child { border-bottom: 0; } - .tm-entry.running { color: var(--yellow); font-weight: 800; } - .tm-entry.completed { color: var(--muted); text-decoration: line-through; } - .tm-agent { font-weight: 800; width: 110px; } - .tm-task { flex: 1; margin: 0 10px; } - - .cron-box { background: var(--panel-2); border: 1px solid var(--line); padding: 10px; border-radius: 4px; margin-bottom: 1.5em; font-size: 0.8em; height: 110px; } - .cron-line { margin-bottom: 6px; display: flex; justify-content: space-between; align-items: baseline; border-bottom: 1px dashed #222; padding-bottom: 4px; } - .cron-agent { font-weight: 800; text-transform: uppercase; } - .cron-timer { color: var(--yellow); font-weight: 800; } - .cron-iter { font-size: 0.75em; color: var(--muted); } - - .audit-container { height: 450px; overflow: auto; border: 1px solid var(--line); background: var(--panel-2); border-radius: 4px; margin-top: 10px; } - table { width: 100%; border-collapse: collapse; font-size: 0.78em; table-layout: fixed; } - th { text-align: left; padding: 12px; background: #000; border-bottom: 2px solid var(--line); color: var(--muted); font-weight: 800; } - td { padding: 12px; border-bottom: 1px solid var(--line); vertical-align: top; } + .shell-line.registry { color: var(--green); border-color: var(--green); } + .shell-line.orchestrator { color: var(--pink); border-color: var(--pink); } + .shell-line.substrate { color: var(--cyan); border-color: var(--cyan); } + .shell-line.error { color: var(--red); background: rgba(255,85,85,0.1); border-color: var(--red); } .stat-box { transition: all 0.3s ease; border: 1px solid var(--line); padding: 12px; background: var(--panel-2); border-radius: 4px; margin-bottom: 10px; } .stat-box.active-glow { box-shadow: 0 0 15px rgba(255, 255, 255, 0.1); } @@ -327,245 +190,160 @@ app.get("/", (c) => { .badge { display: inline-block; padding: 2px 8px; border-radius: 4px; font-size: 0.7em; font-weight: 800; text-transform: uppercase; border: 1px solid var(--line); } .badge.running { background: rgba(175,245,180,0.15); color: var(--green); border-color: var(--green); } - .badge.resuming { background: rgba(88,166,255,0.15); color: var(--cyan); border-color: var(--cyan); } - .badge.suspending { background: rgba(255,184,108,0.15); color: var(--orange); border-color: var(--orange); } + .badge.working { background: rgba(255,121,198,0.15); color: var(--pink); border-color: var(--pink); } .badge.suspended { color: var(--muted); opacity: 0.6; } - .badge.error { background: rgba(255,85,85,0.15); color: var(--red); border-color: var(--red); } - - .btn { background: var(--accent); color: #000; border: 0; padding: 10px 20px; border-radius: 4px; font-weight: 800; cursor: pointer; text-transform: uppercase; font-size: 0.8em; } + + .btn { background: var(--pink); color: #000; border: 0; padding: 8px 16px; border-radius: 4px; font-weight: 800; cursor: pointer; text-transform: uppercase; font-size: 0.7em; } .btn:hover { filter: brightness(1.1); } - .btn-reset { background: #000; color: var(--red); border: 1px solid var(--red); margin-left: 10px; } + + table { width: 100%; border-collapse: collapse; font-size: 0.78em; } + th { text-align: left; padding: 10px; background: #000; border-bottom: 2px solid var(--line); color: var(--muted); } + td { padding: 10px; border-bottom: 1px solid var(--line); vertical-align: top; }
-

Substrate multiplex demo V11.16.1 MASTER

-
CONNECTING...
+

Fleet management broker V11.17.0

+
POLLING BROKER...
- Multiplexing 3 Logical NanoClaw Agents onto 2 substrate workers. This version features State Settlement Logic for reliable rehydration. + Simulating a Managed Fleet Orchestration Flow: NanoClaw agents self-register with the Broker on boot. Physical hardware (2 Pods) multiplexes logical sessions (3 Agents).
-

MT Broker: Orchestration Shell Log

+

Fleet Decision Stream

+

Live telemetry from the external Broker service handling custom registration and task dispatching.

- Advanced Oversubscription Forecast + Substrate Economics
1.50x
Density Ratio
33.3%
HW Savings
$5.00
Dedicated /mo
$0.50
Substrate /mo
-
- Overcommit Reality: Typical workload profile: 3 agents triggering at 1m, 2m, 3m intervals. -

- Logical Work: 0s | - Physical Hardware: 0s +
+ Logical Work: 0s | + Physical HW: 0s
-
-

Dynamic Cron Task Tracker

-
-
-
-

Task Timeline: Queuing Status

-
-
- - -
-
+
+

Platform Registry

+

Agents that have successfully "Checked In" with the Fleet Management Broker.

+
-
-
+

Physical Resource Map

-
-
-
-

Logical Actor Fleet

-
+

Physical pods being recycled by the Substrate control plane.

+
-

Task Audit: Reasoning History

-
- - - -
TimeAgentTaskReasoning Payload
-
+

Logical Actor Fleet

+

Stateful sessions rehydrating across pods. Snapshotted automatically by Nano Service.

+
- Google Substrate v2026.6.22 - High-Fidelity Master Build + Google Substrate x NanoClaw POC + High-Fidelity Platform Build
`); }); +app.get("/api/broker", async (c) => { + const r = await fetch(`${BROKER_URL}/status`); + return c.json(await r.json()); +}); + +app.post("/api/trigger/:id", async (c) => { + const id = c.req.param("id"); + await fetch(`${BROKER_URL}/trigger/${id}`, { method: "POST" }); + return c.json({ ok: true }); +}); + app.get("/api/pods", (c) => c.json({ pods: clusterState.pods })); app.get("/api/actors", (c) => c.json({ actors: clusterState.actors })); -app.get("/api/audit", (c) => c.json({ audits: [...taskAudits].reverse() })); -app.get("/api/timeline", (c) => c.json({ assignments: [...assignments].reverse().slice(0, 10) })); -app.get("/api/cron", (c) => c.json({ lastTrigger: lastTriggerTime, iterations: cronIterations })); app.get("/api/stats", (c) => { const density = stats.totalPhysicalActiveSec > 0 ? (stats.totalLogicalActiveSec / stats.totalPhysicalActiveSec).toFixed(2) : "1.00"; const savings = (100 - (100 / parseFloat(density))).toFixed(1); - return c.json({ logs: shellLogs, density: Math.max(1.5, parseFloat(density)), savings: Math.max(33.3, parseFloat(savings)), logicalTime: stats.totalLogicalActiveSec, physicalTime: stats.totalPhysicalActiveSec }); -}); - -app.post("/api/give-task", async (c) => { - const q = c.req.query("source"); - let name = c.req.query("agent"); - if (!name) { const keys = Object.keys(AGENT_META); name = keys[taskCursor % keys.length]; taskCursor++; } - if (q === "cron") { lastTriggerTime[name] = Date.now(); cronIterations[name]++; logShell(`[broker] CRON Trigger: Received external trigger for **${name}** (Iteration #${cronIterations[name]})`); } - const task = predefinedTasks[Math.floor(Math.random() * predefinedTasks.length)]; - const asg: Assignment = { id: "asg-"+Date.now(), agent: name, task, state: "queued", durationSec: 5, created_at: nowSec() }; - assignments.push(asg); - executeTask(AGENT_META[name].id, asg.id); - return c.json(asg); -}); - -app.post("/api/shell", async (c) => { - const { cmd } = await c.req.json(); - logShell(`[bridge] Executing: ${cmd}`); - try { return c.json({ stdout: await runCmd(cmd) }); } - catch (e: any) { return c.json({ stderr: e.message }, 500); } + return c.json({ logicalTime: stats.totalLogicalActiveSec, physicalTime: stats.totalPhysicalActiveSec, density: Math.max(1.5, parseFloat(density)), savings: Math.max(33.3, parseFloat(savings)) }); }); const port = 8090; diff --git a/demos/sub-agent-multiplex/workload/agent.ts b/demos/sub-agent-multiplex/workload/agent.ts index 0107e9004..258bea5f7 100644 --- a/demos/sub-agent-multiplex/workload/agent.ts +++ b/demos/sub-agent-multiplex/workload/agent.ts @@ -11,10 +11,30 @@ import { serve } from "@hono/node-server"; class NanoClawAgent { private taskCounter: number = 0; private readonly actorId: string; + private readonly brokerUrl: string; constructor() { this.actorId = process.env.ATE_ACTOR_ID || "unknown"; + this.brokerUrl = process.env.BROKER_URL || "http://nano-broker.sub-agent.svc.cluster.local:8091"; console.log(`[NanoClawAgent] Identity ${this.actorId} initialized.`); + + // Self-register with the Fleet Management Broker + this.registerWithBroker(); + } + + private async registerWithBroker() { + console.log(`[NanoClawAgent] Attempting self-registration with broker: ${this.brokerUrl}`); + try { + const resp = await fetch(`${this.brokerUrl}/register`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ actorId: this.actorId }) + }); + const data = await resp.json(); + console.log(`[NanoClawAgent] Registration successful:`, data); + } catch (e: any) { + console.error(`[NanoClawAgent] Registration failed: ${e.message}`); + } } public async performTask(durationMs: number) { From 76bd22b5fc4b128ef5e77e3d38d59e03e467d7e6 Mon Sep 17 00:00:00 2001 From: Maya Wang Date: Tue, 23 Jun 2026 08:28:47 +0000 Subject: [PATCH 3/3] feat(nanoclaw): implement pluggable infrastructure refactor - Modify NanoClaw service (host-sweep.ts) to delegate orchestration to external broker - Implement '/notify-due' endpoint in Fleet Management Broker for infra alerts - Enable agent self-registration on boot for full end-to-end platform flow - Align with Dima's request to replace internal cron logic with external service calls --- demos/sub-agent-multiplex/broker/server.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/demos/sub-agent-multiplex/broker/server.ts b/demos/sub-agent-multiplex/broker/server.ts index 699701e93..b7c140a30 100644 --- a/demos/sub-agent-multiplex/broker/server.ts +++ b/demos/sub-agent-multiplex/broker/server.ts @@ -74,6 +74,16 @@ app.post("/register", async (c) => { return c.json({ status: "registered", broker: "FleetBroker-v1" }); }); +// Infrastructure calls this when it detects due work +app.post("/notify-due", async (c) => { + const { sessionId, dueCount } = await c.req.json(); + log("orchestrator", `Infrastructure Alert: **${sessionId}** has ${dueCount} tasks pending.`); + + // Custom Platform Policy: Always trigger if work is due + performTask(sessionId); + return c.json({ status: "processed" }); +}); + // Dashboard polls this for "Platform View" app.get("/status", (c) => { return c.json({