Compare commits

..

17 Commits

Author SHA1 Message Date
xiaoju f87cb38a67 fix: review — stable sort fallback, cleaner status colors
- Sort: threads without startedAt pushed to bottom (not random)
- Colors: completed=success(green), running/active=accent(blue), failed=error(red)
- Remove opacity hack, simplify ternary

小橘 <xiaoju@shazhou.work>
2026-05-11 12:14:51 +00:00
xiaoju 0970139418 fix: sort thread list newest-first and differentiate status colors
- Sort threads by startedAt descending (newest first)
- completed: green (dimmed) — distinct from running (green, full opacity)
- active: accent color (blue) — was same muted gray as completed
- failed: red — unchanged
- running: green — unchanged

Fixes #191
2026-05-11 12:09:48 +00:00
xiaoju 376dd87b6b chore: bump to v0.3.1, fix workspace:* in published packages
v0.3.0 was published with workspace:* deps (npm doesn't resolve them).
Re-published as v0.3.1 with concrete version refs.
Local monorepo deps restored to workspace:*.

小橘 <xiaoju@shazhou.work>
2026-05-11 11:00:49 +00:00
xiaoju 4d8469a649 chore: bump all packages to v0.3.0
Published to Gitea npm registry (git.shazhou.work).

小橘 <xiaoju@shazhou.work>
2026-05-11 10:52:35 +00:00
xiaoju a929fa4ccb Merge pull request 'feat: generate LLM summary in __end__ node via ReAct loop' (#190) from feat/187-end-node-llm-summary into main 2026-05-11 10:31:24 +00:00
xiaoju ff3e19fd22 docs: add comments explaining summarizer constants
小橘 <xiaoju@shazhou.work>
2026-05-11 10:28:54 +00:00
xiaoju b509d1715e refactor: extract shared CAS reactor pattern into cas-reactor.ts
Deduplicate CAS_GET_TOOL_DEFINITION, isRecord, toolHandler, and
structuredToolFromSchema between summarizer.ts and extract-fn.ts.

Both now use createCasReactor(provider, cas, opts) and only provide
their own systemPrompt.

小橘 <xiaoju@shazhou.work>
2026-05-11 10:25:31 +00:00
xiaoju b93f6e736f feat: generate LLM summary in __end__ node via ReAct loop
Instead of hardcoding 'completed: moderator returned END', the engine now
calls a summarizer (ReAct loop with cas_get tool) to produce a meaningful
summary of the workflow outcome before writing the __end__ node.

- New summarizer.ts following supervisor.ts pattern
- Uses extract scene LLM provider (falls back to raw completion.summary on failure)
- Tracks step contentHashes for summarizer context
- Schema: z.object({ summary: z.string() })

Refs #187, #188

小橘 <xiaoju@shazhou.work>
2026-05-11 10:11:31 +00:00
xiaoju ec13c19505 Merge pull request 'refactor: replace maxRounds with supervisor check interval' (#186) from refactor/185-remove-max-rounds into main 2026-05-11 09:01:24 +00:00
xiaoju 203b86e827 refactor: remove dead hasRoundsRemaining condition, use FALLBACK
The condition always returned true, making the subsequent FALLBACK → END
unreachable. Simplified to FALLBACK → coder directly.

Refs #185
2026-05-11 08:59:14 +00:00
xiaoju 90de1c7025 test: fix develop moderator tests for supervisor-controlled termination
hasRoundsRemaining is now always true — supervisor controls when to
stop, not a round counter. Tests updated to expect coder retry
instead of END on exhaustion.

Refs #185
2026-05-11 08:55:49 +00:00
xiaoju 2b587612d5 refactor: replace maxRounds with supervisor check interval
Removes maxRounds as a hard stop limit from the entire stack. The supervisor
(already configured via workflow.yaml supervisorInterval) is now the sole
termination authority.

Changes across 27 files in 11 packages:
- workflow-protocol: StartStep.meta is now empty, StartNodePayload drops maxRounds
- workflow-cas: isStartPayload no longer checks maxRounds
- workflow-execute: engine, worker, fork-thread all stripped of maxRounds plumbing
- cli-workflow: --max-rounds flag removed from CLI and HTTP API
- workflow-runtime: build-context and create-workflow no longer reference maxRounds
- workflow-dashboard: UI no longer sends maxRounds
- workflow-template-develop/solve-issue: moderator no longer checks rounds remaining
- All tests updated

Fixes #185
2026-05-11 08:51:35 +00:00
xingyue 2342a6e3bd fix: login.tsx use new gateway endpoint path 2026-05-11 16:46:38 +08:00
xingyue 0021596ff0 Merge pull request 'refactor: simplify ExtractFn to (schema, contentHash)' (#184) from refactor/180-simplify-extract-fn into main 2026-05-11 08:03:48 +00:00
xiaomo 56ec8cd401 Merge pull request 'refactor: reorganize gateway routes under /api/ prefix' (#183) from feat/177-gateway-route-reorg into main 2026-05-11 08:01:02 +00:00
xingyue b783027406 fix: remove stray lockfiles + refactor gateway auth with Hono group
- Remove root and workflow-gateway pnpm-lock.yaml (workspace mode)
- Replace startsWith auth skip with Hono route group
- Gateway management routes use GATEWAY_SECRET (per-route)
- /api/gateway/endpoints + /api/agents/* use dashboard auth
- No more global /api/* middleware with path-based exceptions
2026-05-11 15:59:42 +08:00
xingyue 93145cf08c refactor: reorganize gateway routes under /api/ prefix (closes #178, closes #179)
- Gateway management: /api/gateway/register, /api/gateway/endpoints
- Agent proxy: /api/agents/:agent/*
- /healthz stays at root (CF/k8s convention)
- Skip dashboard auth for gateway register routes
- Update CLI serve tunnel registration paths
- Update dashboard API client paths

Ref: #177
2026-05-11 15:48:13 +08:00
69 changed files with 3482 additions and 321 deletions
@@ -45,7 +45,7 @@ describe("gc cli and garbageCollectCas", () => {
{
name: "demo",
hash: bundleHash,
maxRounds: 5,
depth: 0,
},
promptHash,
@@ -100,7 +100,7 @@ describe("gc cli and garbageCollectCas", () => {
{
name: "demo",
hash: bundleHash,
maxRounds: 5,
depth: 0,
},
promptHash,
@@ -135,7 +135,7 @@ describe("gc cli and garbageCollectCas", () => {
{
name: "demo",
hash: bundleHash,
maxRounds: 5,
depth: 0,
},
promptHash,
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@uncaged/cli-workflow",
"version": "0.2.0",
"version": "0.3.1",
"type": "module",
"bin": {
"uncaged-workflow": "src/cli.ts"
+51
View File
@@ -0,0 +1,51 @@
lockfileVersion: '9.0'
settings:
autoInstallPeers: true
excludeLinksFromLockfile: false
importers:
.:
dependencies:
'@uncaged/workflow-cas':
specifier: workspace:*
version: link:../workflow-cas
'@uncaged/workflow-execute':
specifier: workspace:*
version: link:../workflow-execute
'@uncaged/workflow-protocol':
specifier: workspace:*
version: link:../workflow-protocol
'@uncaged/workflow-register':
specifier: workspace:*
version: link:../workflow-register
'@uncaged/workflow-runtime':
specifier: workspace:*
version: link:../workflow-runtime
'@uncaged/workflow-util':
specifier: workspace:*
version: link:../workflow-util
hono:
specifier: ^4.12.18
version: 4.12.18
yaml:
specifier: ^2.8.4
version: 2.8.4
packages:
hono@4.12.18:
resolution: {integrity: sha512-RWzP96k/yv0PQfyXnWjs6zot20TqfpfsNXhOnev8d1InAxubW93L11/oNUc3tQqn2G0bSdAOBpX+2uDFHV7kdQ==}
engines: {node: '>=16.9.0'}
yaml@2.8.4:
resolution: {integrity: sha512-ml/JPOj9fOQK8RNnWojA67GbZ0ApXAUlN2UQclwv2eVgTgn7O9gg9o7paZWKMp4g0H3nTLtS9LVzhkpOFIKzog==}
engines: {node: '>= 14.6'}
hasBin: true
snapshots:
hono@4.12.18: {}
yaml@2.8.4: {}
@@ -156,13 +156,12 @@ export function createThreadRoutes(storageRoot: string): Hono {
const name = body.workflow;
const prompt = body.prompt;
const maxRounds = typeof body.maxRounds === "number" ? body.maxRounds : 10;
if (typeof name !== "string" || typeof prompt !== "string") {
return c.json({ error: "workflow (string) and prompt (string) are required" }, 400);
}
const result = await cmdRun(storageRoot, name, prompt, maxRounds);
const result = await cmdRun(storageRoot, name, prompt);
if (!result.ok) {
return c.json({ error: result.error }, 400);
}
@@ -42,7 +42,7 @@ export async function registerWithGateway(
agentToken: string,
): Promise<boolean> {
try {
const resp = await fetch(`${gatewayUrl}/register`, {
const resp = await fetch(`${gatewayUrl}/api/gateway/register`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ name, url: tunnelUrl, secret, agentToken }),
@@ -65,7 +65,7 @@ export async function unregisterFromGateway(
secret: string,
): Promise<void> {
try {
await fetch(`${gatewayUrl}/register/${name}`, {
await fetch(`${gatewayUrl}/api/gateway/register/${name}`, {
method: "DELETE",
headers: { Authorization: `Bearer ${secret}` },
});
@@ -26,12 +26,7 @@ export async function dispatchRun(storageRoot: string, argv: string[]): Promise<
return 1;
}
const result = await cmdRun(
storageRoot,
parsed.value.name,
parsed.value.prompt,
parsed.value.maxRounds,
);
const result = await cmdRun(storageRoot, parsed.value.name, parsed.value.prompt);
if (!result.ok) {
printCliError(result.error);
return 1;
@@ -166,7 +161,7 @@ export async function dispatchFork(storageRoot: string, argv: string[]): Promise
export const THREAD_SUBCOMMAND_TABLE: Record<string, CommandEntry> = {
run: {
handler: dispatchRun,
args: "<name> [--prompt <text>] [--max-rounds N]",
args: "<name> [--prompt <text>]",
description: "Start a new thread executing a workflow",
},
list: {
@@ -10,7 +10,6 @@ export async function cmdRun(
storageRoot: string,
name: string,
prompt: string,
maxRounds: number,
): Promise<Result<{ threadId: string }, string>> {
const nameOk = validateCliWorkflowName(name);
if (!nameOk.ok) {
@@ -41,7 +40,7 @@ export async function cmdRun(
threadId,
workflowName: name,
prompt,
options: { maxRounds, depth: 0 },
options: { depth: 0 },
},
{ awaitResponseLine: false },
);
+6 -23
View File
@@ -3,12 +3,12 @@ import { err, ok, type Result } from "@uncaged/workflow-protocol";
export type ParsedRunArgv = {
name: string;
prompt: string;
maxRounds: number;
};
type FlagOk = { kind: "prompt"; value: string } | { kind: "max-rounds"; value: number };
function parseFlagAt(argv: string[], index: number): Result<FlagOk, string> | null {
function parseFlagAt(
argv: string[],
index: number,
): Result<{ kind: "prompt"; value: string }, string> | null {
const flag = argv[index];
if (flag === "--prompt") {
const value = argv[index + 1];
@@ -17,24 +17,12 @@ function parseFlagAt(argv: string[], index: number): Result<FlagOk, string> | nu
}
return ok({ kind: "prompt", value });
}
if (flag === "--max-rounds") {
const value = argv[index + 1];
if (value === undefined) {
return err("missing value for --max-rounds");
}
const n = Number(value);
if (!Number.isFinite(n) || !Number.isInteger(n) || n < 0) {
return err("--max-rounds must be a non-negative integer");
}
return ok({ kind: "max-rounds", value: n });
}
return null;
}
export function parseRunArgv(argv: string[]): Result<ParsedRunArgv, string> {
let name: string | undefined;
let prompt = "";
let maxRounds = 10;
let i = 0;
const first = argv[0];
@@ -54,12 +42,7 @@ export function parseRunArgv(argv: string[]): Result<ParsedRunArgv, string> {
}
const flag = parsed.value;
if (flag.kind === "prompt") {
prompt = flag.value;
i += 2;
continue;
}
maxRounds = flag.value;
prompt = flag.value;
i += 2;
}
@@ -67,5 +50,5 @@ export function parseRunArgv(argv: string[]): Result<ParsedRunArgv, string> {
return err("run requires <name>");
}
return ok({ name, prompt, maxRounds });
return ok({ name, prompt });
}
-6
View File
@@ -107,12 +107,6 @@ ${commandSections.join("\n\n")}
| \`completed\` | Finished with \`returnCode === 0\` (has \`__end__\` frame in CAS) |
| \`failed\` | Finished with non-zero return code, or worker crashed (dead PID / no ctl) |
## Defaults
| Setting | CLI | HTTP API |
|---------|-----|----------|
| \`maxRounds\` | 10 | 10 |
## Exit Codes
| Code | Meaning |
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@uncaged/workflow-agent-cursor",
"version": "0.2.0",
"version": "0.3.1",
"type": "module",
"main": "src/index.ts",
"types": "src/index.ts",
+28
View File
@@ -0,0 +1,28 @@
lockfileVersion: '9.0'
settings:
autoInstallPeers: true
excludeLinksFromLockfile: false
importers:
.:
dependencies:
'@uncaged/workflow-runtime':
specifier: workspace:*
version: link:../workflow-runtime
'@uncaged/workflow-util-agent':
specifier: workspace:*
version: link:../workflow-util-agent
zod:
specifier: ^4.0.0
version: 4.4.3
packages:
zod@4.4.3:
resolution: {integrity: sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==}
snapshots:
zod@4.4.3: {}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@uncaged/workflow-agent-hermes",
"version": "0.2.0",
"version": "0.3.1",
"type": "module",
"main": "src/index.ts",
"types": "src/index.ts",
+16
View File
@@ -0,0 +1,16 @@
lockfileVersion: '9.0'
settings:
autoInstallPeers: true
excludeLinksFromLockfile: false
importers:
.:
dependencies:
'@uncaged/workflow-runtime':
specifier: workspace:*
version: link:../workflow-runtime
'@uncaged/workflow-util-agent':
specifier: workspace:*
version: link:../workflow-util-agent
@@ -8,7 +8,7 @@ function makeCtx(userContent: string): AgentContext {
start: {
role: START,
content: userContent,
meta: { maxRounds: 10 },
meta: {},
timestamp: 1,
},
depth: 0,
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@uncaged/workflow-agent-llm",
"version": "0.2.0",
"version": "0.3.1",
"type": "module",
"main": "src/index.ts",
"types": "src/index.ts",
+13
View File
@@ -0,0 +1,13 @@
lockfileVersion: '9.0'
settings:
autoInstallPeers: true
excludeLinksFromLockfile: false
importers:
.:
dependencies:
'@uncaged/workflow-runtime':
specifier: workspace:*
version: link:../workflow-runtime
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@uncaged/workflow-cas",
"version": "0.1.0",
"version": "0.3.1",
"type": "module",
"scripts": {
"test": "bun test"
+75
View File
@@ -0,0 +1,75 @@
lockfileVersion: '9.0'
settings:
autoInstallPeers: true
excludeLinksFromLockfile: false
importers:
.:
dependencies:
'@uncaged/workflow-protocol':
specifier: workspace:*
version: link:../workflow-protocol
'@uncaged/workflow-util':
specifier: workspace:*
version: link:../workflow-util
xxhashjs:
specifier: ^0.2.2
version: 0.2.2
yaml:
specifier: ^2.7.1
version: 2.8.4
devDependencies:
'@types/bun':
specifier: latest
version: 1.3.13
packages:
'@types/bun@1.3.13':
resolution: {integrity: sha512-9fqXWk5YIHGGnUau9TEi+qdlTYDAnOj+xLCmSTwXfAIqXr2x4tytJb43E9uCvt09zJURKXwAtkoH4nLQfzeTXw==}
'@types/node@25.6.2':
resolution: {integrity: sha512-sokuT28dxf9JT5Kady1fsXOvI4HVpjZa95NKT5y9PNTIrs2AsobR4GFAA90ZG8M+nxVRLysCXsVj6eGC7Vbrlw==}
bun-types@1.3.13:
resolution: {integrity: sha512-QXKeHLlOLqQX9LgYaHJfzdBaV21T63HhFJnvuRCcjZiaUDpbs5ED1MgxbMra71CsryN/1dAoXuJJJwIv/2drVA==}
cuint@0.2.2:
resolution: {integrity: sha512-d4ZVpCW31eWwCMe1YT3ur7mUDnTXbgwyzaL320DrcRT45rfjYxkt5QWLrmOJ+/UEAI2+fQgKe/fCjR8l4TpRgw==}
undici-types@7.19.2:
resolution: {integrity: sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg==}
xxhashjs@0.2.2:
resolution: {integrity: sha512-AkTuIuVTET12tpsVIQo+ZU6f/qDmKuRUcjaqR+OIvm+aCBsZ95i7UVY5WJ9TMsSaZ0DA2WxoZ4acu0sPH+OKAw==}
yaml@2.8.4:
resolution: {integrity: sha512-ml/JPOj9fOQK8RNnWojA67GbZ0ApXAUlN2UQclwv2eVgTgn7O9gg9o7paZWKMp4g0H3nTLtS9LVzhkpOFIKzog==}
engines: {node: '>= 14.6'}
hasBin: true
snapshots:
'@types/bun@1.3.13':
dependencies:
bun-types: 1.3.13
'@types/node@25.6.2':
dependencies:
undici-types: 7.19.2
bun-types@1.3.13:
dependencies:
'@types/node': 25.6.2
cuint@0.2.2: {}
undici-types@7.19.2: {}
xxhashjs@0.2.2:
dependencies:
cuint: 0.2.2
yaml@2.8.4: {}
-1
View File
@@ -21,7 +21,6 @@ function isStartPayload(value: unknown): value is StartNodePayload {
return (
typeof value.name === "string" &&
typeof value.hash === "string" &&
typeof value.maxRounds === "number" &&
typeof value.depth === "number"
);
}
File diff suppressed because it is too large Load Diff
+3 -4
View File
@@ -28,7 +28,7 @@ function authHeaders(): Record<string, string> {
function agentBase(agent: string): string {
if (GATEWAY_URL) {
return `${GATEWAY_URL}/api/${agent}`;
return `${GATEWAY_URL}/api/agents/${agent}`;
}
// Local dev: proxy via vite, no agent prefix
return "/api";
@@ -108,7 +108,7 @@ export type ThreadRecord = ThreadStartRecord | RoleRecord | WorkflowResultRecord
export function listAgents(): Promise<AgentEndpoint[]> {
const url = GATEWAY_URL || "";
return fetchJson(url, "/endpoints");
return fetchJson(url, "/api/gateway/endpoints");
}
// ── Agent-scoped endpoints ──────────────────────────────────────────
@@ -133,9 +133,8 @@ export function runThread(
agent: string,
workflow: string,
prompt: string,
maxRounds: number = 10,
): Promise<{ threadId: string }> {
return postJson(agentBase(agent), "/threads", { workflow, prompt, maxRounds });
return postJson(agentBase(agent), "/threads", { workflow, prompt });
}
export function killThread(agent: string, threadId: string): Promise<{ ok: boolean }> {
+1 -1
View File
@@ -1,5 +1,5 @@
import { useState } from "react";
import { hasApiKey, clearApiKey } from "./api.ts";
import { clearApiKey, hasApiKey } from "./api.ts";
import { LoginPage } from "./components/login.tsx";
import { RunDialog } from "./components/run-dialog.tsx";
import { Sidebar } from "./components/sidebar.tsx";
@@ -20,7 +20,7 @@ export function LoginPage({ onLogin }: Props) {
// Test the key by hitting the endpoints list
const gatewayUrl = import.meta.env.VITE_GATEWAY_URL || "";
try {
const res = await fetch(`${gatewayUrl}/endpoints`, {
const res = await fetch(`${gatewayUrl}/api/gateway/endpoints`, {
headers: { Authorization: `Bearer ${key.trim()}` },
});
if (res.status === 401) {
@@ -1,10 +1,10 @@
import ReactMarkdown from "react-markdown";
import { useEffect, useState } from "react";
import ReactMarkdown from "react-markdown";
import {
createHighlighter,
type HighlighterGeneric,
type BundledLanguage,
type BundledTheme,
createHighlighter,
type HighlighterGeneric,
} from "shiki";
let highlighterPromise: Promise<HighlighterGeneric<BundledLanguage, BundledTheme>> | null = null;
@@ -1,4 +1,4 @@
import type { ThreadStartRecord, RoleRecord, WorkflowResultRecord, ThreadRecord } from "../api.ts";
import type { RoleRecord, ThreadRecord, ThreadStartRecord, WorkflowResultRecord } from "../api.ts";
import { Markdown } from "./markdown.tsx";
const ROLE_COLORS: Record<string, string> = {
@@ -12,7 +12,6 @@ export function RunDialog({ agent, onClose, onCreated }: Props) {
const workflows = useFetch(() => listWorkflows(agent), [agent]);
const [workflow, setWorkflow] = useState("");
const [prompt, setPrompt] = useState("");
const [maxRounds, setMaxRounds] = useState(10);
const [submitting, setSubmitting] = useState(false);
const [error, setError] = useState<string | null>(null);
@@ -22,7 +21,7 @@ export function RunDialog({ agent, onClose, onCreated }: Props) {
setSubmitting(true);
setError(null);
try {
const result = await runThread(agent, workflow, prompt, maxRounds);
const result = await runThread(agent, workflow, prompt);
onCreated(result.threadId);
} catch (err) {
setError(err instanceof Error ? err.message : String(err));
@@ -91,29 +90,6 @@ export function RunDialog({ agent, onClose, onCreated }: Props) {
placeholder="Enter the task prompt..."
/>
</div>
<div>
<label
htmlFor="run-max-rounds"
className="text-sm block mb-1"
style={{ color: "var(--color-text-muted)" }}
>
Max Rounds
</label>
<input
id="run-max-rounds"
type="number"
value={maxRounds}
onChange={(e) => setMaxRounds(Number(e.target.value))}
min={1}
max={100}
className="w-24 px-3 py-2 rounded border text-sm"
style={{
background: "var(--color-bg)",
borderColor: "var(--color-border)",
color: "var(--color-text)",
}}
/>
</div>
{error && (
<p className="text-sm" style={{ color: "var(--color-error)" }}>
{error}
@@ -13,7 +13,12 @@ export function ThreadList({ agent, onSelect }: Props) {
return <p style={{ color: "var(--color-text-muted)" }}>Loading threads...</p>;
if (status === "error") return <p style={{ color: "var(--color-error)" }}>Error: {error}</p>;
const threads = data.threads;
const threads = [...data.threads].sort((a, b) => {
if (!a.startedAt && !b.startedAt) return 0;
if (!a.startedAt) return 1;
if (!b.startedAt) return -1;
return b.startedAt.localeCompare(a.startedAt);
});
return (
<div>
@@ -39,11 +44,11 @@ export function ThreadList({ agent, onSelect }: Props) {
className="text-xs px-2 py-0.5 rounded"
style={{
background:
t.status === "running"
t.status === "completed"
? "var(--color-success)"
: t.status === "failed"
? "var(--color-error)"
: "var(--color-text-muted)",
: "var(--color-accent)",
color: "#000",
}}
>
@@ -34,7 +34,6 @@ function noLogger(): (tag: string, content: string) => void {
function makeOptions(overrides: Partial<ExecuteThreadOptions>): ExecuteThreadOptions {
return {
maxRounds: 5,
depth: 0,
signal: new AbortController().signal,
awaitAfterEachYield: async () => {},
@@ -107,7 +106,7 @@ describe("executeThread (Phase 2 — CAS thread storage)", () => {
wf,
"demo",
{ prompt: "hello", steps: [] },
makeOptions({ storageRoot, maxRounds: 5 }),
makeOptions({ storageRoot }),
io,
noLogger(),
);
@@ -127,7 +126,6 @@ describe("executeThread (Phase 2 — CAS thread storage)", () => {
expect(startNode.type).toBe("start");
expect((startNode.payload as Record<string, unknown>).name).toBe("demo");
expect((startNode.payload as Record<string, unknown>).hash).toBe(bundleHash);
expect((startNode.payload as Record<string, unknown>).maxRounds).toBe(5);
const refs = startNode.refs as string[];
expect(refs.length).toBe(1);
@@ -164,7 +162,6 @@ describe("executeThread (Phase 2 — CAS thread storage)", () => {
const opts = makeOptions({
storageRoot,
maxRounds: 5,
awaitAfterEachYield: async () => {
const text = await readFile(join(bundleDir, "threads.json"), "utf8");
const parsed = JSON.parse(text) as Record<string, { head: string }>;
@@ -228,7 +225,7 @@ describe("executeThread (Phase 2 — CAS thread storage)", () => {
wf,
"demo",
{ prompt: "p", steps: [] },
makeOptions({ storageRoot, maxRounds: 5 }),
makeOptions({ storageRoot }),
io,
noLogger(),
);
@@ -279,7 +276,7 @@ describe("executeThread (Phase 2 — CAS thread storage)", () => {
wf,
"demo",
{ prompt: "p", steps: [] },
makeOptions({ storageRoot, maxRounds: 5 }),
makeOptions({ storageRoot }),
io,
noLogger(),
);
@@ -45,7 +45,6 @@ describe("garbageCollectCas (mark-and-sweep)", () => {
{
name: "demo",
hash: bundleHash,
maxRounds: 5,
depth: 0,
},
promptHash,
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@uncaged/workflow-execute",
"version": "0.2.0",
"version": "0.3.1",
"type": "module",
"exports": {
".": {
+51
View File
@@ -0,0 +1,51 @@
lockfileVersion: '9.0'
settings:
autoInstallPeers: true
excludeLinksFromLockfile: false
importers:
.:
dependencies:
'@uncaged/workflow-cas':
specifier: workspace:*
version: link:../workflow-cas
'@uncaged/workflow-protocol':
specifier: workspace:*
version: link:../workflow-protocol
'@uncaged/workflow-reactor':
specifier: workspace:*
version: link:../workflow-reactor
'@uncaged/workflow-register':
specifier: workspace:*
version: link:../workflow-register
'@uncaged/workflow-runtime':
specifier: workspace:*
version: link:../workflow-runtime
'@uncaged/workflow-util':
specifier: workspace:*
version: link:../workflow-util
yaml:
specifier: ^2.7.1
version: 2.8.4
devDependencies:
zod:
specifier: ^4.0.0
version: 4.4.3
packages:
yaml@2.8.4:
resolution: {integrity: sha512-ml/JPOj9fOQK8RNnWojA67GbZ0ApXAUlN2UQclwv2eVgTgn7O9gg9o7paZWKMp4g0H3nTLtS9LVzhkpOFIKzog==}
engines: {node: '>= 14.6'}
hasBin: true
zod@4.4.3:
resolution: {integrity: sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==}
snapshots:
yaml@2.8.4: {}
zod@4.4.3: {}
@@ -0,0 +1,79 @@
import type { CasStore } from "@uncaged/workflow-cas";
import type { ThreadReactorFn } from "@uncaged/workflow-reactor";
import { createLlmFn, createThreadReactor } from "@uncaged/workflow-reactor";
import type { LlmProvider } from "@uncaged/workflow-runtime";
import { extractFunctionToolFromZodSchema } from "./extract/index.js";
export type CasReactorThread = {
cas: CasStore;
};
const CAS_GET_TOOL_DEFINITION = {
type: "function" as const,
function: {
name: "cas_get",
description:
"Read a Merkle DAG node from content-addressed storage by its hash. Returns YAML-formatted node with type, payload, and refs or children fields (content nodes use refs).",
parameters: {
type: "object",
properties: {
hash: { type: "string", description: "The CAS hash to retrieve" },
},
required: ["hash"],
},
},
};
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null && !Array.isArray(value);
}
export type CasReactorOpts = {
maxRounds: number;
systemPromptForStructuredTool: (structuredToolName: string) => string;
};
export function createCasReactor(
provider: LlmProvider,
cas: CasStore,
opts: CasReactorOpts,
): ThreadReactorFn<CasReactorThread> {
return createThreadReactor<CasReactorThread>({
llm: createLlmFn(provider),
maxRounds: opts.maxRounds,
staticTools: [CAS_GET_TOOL_DEFINITION],
structuredToolFromSchema: (schema) => {
const t = extractFunctionToolFromZodSchema(schema);
return {
name: t.name,
tool: {
type: "function" as const,
function: {
name: t.name,
description: t.description,
parameters: t.parameters,
},
},
};
},
systemPromptForStructuredTool: opts.systemPromptForStructuredTool,
toolHandler: async (call, _thread) => {
if (call.function.name !== "cas_get") {
return `Unknown tool: ${call.function.name}`;
}
let hash: string;
try {
const ta = JSON.parse(call.function.arguments) as unknown;
if (!isRecord(ta) || typeof ta.hash !== "string") {
return 'cas_get requires {"hash": "<cas-hash>"}.';
}
hash = ta.hash;
} catch {
return "cas_get arguments were not valid JSON.";
}
const blob = await cas.get(hash);
return blob === null ? "null" : blob;
},
});
}
+25 -35
View File
@@ -26,6 +26,7 @@ import { END, START } from "@uncaged/workflow-runtime";
import { err, type LogFn, ok, type Result } from "@uncaged/workflow-util";
import { createExtract } from "../extract/index.js";
import { createSummarizer, type SummarizeFn } from "./summarizer.js";
import { runSupervisor } from "./supervisor.js";
import {
appendThreadHistoryEntry,
@@ -53,6 +54,7 @@ async function resolveEngineRegistryRuntime(
Result<
{
extract: ReturnType<typeof createExtract>;
summarize: SummarizeFn;
workflowConfig: WorkflowConfig;
},
string
@@ -76,7 +78,11 @@ async function resolveEngineRegistryRuntime(
apiKey: ex.apiKey,
model: ex.model,
};
return ok({ extract: createExtract(llmProvider, { cas }), workflowConfig: cfg });
return ok({
extract: createExtract(llmProvider, { cas }),
summarize: createSummarizer(llmProvider, cas),
workflowConfig: cfg,
});
}
async function appendStateForStep(params: {
@@ -250,6 +256,7 @@ async function driveWorkflowGenerator(params: {
bundleDir: string;
startHash: string;
chain: ChainState;
summarize: SummarizeFn;
}): Promise<WorkflowResult> {
const {
fn,
@@ -262,6 +269,7 @@ async function driveWorkflowGenerator(params: {
cas,
bundleDir,
startHash,
summarize,
} = params;
let chain: ChainState = params.chain;
const gen = fn(thread, runtime);
@@ -270,6 +278,10 @@ async function driveWorkflowGenerator(params: {
role: s.role,
summary: JSON.stringify(s.meta),
}));
const summarizerSteps: { role: string; contentHash: string }[] = thread.steps.map((s) => ({
role: s.role,
contentHash: s.contentHash,
}));
while (true) {
if (executeOptions.signal.aborted) {
@@ -284,32 +296,24 @@ async function driveWorkflowGenerator(params: {
});
}
if (written >= executeOptions.maxRounds) {
logger("R3CW7YBQ", `thread ${threadId} stopped at maxRounds=${executeOptions.maxRounds}`);
return await finalizeThread({
cas,
bundleDir,
threadId,
startHash,
chain,
completion: {
returnCode: 0,
summary: `completed: reached maxRounds (${executeOptions.maxRounds})`,
},
});
}
const iterResult = await gen.next();
if (iterResult.done) {
logger("F3HN8QKP", `thread ${threadId} generator finished`);
const rawCompletion = iterResult.value;
const llmSummary = await summarize({
prompt: thread.start.content,
recentSteps: summarizerSteps,
fallback: rawCompletion.summary,
logger,
});
return await finalizeThread({
cas,
bundleDir,
threadId,
startHash,
chain,
completion: iterResult.value,
completion: { ...rawCompletion, summary: llmSummary },
});
}
@@ -335,6 +339,7 @@ async function driveWorkflowGenerator(params: {
role: step.role,
summary: JSON.stringify(step.meta),
});
summarizerSteps.push({ role: step.role, contentHash: step.contentHash });
await Promise.race([
executeOptions.awaitAfterEachYield(),
@@ -383,7 +388,7 @@ async function driveWorkflowGenerator(params: {
* Persistence layout (RFC v3 — CAS-based thread storage):
* - Thread chain is written as immutable CAS blobs: a single {@link StartNode}
* plus one {@link StateNode} per role step (including a final `__end__`
* state on completion / abort / `maxRounds`).
* state on completion / abort).
* - The active thread head is published in `<bundleDir>/threads.json`; on
* completion it is removed and a record is appended to
* `<bundleDir>/history/{YYYY-MM-DD}.jsonl`.
@@ -433,7 +438,6 @@ export async function executeThread(
{
name: workflowName,
hash: io.hash,
maxRounds: options.maxRounds,
depth: options.depth,
},
promptHash,
@@ -475,21 +479,6 @@ export async function executeThread(
const nowMs = Date.now();
if (options.maxRounds <= 0) {
logger("R3CW7YBQ", `thread ${io.threadId} stopped at maxRounds=${options.maxRounds}`);
return await finalizeThread({
cas: io.cas,
bundleDir,
threadId: io.threadId,
startHash,
chain,
completion: {
returnCode: 0,
summary: `completed: reached maxRounds (${options.maxRounds})`,
},
});
}
const registryRuntime = await resolveEngineRegistryRuntime(options.storageRoot, io.cas);
if (!registryRuntime.ok) {
throw new Error(registryRuntime.error);
@@ -501,7 +490,7 @@ export async function executeThread(
start: {
role: START,
content: input.prompt,
meta: { maxRounds: options.maxRounds },
meta: {},
timestamp: nowMs,
},
steps: input.steps.map((out, i) => ({
@@ -530,5 +519,6 @@ export async function executeThread(
bundleDir,
startHash,
chain,
summarize: registryRuntime.value.summarize,
});
}
@@ -104,9 +104,7 @@ async function readPromptText(cas: CasStore, promptHash: string): Promise<Result
async function readStartWorkflowIdentity(params: {
cas: CasStore;
startHash: string;
}): Promise<
Result<{ workflowName: string; maxRounds: number; depth: number; prompt: string }, string>
> {
}): Promise<Result<{ workflowName: string; depth: number; prompt: string }, string>> {
const yamlText = await params.cas.get(params.startHash);
if (yamlText === null) {
return err(`start node missing in CAS: ${params.startHash}`);
@@ -127,7 +125,6 @@ async function readStartWorkflowIdentity(params: {
const p = parsed.node.payload;
return ok({
workflowName: p.name,
maxRounds: p.maxRounds,
depth: p.depth,
prompt: prompt.value,
});
@@ -317,7 +314,7 @@ export async function prepareCasFork(params: {
hash: params.bundleHash,
sourceThreadId: params.sourceThreadId,
prompt: id.value.prompt,
runOptions: { maxRounds: id.value.maxRounds, depth: id.value.depth },
runOptions: { depth: id.value.depth },
steps,
stepTimestamps,
forkContinuation: cont.value,
@@ -0,0 +1,56 @@
import type { CasStore } from "@uncaged/workflow-cas";
import type { LlmProvider } from "@uncaged/workflow-runtime";
import type { LogFn } from "@uncaged/workflow-util";
import * as z from "zod/v4";
import { createCasReactor } from "../cas-reactor.js";
/** Max ReAct rounds: 3 cas_get reads + 1 structured output = 4 rounds is sufficient. */
const SUMMARIZER_MAX_REACT_ROUNDS = 4;
/** Only pass the last N steps; each step is just a role+contentHash reference (~60 chars), not full content. */
const SUMMARIZER_RECENT_STEP_LIMIT = 20;
const summarySchema = z.object({ summary: z.string() }).meta({
title: "workflow_summary",
description: "A concise summary of the completed workflow's results and outcome.",
});
function buildSummarizerInput(args: {
prompt: string;
recentSteps: readonly { role: string; contentHash: string }[];
}): string {
const recent = args.recentSteps.slice(-SUMMARIZER_RECENT_STEP_LIMIT);
const stepsBlock = recent
.map((s, i) => `${i + 1}. [${s.role}] contentHash: ${s.contentHash}`)
.join("\n");
return `Original task:\n${args.prompt}\n\nCompleted steps (oldest first):\n${stepsBlock === "" ? "(none)" : stepsBlock}\n\nUse cas_get to read step content if needed. Summarize the workflow outcome concisely.`;
}
export type SummarizeFn = (args: {
prompt: string;
recentSteps: readonly { role: string; contentHash: string }[];
fallback: string;
logger: LogFn;
}) => Promise<string>;
export function createSummarizer(provider: LlmProvider, cas: CasStore): SummarizeFn {
const reactor = createCasReactor(provider, cas, {
maxRounds: SUMMARIZER_MAX_REACT_ROUNDS,
systemPromptForStructuredTool: (structuredToolName) =>
`You summarize completed workflow threads. You have access to cas_get to read step content by hash. After reviewing the steps, call the ${structuredToolName} tool with a concise summary of the workflow outcome and results. Or reply with only a JSON object such as {"summary":"..."}.`,
});
return async (args) => {
const result = await reactor({
thread: { cas },
input: buildSummarizerInput(args),
schema: summarySchema,
});
if (!result.ok) {
args.logger("P2WX7KNR", `summarizer failed: ${result.error}`);
return args.fallback;
}
args.logger("Q5MT3VBF", "summarizer produced workflow summary");
return result.value.summary;
};
}
@@ -39,7 +39,6 @@ export type PrefilledDiskStep = {
};
export type ExecuteThreadOptions = {
maxRounds: number;
/** Passed to the bundle thread context as `ThreadContext.depth`. */
depth: number;
signal: AbortSignal;
@@ -68,7 +67,7 @@ export type CasForkPlan = {
hash: string;
sourceThreadId: string;
prompt: string;
runOptions: { maxRounds: number; depth: number };
runOptions: { depth: number };
steps: RoleOutput[];
stepTimestamps: number[];
forkContinuation: ForkContinuationOptions;
@@ -32,7 +32,7 @@ type RunCommand = {
threadId: string;
workflowName: string;
prompt: string;
options: { maxRounds: number; depth: number };
options: { depth: number };
steps: RoleOutput[];
/** Timestamps aligned with `steps` for replay / fork restore; length must match `steps` when steps are non-empty. */
stepTimestamps: number[] | null;
@@ -185,10 +185,6 @@ function parseRunControlPayload(rec: Record<string, unknown>): RunCommand | null
return null;
}
const optRec = options as Record<string, unknown>;
const maxRounds = optRec.maxRounds;
if (typeof maxRounds !== "number") {
return null;
}
const depthRaw = optRec.depth;
const depth =
typeof depthRaw === "number" && Number.isFinite(depthRaw) ? Math.trunc(depthRaw) : 0;
@@ -210,7 +206,7 @@ function parseRunControlPayload(rec: Record<string, unknown>): RunCommand | null
threadId,
workflowName,
prompt,
options: { maxRounds, depth },
options: { depth },
steps: parsedSteps.steps,
stepTimestamps: parsedSteps.stepTimestamps,
forkSourceThreadId,
@@ -1,12 +1,8 @@
import { type CasStore, getContentMerklePayload } from "@uncaged/workflow-cas";
import { createLlmFn, createThreadReactor } from "@uncaged/workflow-reactor";
import type {
ExtractFn,
ExtractResult,
LlmProvider,
} from "@uncaged/workflow-runtime";
import type { ExtractFn, ExtractResult, LlmProvider } from "@uncaged/workflow-runtime";
import type * as z from "zod/v4";
import { extractFunctionToolFromZodSchema } from "./llm-extract.js";
import { createCasReactor } from "../cas-reactor.js";
export type ExtractDeps = {
cas: CasStore;
@@ -14,30 +10,6 @@ export type ExtractDeps = {
const MAX_REACT_ROUNDS = 10;
const CAS_GET_TOOL_DEFINITION = {
type: "function" as const,
function: {
name: "cas_get",
description:
"Read a Merkle DAG node from content-addressed storage by its hash. Returns YAML-formatted node with type, payload, and refs or children fields (content nodes use refs).",
parameters: {
type: "object",
properties: {
hash: { type: "string", description: "The CAS hash to retrieve" },
},
required: ["hash"],
},
},
};
type ExtractThreadContext = {
cas: CasStore;
};
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null && !Array.isArray(value);
}
/**
* Create an ExtractFn backed by an LLM provider.
*
@@ -46,44 +18,10 @@ function isRecord(value: unknown): value is Record<string, unknown> {
* assistant reply as a short-circuit, which covers the legacy "single" extraction path.
*/
export function createExtract(provider: LlmProvider, deps: ExtractDeps): ExtractFn {
const llm = createLlmFn(provider);
const reactor = createThreadReactor<ExtractThreadContext>({
llm,
const reactor = createCasReactor(provider, deps.cas, {
maxRounds: MAX_REACT_ROUNDS,
staticTools: [CAS_GET_TOOL_DEFINITION],
structuredToolFromSchema: (schema) => {
const t = extractFunctionToolFromZodSchema(schema);
return {
name: t.name,
tool: {
type: "function" as const,
function: {
name: t.name,
description: t.description,
parameters: t.parameters,
},
},
};
},
systemPromptForStructuredTool: (structuredToolName) =>
`You extract structured metadata from content. The content is from a CAS node. Use cas_get to read referenced nodes if needed. When ready, call the ${structuredToolName} tool with JSON matching the schema. You may instead reply with only a JSON object (no prose) when no tools are needed.`,
toolHandler: async (call, thread) => {
if (call.function.name !== "cas_get") {
return `Unexpected tool routed to handler: ${call.function.name}`;
}
let hash: string;
try {
const ta = JSON.parse(call.function.arguments) as unknown;
if (!isRecord(ta) || typeof ta.hash !== "string") {
return 'cas_get requires a JSON object with a string "hash" field.';
}
hash = ta.hash;
} catch {
return 'cas_get arguments were not valid JSON. Provide {"hash": "<cas-hash>"}.';
}
const blob = await thread.cas.get(hash);
return blob === null ? "null" : blob;
},
});
return async <T extends Record<string, unknown>>(
@@ -95,7 +95,6 @@ export function workflowAsAgent(
workflowName,
input,
{
maxRounds: ctx.start.meta.maxRounds,
depth: nextDepth,
signal: signalNever.signal,
awaitAfterEachYield: async () => {},
+888
View File
@@ -0,0 +1,888 @@
lockfileVersion: '9.0'
settings:
autoInstallPeers: true
excludeLinksFromLockfile: false
importers:
.:
dependencies:
hono:
specifier: ^4.7.11
version: 4.12.18
devDependencies:
'@cloudflare/workers-types':
specifier: ^4.20260425.1
version: 4.20260511.1
wrangler:
specifier: ^4.20.0
version: 4.90.0(@cloudflare/workers-types@4.20260511.1)
packages:
'@cloudflare/kv-asset-handler@0.5.0':
resolution: {integrity: sha512-jxQYkj8dSIzc0cD6cMMNdOc1UVjqSqu8BZdor5s8cGjW2I8BjODt/kWPVdY+u9zj3ms75Q5qaZgnxUad83+eAg==}
engines: {node: '>=22.0.0'}
'@cloudflare/unenv-preset@2.16.1':
resolution: {integrity: sha512-ECxObrMfyTl5bhQf/lZCXwo5G6xX9IAUo+nDMKK4SZ8m4Jvvxp52vilxyySSWh2YTZz8+HQ07qGH/2rEom1vDw==}
peerDependencies:
unenv: 2.0.0-rc.24
workerd: '>1.20260305.0 <2.0.0-0'
peerDependenciesMeta:
workerd:
optional: true
'@cloudflare/workerd-darwin-64@1.20260507.1':
resolution: {integrity: sha512-S85aMwcaPJUjKWDiG6iMMnioKWtPLACa6m0j/EhHR1GYfVpnxb974cBc6d25L+sf7jHWHJI2u5hGp0UTJ7MtXQ==}
engines: {node: '>=16'}
cpu: [x64]
os: [darwin]
'@cloudflare/workerd-darwin-arm64@1.20260507.1':
resolution: {integrity: sha512-GMEBu8Zp9Q97HLnf7bWJN4KjWpN5MxpeqdvHjBGWNl8UYprJI0k+Jkp89+Wh5S8vIon+HoVbDfOzPa7VwgL6Eg==}
engines: {node: '>=16'}
cpu: [arm64]
os: [darwin]
'@cloudflare/workerd-linux-64@1.20260507.1':
resolution: {integrity: sha512-QlrKEBdgA3uVc0Ok0Q3+0/CW0CTjgj5ySir1i1YY5FXVv0X6GpwtnB5umjunjF2MFprss+L+iFGZzxcSvMC1nA==}
engines: {node: '>=16'}
cpu: [x64]
os: [linux]
'@cloudflare/workerd-linux-arm64@1.20260507.1':
resolution: {integrity: sha512-eGbbupEtK2nh9V9Dhcx3vv3GTKeXqSVNgAEYVCCN0NGS9tl9HbMoHRX/4JL181FKXROMigWBCQVL//qPhsAzBQ==}
engines: {node: '>=16'}
cpu: [arm64]
os: [linux]
'@cloudflare/workerd-windows-64@1.20260507.1':
resolution: {integrity: sha512-dmClJ/E0BAcuDetQIZFqbeAXejWrG5pysGRMQ6T83Y0IW/7IAamY2zFEkAJ10I5xwZsdHuYsZtzlOxpEXpJs7A==}
engines: {node: '>=16'}
cpu: [x64]
os: [win32]
'@cloudflare/workers-types@4.20260511.1':
resolution: {integrity: sha512-FA+si7cOq9i/gtCHhIc0XJL0l1F/ApF+m00752Aj7WZFJrj3ZulT2T8/+rT3BabMT0QEnqFEGIqCgrmqhgEfMg==}
'@cspotcode/source-map-support@0.8.1':
resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==}
engines: {node: '>=12'}
'@emnapi/runtime@1.10.0':
resolution: {integrity: sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==}
'@esbuild/aix-ppc64@0.27.3':
resolution: {integrity: sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==}
engines: {node: '>=18'}
cpu: [ppc64]
os: [aix]
'@esbuild/android-arm64@0.27.3':
resolution: {integrity: sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==}
engines: {node: '>=18'}
cpu: [arm64]
os: [android]
'@esbuild/android-arm@0.27.3':
resolution: {integrity: sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==}
engines: {node: '>=18'}
cpu: [arm]
os: [android]
'@esbuild/android-x64@0.27.3':
resolution: {integrity: sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==}
engines: {node: '>=18'}
cpu: [x64]
os: [android]
'@esbuild/darwin-arm64@0.27.3':
resolution: {integrity: sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==}
engines: {node: '>=18'}
cpu: [arm64]
os: [darwin]
'@esbuild/darwin-x64@0.27.3':
resolution: {integrity: sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==}
engines: {node: '>=18'}
cpu: [x64]
os: [darwin]
'@esbuild/freebsd-arm64@0.27.3':
resolution: {integrity: sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==}
engines: {node: '>=18'}
cpu: [arm64]
os: [freebsd]
'@esbuild/freebsd-x64@0.27.3':
resolution: {integrity: sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==}
engines: {node: '>=18'}
cpu: [x64]
os: [freebsd]
'@esbuild/linux-arm64@0.27.3':
resolution: {integrity: sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==}
engines: {node: '>=18'}
cpu: [arm64]
os: [linux]
'@esbuild/linux-arm@0.27.3':
resolution: {integrity: sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==}
engines: {node: '>=18'}
cpu: [arm]
os: [linux]
'@esbuild/linux-ia32@0.27.3':
resolution: {integrity: sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==}
engines: {node: '>=18'}
cpu: [ia32]
os: [linux]
'@esbuild/linux-loong64@0.27.3':
resolution: {integrity: sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==}
engines: {node: '>=18'}
cpu: [loong64]
os: [linux]
'@esbuild/linux-mips64el@0.27.3':
resolution: {integrity: sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==}
engines: {node: '>=18'}
cpu: [mips64el]
os: [linux]
'@esbuild/linux-ppc64@0.27.3':
resolution: {integrity: sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==}
engines: {node: '>=18'}
cpu: [ppc64]
os: [linux]
'@esbuild/linux-riscv64@0.27.3':
resolution: {integrity: sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==}
engines: {node: '>=18'}
cpu: [riscv64]
os: [linux]
'@esbuild/linux-s390x@0.27.3':
resolution: {integrity: sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==}
engines: {node: '>=18'}
cpu: [s390x]
os: [linux]
'@esbuild/linux-x64@0.27.3':
resolution: {integrity: sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==}
engines: {node: '>=18'}
cpu: [x64]
os: [linux]
'@esbuild/netbsd-arm64@0.27.3':
resolution: {integrity: sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==}
engines: {node: '>=18'}
cpu: [arm64]
os: [netbsd]
'@esbuild/netbsd-x64@0.27.3':
resolution: {integrity: sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==}
engines: {node: '>=18'}
cpu: [x64]
os: [netbsd]
'@esbuild/openbsd-arm64@0.27.3':
resolution: {integrity: sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==}
engines: {node: '>=18'}
cpu: [arm64]
os: [openbsd]
'@esbuild/openbsd-x64@0.27.3':
resolution: {integrity: sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==}
engines: {node: '>=18'}
cpu: [x64]
os: [openbsd]
'@esbuild/openharmony-arm64@0.27.3':
resolution: {integrity: sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==}
engines: {node: '>=18'}
cpu: [arm64]
os: [openharmony]
'@esbuild/sunos-x64@0.27.3':
resolution: {integrity: sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==}
engines: {node: '>=18'}
cpu: [x64]
os: [sunos]
'@esbuild/win32-arm64@0.27.3':
resolution: {integrity: sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==}
engines: {node: '>=18'}
cpu: [arm64]
os: [win32]
'@esbuild/win32-ia32@0.27.3':
resolution: {integrity: sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==}
engines: {node: '>=18'}
cpu: [ia32]
os: [win32]
'@esbuild/win32-x64@0.27.3':
resolution: {integrity: sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==}
engines: {node: '>=18'}
cpu: [x64]
os: [win32]
'@img/colour@1.1.0':
resolution: {integrity: sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==}
engines: {node: '>=18'}
'@img/sharp-darwin-arm64@0.34.5':
resolution: {integrity: sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [arm64]
os: [darwin]
'@img/sharp-darwin-x64@0.34.5':
resolution: {integrity: sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [x64]
os: [darwin]
'@img/sharp-libvips-darwin-arm64@1.2.4':
resolution: {integrity: sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==}
cpu: [arm64]
os: [darwin]
'@img/sharp-libvips-darwin-x64@1.2.4':
resolution: {integrity: sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==}
cpu: [x64]
os: [darwin]
'@img/sharp-libvips-linux-arm64@1.2.4':
resolution: {integrity: sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@img/sharp-libvips-linux-arm@1.2.4':
resolution: {integrity: sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==}
cpu: [arm]
os: [linux]
libc: [glibc]
'@img/sharp-libvips-linux-ppc64@1.2.4':
resolution: {integrity: sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==}
cpu: [ppc64]
os: [linux]
libc: [glibc]
'@img/sharp-libvips-linux-riscv64@1.2.4':
resolution: {integrity: sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==}
cpu: [riscv64]
os: [linux]
libc: [glibc]
'@img/sharp-libvips-linux-s390x@1.2.4':
resolution: {integrity: sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==}
cpu: [s390x]
os: [linux]
libc: [glibc]
'@img/sharp-libvips-linux-x64@1.2.4':
resolution: {integrity: sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==}
cpu: [x64]
os: [linux]
libc: [glibc]
'@img/sharp-libvips-linuxmusl-arm64@1.2.4':
resolution: {integrity: sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==}
cpu: [arm64]
os: [linux]
libc: [musl]
'@img/sharp-libvips-linuxmusl-x64@1.2.4':
resolution: {integrity: sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==}
cpu: [x64]
os: [linux]
libc: [musl]
'@img/sharp-linux-arm64@0.34.5':
resolution: {integrity: sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@img/sharp-linux-arm@0.34.5':
resolution: {integrity: sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [arm]
os: [linux]
libc: [glibc]
'@img/sharp-linux-ppc64@0.34.5':
resolution: {integrity: sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [ppc64]
os: [linux]
libc: [glibc]
'@img/sharp-linux-riscv64@0.34.5':
resolution: {integrity: sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [riscv64]
os: [linux]
libc: [glibc]
'@img/sharp-linux-s390x@0.34.5':
resolution: {integrity: sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [s390x]
os: [linux]
libc: [glibc]
'@img/sharp-linux-x64@0.34.5':
resolution: {integrity: sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [x64]
os: [linux]
libc: [glibc]
'@img/sharp-linuxmusl-arm64@0.34.5':
resolution: {integrity: sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [arm64]
os: [linux]
libc: [musl]
'@img/sharp-linuxmusl-x64@0.34.5':
resolution: {integrity: sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [x64]
os: [linux]
libc: [musl]
'@img/sharp-wasm32@0.34.5':
resolution: {integrity: sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [wasm32]
'@img/sharp-win32-arm64@0.34.5':
resolution: {integrity: sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [arm64]
os: [win32]
'@img/sharp-win32-ia32@0.34.5':
resolution: {integrity: sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [ia32]
os: [win32]
'@img/sharp-win32-x64@0.34.5':
resolution: {integrity: sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [x64]
os: [win32]
'@jridgewell/resolve-uri@3.1.2':
resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==}
engines: {node: '>=6.0.0'}
'@jridgewell/sourcemap-codec@1.5.5':
resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==}
'@jridgewell/trace-mapping@0.3.9':
resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==}
'@poppinss/colors@4.1.6':
resolution: {integrity: sha512-H9xkIdFswbS8n1d6vmRd8+c10t2Qe+rZITbbDHHkQixH5+2x1FDGmi/0K+WgWiqQFKPSlIYB7jlH6Kpfn6Fleg==}
'@poppinss/dumper@0.6.5':
resolution: {integrity: sha512-NBdYIb90J7LfOI32dOewKI1r7wnkiH6m920puQ3qHUeZkxNkQiFnXVWoE6YtFSv6QOiPPf7ys6i+HWWecDz7sw==}
'@poppinss/exception@1.2.3':
resolution: {integrity: sha512-dCED+QRChTVatE9ibtoaxc+WkdzOSjYTKi/+uacHWIsfodVfpsueo3+DKpgU5Px8qXjgmXkSvhXvSCz3fnP9lw==}
'@sindresorhus/is@7.2.0':
resolution: {integrity: sha512-P1Cz1dWaFfR4IR+U13mqqiGsLFf1KbayybWwdd2vfctdV6hDpUkgCY0nKOLLTMSoRd/jJNjtbqzf13K8DCCXQw==}
engines: {node: '>=18'}
'@speed-highlight/core@1.2.15':
resolution: {integrity: sha512-BMq1K3DsElxDWawkX6eLg9+CKJrTVGCBAWVuHXVUV2u0s2711qiChLSId6ikYPfxhdYocLNt3wWwSvDiTvFabw==}
blake3-wasm@2.1.5:
resolution: {integrity: sha512-F1+K8EbfOZE49dtoPtmxUQrpXaBIl3ICvasLh+nJta0xkz+9kF/7uet9fLnwKqhDrmj6g+6K3Tw9yQPUg2ka5g==}
cookie@1.1.1:
resolution: {integrity: sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==}
engines: {node: '>=18'}
detect-libc@2.1.2:
resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==}
engines: {node: '>=8'}
error-stack-parser-es@1.0.5:
resolution: {integrity: sha512-5qucVt2XcuGMcEGgWI7i+yZpmpByQ8J1lHhcL7PwqCwu9FPP3VUXzT4ltHe5i2z9dePwEHcDVOAfSnHsOlCXRA==}
esbuild@0.27.3:
resolution: {integrity: sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==}
engines: {node: '>=18'}
hasBin: true
fsevents@2.3.3:
resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
os: [darwin]
hono@4.12.18:
resolution: {integrity: sha512-RWzP96k/yv0PQfyXnWjs6zot20TqfpfsNXhOnev8d1InAxubW93L11/oNUc3tQqn2G0bSdAOBpX+2uDFHV7kdQ==}
engines: {node: '>=16.9.0'}
kleur@4.1.5:
resolution: {integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==}
engines: {node: '>=6'}
miniflare@4.20260507.1:
resolution: {integrity: sha512-PSXBiLExTdZ4UGO/raKCHQauUpYL7F880ZRB7j0+78Rv8h7TsdN2E/iEDK9sK2Y+SPQ5wJSeAa+rDeVKoZZoEw==}
engines: {node: '>=22.0.0'}
hasBin: true
path-to-regexp@6.3.0:
resolution: {integrity: sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==}
pathe@2.0.3:
resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==}
semver@7.8.0:
resolution: {integrity: sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA==}
engines: {node: '>=10'}
hasBin: true
sharp@0.34.5:
resolution: {integrity: sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
supports-color@10.2.2:
resolution: {integrity: sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==}
engines: {node: '>=18'}
tslib@2.8.1:
resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
undici@7.24.8:
resolution: {integrity: sha512-6KQ/+QxK49Z/p3HO6E5ZCZWNnCasyZLa5ExaVYyvPxUwKtbCPMKELJOqh7EqOle0t9cH/7d2TaaTRRa6Nhs4YQ==}
engines: {node: '>=20.18.1'}
unenv@2.0.0-rc.24:
resolution: {integrity: sha512-i7qRCmY42zmCwnYlh9H2SvLEypEFGye5iRmEMKjcGi7zk9UquigRjFtTLz0TYqr0ZGLZhaMHl/foy1bZR+Cwlw==}
workerd@1.20260507.1:
resolution: {integrity: sha512-z7JhsFSe6+X1b5fUHaVpo15VM1IRMJiLofEkq8iKdCo+Veqc+FUg5lIsuz8NwePxuSKrXtO4ZQpGkQLbPVXFhg==}
engines: {node: '>=16'}
hasBin: true
wrangler@4.90.0:
resolution: {integrity: sha512-bmNIykl59TfCUn5xQgU7IWylSsPx3LQaPLMSAq2VQHt89CBrcj9qXQ0eYfjBCWA5XTBVgten391evt7xxtXwcA==}
engines: {node: '>=22.0.0'}
hasBin: true
peerDependencies:
'@cloudflare/workers-types': ^4.20260507.1
peerDependenciesMeta:
'@cloudflare/workers-types':
optional: true
ws@8.18.0:
resolution: {integrity: sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==}
engines: {node: '>=10.0.0'}
peerDependencies:
bufferutil: ^4.0.1
utf-8-validate: '>=5.0.2'
peerDependenciesMeta:
bufferutil:
optional: true
utf-8-validate:
optional: true
youch-core@0.3.3:
resolution: {integrity: sha512-ho7XuGjLaJ2hWHoK8yFnsUGy2Y5uDpqSTq1FkHLK4/oqKtyUU1AFbOOxY4IpC9f0fTLjwYbslUz0Po5BpD1wrA==}
youch@4.1.0-beta.10:
resolution: {integrity: sha512-rLfVLB4FgQneDr0dv1oddCVZmKjcJ6yX6mS4pU82Mq/Dt9a3cLZQ62pDBL4AUO+uVrCvtWz3ZFUL2HFAFJ/BXQ==}
snapshots:
'@cloudflare/kv-asset-handler@0.5.0': {}
'@cloudflare/unenv-preset@2.16.1(unenv@2.0.0-rc.24)(workerd@1.20260507.1)':
dependencies:
unenv: 2.0.0-rc.24
optionalDependencies:
workerd: 1.20260507.1
'@cloudflare/workerd-darwin-64@1.20260507.1':
optional: true
'@cloudflare/workerd-darwin-arm64@1.20260507.1':
optional: true
'@cloudflare/workerd-linux-64@1.20260507.1':
optional: true
'@cloudflare/workerd-linux-arm64@1.20260507.1':
optional: true
'@cloudflare/workerd-windows-64@1.20260507.1':
optional: true
'@cloudflare/workers-types@4.20260511.1': {}
'@cspotcode/source-map-support@0.8.1':
dependencies:
'@jridgewell/trace-mapping': 0.3.9
'@emnapi/runtime@1.10.0':
dependencies:
tslib: 2.8.1
optional: true
'@esbuild/aix-ppc64@0.27.3':
optional: true
'@esbuild/android-arm64@0.27.3':
optional: true
'@esbuild/android-arm@0.27.3':
optional: true
'@esbuild/android-x64@0.27.3':
optional: true
'@esbuild/darwin-arm64@0.27.3':
optional: true
'@esbuild/darwin-x64@0.27.3':
optional: true
'@esbuild/freebsd-arm64@0.27.3':
optional: true
'@esbuild/freebsd-x64@0.27.3':
optional: true
'@esbuild/linux-arm64@0.27.3':
optional: true
'@esbuild/linux-arm@0.27.3':
optional: true
'@esbuild/linux-ia32@0.27.3':
optional: true
'@esbuild/linux-loong64@0.27.3':
optional: true
'@esbuild/linux-mips64el@0.27.3':
optional: true
'@esbuild/linux-ppc64@0.27.3':
optional: true
'@esbuild/linux-riscv64@0.27.3':
optional: true
'@esbuild/linux-s390x@0.27.3':
optional: true
'@esbuild/linux-x64@0.27.3':
optional: true
'@esbuild/netbsd-arm64@0.27.3':
optional: true
'@esbuild/netbsd-x64@0.27.3':
optional: true
'@esbuild/openbsd-arm64@0.27.3':
optional: true
'@esbuild/openbsd-x64@0.27.3':
optional: true
'@esbuild/openharmony-arm64@0.27.3':
optional: true
'@esbuild/sunos-x64@0.27.3':
optional: true
'@esbuild/win32-arm64@0.27.3':
optional: true
'@esbuild/win32-ia32@0.27.3':
optional: true
'@esbuild/win32-x64@0.27.3':
optional: true
'@img/colour@1.1.0': {}
'@img/sharp-darwin-arm64@0.34.5':
optionalDependencies:
'@img/sharp-libvips-darwin-arm64': 1.2.4
optional: true
'@img/sharp-darwin-x64@0.34.5':
optionalDependencies:
'@img/sharp-libvips-darwin-x64': 1.2.4
optional: true
'@img/sharp-libvips-darwin-arm64@1.2.4':
optional: true
'@img/sharp-libvips-darwin-x64@1.2.4':
optional: true
'@img/sharp-libvips-linux-arm64@1.2.4':
optional: true
'@img/sharp-libvips-linux-arm@1.2.4':
optional: true
'@img/sharp-libvips-linux-ppc64@1.2.4':
optional: true
'@img/sharp-libvips-linux-riscv64@1.2.4':
optional: true
'@img/sharp-libvips-linux-s390x@1.2.4':
optional: true
'@img/sharp-libvips-linux-x64@1.2.4':
optional: true
'@img/sharp-libvips-linuxmusl-arm64@1.2.4':
optional: true
'@img/sharp-libvips-linuxmusl-x64@1.2.4':
optional: true
'@img/sharp-linux-arm64@0.34.5':
optionalDependencies:
'@img/sharp-libvips-linux-arm64': 1.2.4
optional: true
'@img/sharp-linux-arm@0.34.5':
optionalDependencies:
'@img/sharp-libvips-linux-arm': 1.2.4
optional: true
'@img/sharp-linux-ppc64@0.34.5':
optionalDependencies:
'@img/sharp-libvips-linux-ppc64': 1.2.4
optional: true
'@img/sharp-linux-riscv64@0.34.5':
optionalDependencies:
'@img/sharp-libvips-linux-riscv64': 1.2.4
optional: true
'@img/sharp-linux-s390x@0.34.5':
optionalDependencies:
'@img/sharp-libvips-linux-s390x': 1.2.4
optional: true
'@img/sharp-linux-x64@0.34.5':
optionalDependencies:
'@img/sharp-libvips-linux-x64': 1.2.4
optional: true
'@img/sharp-linuxmusl-arm64@0.34.5':
optionalDependencies:
'@img/sharp-libvips-linuxmusl-arm64': 1.2.4
optional: true
'@img/sharp-linuxmusl-x64@0.34.5':
optionalDependencies:
'@img/sharp-libvips-linuxmusl-x64': 1.2.4
optional: true
'@img/sharp-wasm32@0.34.5':
dependencies:
'@emnapi/runtime': 1.10.0
optional: true
'@img/sharp-win32-arm64@0.34.5':
optional: true
'@img/sharp-win32-ia32@0.34.5':
optional: true
'@img/sharp-win32-x64@0.34.5':
optional: true
'@jridgewell/resolve-uri@3.1.2': {}
'@jridgewell/sourcemap-codec@1.5.5': {}
'@jridgewell/trace-mapping@0.3.9':
dependencies:
'@jridgewell/resolve-uri': 3.1.2
'@jridgewell/sourcemap-codec': 1.5.5
'@poppinss/colors@4.1.6':
dependencies:
kleur: 4.1.5
'@poppinss/dumper@0.6.5':
dependencies:
'@poppinss/colors': 4.1.6
'@sindresorhus/is': 7.2.0
supports-color: 10.2.2
'@poppinss/exception@1.2.3': {}
'@sindresorhus/is@7.2.0': {}
'@speed-highlight/core@1.2.15': {}
blake3-wasm@2.1.5: {}
cookie@1.1.1: {}
detect-libc@2.1.2: {}
error-stack-parser-es@1.0.5: {}
esbuild@0.27.3:
optionalDependencies:
'@esbuild/aix-ppc64': 0.27.3
'@esbuild/android-arm': 0.27.3
'@esbuild/android-arm64': 0.27.3
'@esbuild/android-x64': 0.27.3
'@esbuild/darwin-arm64': 0.27.3
'@esbuild/darwin-x64': 0.27.3
'@esbuild/freebsd-arm64': 0.27.3
'@esbuild/freebsd-x64': 0.27.3
'@esbuild/linux-arm': 0.27.3
'@esbuild/linux-arm64': 0.27.3
'@esbuild/linux-ia32': 0.27.3
'@esbuild/linux-loong64': 0.27.3
'@esbuild/linux-mips64el': 0.27.3
'@esbuild/linux-ppc64': 0.27.3
'@esbuild/linux-riscv64': 0.27.3
'@esbuild/linux-s390x': 0.27.3
'@esbuild/linux-x64': 0.27.3
'@esbuild/netbsd-arm64': 0.27.3
'@esbuild/netbsd-x64': 0.27.3
'@esbuild/openbsd-arm64': 0.27.3
'@esbuild/openbsd-x64': 0.27.3
'@esbuild/openharmony-arm64': 0.27.3
'@esbuild/sunos-x64': 0.27.3
'@esbuild/win32-arm64': 0.27.3
'@esbuild/win32-ia32': 0.27.3
'@esbuild/win32-x64': 0.27.3
fsevents@2.3.3:
optional: true
hono@4.12.18: {}
kleur@4.1.5: {}
miniflare@4.20260507.1:
dependencies:
'@cspotcode/source-map-support': 0.8.1
sharp: 0.34.5
undici: 7.24.8
workerd: 1.20260507.1
ws: 8.18.0
youch: 4.1.0-beta.10
transitivePeerDependencies:
- bufferutil
- utf-8-validate
path-to-regexp@6.3.0: {}
pathe@2.0.3: {}
semver@7.8.0: {}
sharp@0.34.5:
dependencies:
'@img/colour': 1.1.0
detect-libc: 2.1.2
semver: 7.8.0
optionalDependencies:
'@img/sharp-darwin-arm64': 0.34.5
'@img/sharp-darwin-x64': 0.34.5
'@img/sharp-libvips-darwin-arm64': 1.2.4
'@img/sharp-libvips-darwin-x64': 1.2.4
'@img/sharp-libvips-linux-arm': 1.2.4
'@img/sharp-libvips-linux-arm64': 1.2.4
'@img/sharp-libvips-linux-ppc64': 1.2.4
'@img/sharp-libvips-linux-riscv64': 1.2.4
'@img/sharp-libvips-linux-s390x': 1.2.4
'@img/sharp-libvips-linux-x64': 1.2.4
'@img/sharp-libvips-linuxmusl-arm64': 1.2.4
'@img/sharp-libvips-linuxmusl-x64': 1.2.4
'@img/sharp-linux-arm': 0.34.5
'@img/sharp-linux-arm64': 0.34.5
'@img/sharp-linux-ppc64': 0.34.5
'@img/sharp-linux-riscv64': 0.34.5
'@img/sharp-linux-s390x': 0.34.5
'@img/sharp-linux-x64': 0.34.5
'@img/sharp-linuxmusl-arm64': 0.34.5
'@img/sharp-linuxmusl-x64': 0.34.5
'@img/sharp-wasm32': 0.34.5
'@img/sharp-win32-arm64': 0.34.5
'@img/sharp-win32-ia32': 0.34.5
'@img/sharp-win32-x64': 0.34.5
supports-color@10.2.2: {}
tslib@2.8.1:
optional: true
undici@7.24.8: {}
unenv@2.0.0-rc.24:
dependencies:
pathe: 2.0.3
workerd@1.20260507.1:
optionalDependencies:
'@cloudflare/workerd-darwin-64': 1.20260507.1
'@cloudflare/workerd-darwin-arm64': 1.20260507.1
'@cloudflare/workerd-linux-64': 1.20260507.1
'@cloudflare/workerd-linux-arm64': 1.20260507.1
'@cloudflare/workerd-windows-64': 1.20260507.1
wrangler@4.90.0(@cloudflare/workers-types@4.20260511.1):
dependencies:
'@cloudflare/kv-asset-handler': 0.5.0
'@cloudflare/unenv-preset': 2.16.1(unenv@2.0.0-rc.24)(workerd@1.20260507.1)
blake3-wasm: 2.1.5
esbuild: 0.27.3
miniflare: 4.20260507.1
path-to-regexp: 6.3.0
unenv: 2.0.0-rc.24
workerd: 1.20260507.1
optionalDependencies:
'@cloudflare/workers-types': 4.20260511.1
fsevents: 2.3.3
transitivePeerDependencies:
- bufferutil
- utf-8-validate
ws@8.18.0: {}
youch-core@0.3.3:
dependencies:
'@poppinss/exception': 1.2.3
error-stack-parser-es: 1.0.5
youch@4.1.0-beta.10:
dependencies:
'@poppinss/colors': 4.1.6
'@poppinss/dumper': 0.6.5
'@speed-highlight/core': 1.2.15
cookie: 1.1.1
youch-core: 0.3.3
+15 -19
View File
@@ -23,16 +23,6 @@ const app = new Hono<Env>();
app.use("*", cors());
// ── Dashboard API key auth (skip healthz + register) ─────────────
app.use("/endpoints", async (c, next) => {
if (!checkDashboardAuth(c)) return c.json({ error: "unauthorized" }, 401);
await next();
});
app.use("/api/*", async (c, next) => {
if (!checkDashboardAuth(c)) return c.json({ error: "unauthorized" }, 401);
await next();
});
function checkDashboardAuth(c: {
req: { header: (n: string) => string | undefined; query: (n: string) => string | undefined };
env: Env["Bindings"];
@@ -46,8 +36,10 @@ function checkDashboardAuth(c: {
// ── Health ──────────────────────────────────────────────────────────
app.get("/healthz", (c) => c.json({ ok: true }));
// ── Register / heartbeat ────────────────────────────────────────────
app.post("/register", async (c) => {
// ── Gateway management (GATEWAY_SECRET auth) ────────────────────────
const gateway = new Hono<Env>();
gateway.post("/register", async (c) => {
const body = await c.req.json<{
name?: string;
url?: string;
@@ -82,8 +74,7 @@ app.post("/register", async (c) => {
return c.json({ registered: name }, status);
});
// ── Unregister ──────────────────────────────────────────────────────
app.delete("/register/:name", async (c) => {
gateway.delete("/register/:name", async (c) => {
const auth = c.req.header("Authorization");
if (auth !== `Bearer ${c.env.GATEWAY_SECRET}`) {
return c.json({ error: "unauthorized" }, 401);
@@ -94,8 +85,10 @@ app.delete("/register/:name", async (c) => {
return c.json({ unregistered: name });
});
// ── List endpoints ──────────────────────────────────────────────────
app.get("/endpoints", async (c) => {
// endpoints requires dashboard auth
gateway.get("/endpoints", async (c) => {
if (!checkDashboardAuth(c)) return c.json({ error: "unauthorized" }, 401);
const list = await c.env.ENDPOINTS.list();
const endpoints: Array<{ name: string; url: string; status: string; lastHeartbeat: number }> = [];
@@ -115,8 +108,11 @@ app.get("/endpoints", async (c) => {
return c.json(endpoints);
});
// ── API proxy: /api/:agent/* → agent's tunnel URL ───────────────────
app.all("/api/:agent/*", async (c) => {
app.route("/api/gateway", gateway);
// ── API proxy: /api/agents/:agent/* → agent's tunnel URL (dashboard auth) ──
app.all("/api/agents/:agent/*", async (c) => {
if (!checkDashboardAuth(c)) return c.json({ error: "unauthorized" }, 401);
const agent = c.req.param("agent");
const record = await c.env.ENDPOINTS.get<EndpointRecord>(agent, "json");
@@ -126,7 +122,7 @@ app.all("/api/:agent/*", async (c) => {
// Build target URL: strip /api/:agent prefix, forward the rest
const url = new URL(c.req.url);
const pathAfterAgent = url.pathname.replace(`/api/${agent}`, "");
const pathAfterAgent = url.pathname.replace(`/api/agents/${agent}`, "");
const targetUrl = `${record.url}/api${pathAfterAgent}${url.search}`;
const headers = new Headers(c.req.raw.headers);
@@ -24,7 +24,7 @@ function makeCtx(roles: (keyof TestMeta & string)[]): ModeratorContext<TestMeta>
start: {
role: START,
content: "test",
meta: { maxRounds: 10 },
meta: {},
timestamp: Date.now(),
} as StartStep,
steps,
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@uncaged/workflow-protocol",
"version": "0.1.0",
"version": "0.3.1",
"type": "module",
"exports": {
".": {
+32
View File
@@ -0,0 +1,32 @@
lockfileVersion: '9.0'
settings:
autoInstallPeers: true
excludeLinksFromLockfile: false
importers:
.:
devDependencies:
typescript:
specifier: ^5.8.3
version: 5.9.3
zod:
specifier: ^4.0.0
version: 4.4.3
packages:
typescript@5.9.3:
resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==}
engines: {node: '>=14.17'}
hasBin: true
zod@4.4.3:
resolution: {integrity: sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==}
snapshots:
typescript@5.9.3: {}
zod@4.4.3: {}
@@ -3,7 +3,6 @@
export type StartNodePayload = {
name: string;
hash: string;
maxRounds: number;
depth: number;
};
+1 -1
View File
@@ -46,7 +46,7 @@ export type RoleOutput = {
export type StartStep = {
role: typeof START;
content: string;
meta: { maxRounds: number };
meta: Record<string, never>;
timestamp: number;
};
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@uncaged/workflow-reactor",
"version": "0.1.0",
"version": "0.3.1",
"type": "module",
"exports": {
".": {
+36
View File
@@ -0,0 +1,36 @@
lockfileVersion: '9.0'
settings:
autoInstallPeers: true
excludeLinksFromLockfile: false
importers:
.:
dependencies:
'@uncaged/workflow-protocol':
specifier: workspace:*
version: link:../workflow-protocol
devDependencies:
typescript:
specifier: ^5.8.3
version: 5.9.3
zod:
specifier: ^4.0.0
version: 4.4.3
packages:
typescript@5.9.3:
resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==}
engines: {node: '>=14.17'}
hasBin: true
zod@4.4.3:
resolution: {integrity: sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==}
snapshots:
typescript@5.9.3: {}
zod@4.4.3: {}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@uncaged/workflow-register",
"version": "0.1.0",
"version": "0.3.1",
"type": "module",
"exports": {
".": {
+59
View File
@@ -0,0 +1,59 @@
lockfileVersion: '9.0'
settings:
autoInstallPeers: true
excludeLinksFromLockfile: false
importers:
.:
dependencies:
'@uncaged/workflow-protocol':
specifier: workspace:*
version: link:../workflow-protocol
'@uncaged/workflow-util':
specifier: workspace:*
version: link:../workflow-util
devDependencies:
acorn:
specifier: ^8.14.1
version: 8.16.0
typescript:
specifier: ^5.8.3
version: 5.9.3
yaml:
specifier: ^2.7.1
version: 2.8.4
zod:
specifier: ^4.0.0
version: 4.4.3
packages:
acorn@8.16.0:
resolution: {integrity: sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==}
engines: {node: '>=0.4.0'}
hasBin: true
typescript@5.9.3:
resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==}
engines: {node: '>=14.17'}
hasBin: true
yaml@2.8.4:
resolution: {integrity: sha512-ml/JPOj9fOQK8RNnWojA67GbZ0ApXAUlN2UQclwv2eVgTgn7O9gg9o7paZWKMp4g0H3nTLtS9LVzhkpOFIKzog==}
engines: {node: '>= 14.6'}
hasBin: true
zod@4.4.3:
resolution: {integrity: sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==}
snapshots:
acorn@8.16.0: {}
typescript@5.9.3: {}
yaml@2.8.4: {}
zod@4.4.3: {}
@@ -27,7 +27,7 @@ describe("buildThreadContext", () => {
const bundleHash = "BHAAAAAAAAAAA";
const startHash = await putStartNode(
cas,
{ name: "demo", hash: bundleHash, maxRounds: 99, depth: 2 },
{ name: "demo", hash: bundleHash, depth: 2 },
promptHash,
);
@@ -59,7 +59,6 @@ describe("buildThreadContext", () => {
expect(ctx.depth).toBe(2);
expect(ctx.start.role).toBe(START);
expect(ctx.start.content).toBe("hello-task");
expect(ctx.start.meta.maxRounds).toBe(99);
expect(ctx.steps.map((s) => s.role)).toEqual(["planner", "coder"]);
expect(ctx.steps[0]?.refs).toEqual([art]);
expect(ctx.steps[1]?.refs).toEqual([]);
@@ -72,7 +71,7 @@ describe("buildThreadContext", () => {
const promptHash = await cas.put("only-prompt");
const startHash = await putStartNode(
cas,
{ name: "solo", hash: "BHBBBBBBBBBBB", maxRounds: 3, depth: 1 },
{ name: "solo", hash: "BHBBBBBBBBBBB", depth: 1 },
promptHash,
);
@@ -80,7 +79,6 @@ describe("buildThreadContext", () => {
expect(ctx.steps).toEqual([]);
expect(ctx.start.content).toBe("only-prompt");
expect(ctx.depth).toBe(1);
expect(ctx.start.meta.maxRounds).toBe(3);
});
test("omits __end__ states from steps", async () => {
@@ -89,7 +87,7 @@ describe("buildThreadContext", () => {
const bundleHash = "BHCCCCCCCCCCC";
const startHash = await putStartNode(
cas,
{ name: "demo", hash: bundleHash, maxRounds: 10, depth: 0 },
{ name: "demo", hash: bundleHash, depth: 0 },
promptHash,
);
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@uncaged/workflow-runtime",
"version": "0.2.0",
"version": "0.3.1",
"type": "module",
"main": "src/index.ts",
"types": "src/index.ts",
+29
View File
@@ -0,0 +1,29 @@
lockfileVersion: '9.0'
settings:
autoInstallPeers: true
excludeLinksFromLockfile: false
importers:
.:
dependencies:
'@uncaged/workflow-cas':
specifier: workspace:*
version: link:../workflow-cas
'@uncaged/workflow-protocol':
specifier: workspace:*
version: link:../workflow-protocol
devDependencies:
zod:
specifier: ^4.0.0
version: 4.4.3
packages:
zod@4.4.3:
resolution: {integrity: sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==}
snapshots:
zod@4.4.3: {}
@@ -57,7 +57,7 @@ async function threadFromStartHead<M extends RoleMeta>(
start: {
role: START,
content: prompt,
meta: { maxRounds: p.maxRounds },
meta: {},
timestamp: 0,
},
steps: [],
@@ -116,7 +116,7 @@ async function threadFromStateHead<M extends RoleMeta>(
start: {
role: START,
content: prompt,
meta: { maxRounds: sp.maxRounds },
meta: {},
timestamp: firstTs,
},
steps,
@@ -101,9 +101,10 @@ async function advanceOneRound<M extends RoleMeta>(
);
const artifactRefs = mergeUniqueHashes(extracted.refs, refsFromMeta);
const contentHash = artifactRefs.length === 0
? agentContentHash
: await putContentNodeWithRefs(runtime.cas, extracted.contentPayload, artifactRefs);
const contentHash =
artifactRefs.length === 0
? agentContentHash
: await putContentNodeWithRefs(runtime.cas, extracted.contentPayload, artifactRefs);
const refs = artifactRefs.includes(contentHash) ? artifactRefs : [...artifactRefs, contentHash];
const step = {
@@ -144,17 +145,9 @@ export function createWorkflow<M extends RoleMeta>(
if (thread.start.role !== START) {
throw new Error(`workflow loop expected start role to be ${START}`);
}
const maxRounds = thread.start.meta.maxRounds;
let currentThread = thread as ModeratorContext<M>;
while (true) {
if (currentThread.steps.length >= maxRounds) {
return {
returnCode: 0,
summary: `completed: reached maxRounds (${maxRounds})`,
};
}
const outcome = await advanceOneRound(def, binding, {
thread: currentThread,
runtime,
@@ -13,23 +13,20 @@ const DEFAULT_PHASES: PlannerMeta["phases"] = [
},
];
function makeStart(maxRounds: number): ModeratorContext<DevelopMeta>["start"] {
function makeStart(): ModeratorContext<DevelopMeta>["start"] {
return {
role: START,
content: "Implement the feature",
meta: { maxRounds },
meta: {},
timestamp: 0,
};
}
function makeCtx(
maxRounds: number,
steps: ModeratorContext<DevelopMeta>["steps"],
): ModeratorContext<DevelopMeta> {
function makeCtx(steps: ModeratorContext<DevelopMeta>["steps"]): ModeratorContext<DevelopMeta> {
return {
threadId: "01TEST000000000000000000TR",
depth: 0,
start: makeStart(maxRounds),
start: makeStart(),
steps,
};
}
@@ -90,20 +87,18 @@ function committerStep(meta: CommitterMeta): RoleStep<DevelopMeta> {
describe("developModerator", () => {
test("routes initial → planner → coder → reviewer → tester → committer → END", () => {
expect(developModerator(makeCtx(20, []))).toBe("planner");
expect(developModerator(makeCtx(20, [plannerStep()]))).toBe("coder");
expect(developModerator(makeCtx(20, [plannerStep(), coderStep()]))).toBe("reviewer");
expect(developModerator(makeCtx(20, [plannerStep(), coderStep(), reviewerStep(true)]))).toBe(
expect(developModerator(makeCtx([]))).toBe("planner");
expect(developModerator(makeCtx([plannerStep()]))).toBe("coder");
expect(developModerator(makeCtx([plannerStep(), coderStep()]))).toBe("reviewer");
expect(developModerator(makeCtx([plannerStep(), coderStep(), reviewerStep(true)]))).toBe(
"tester",
);
expect(
developModerator(
makeCtx(20, [plannerStep(), coderStep(), reviewerStep(true), testerStep(true)]),
),
developModerator(makeCtx([plannerStep(), coderStep(), reviewerStep(true), testerStep(true)])),
).toBe("committer");
expect(
developModerator(
makeCtx(20, [
makeCtx([
plannerStep(),
coderStep(),
reviewerStep(true),
@@ -120,16 +115,16 @@ describe("developModerator", () => {
coderStep(),
reviewerStep(false),
];
expect(developModerator(makeCtx(20, steps))).toBe("coder");
expect(developModerator(makeCtx(steps))).toBe("coder");
});
test("reviewer rejects → END when max rounds exhausted", () => {
test("reviewer rejects → coder retry (supervisor controls termination)", () => {
const steps: ModeratorContext<DevelopMeta>["steps"] = [
plannerStep(),
coderStep(),
reviewerStep(false),
];
expect(developModerator(makeCtx(4, steps))).toBe(END);
expect(developModerator(makeCtx(steps))).toBe("coder");
});
test("tester failed → coder retry when budget allows", () => {
@@ -139,17 +134,17 @@ describe("developModerator", () => {
reviewerStep(true),
testerStep(false),
];
expect(developModerator(makeCtx(20, steps))).toBe("coder");
expect(developModerator(makeCtx(steps))).toBe("coder");
});
test("tester failed → END when max rounds exhausted", () => {
test("tester failed → coder retry (supervisor controls termination)", () => {
const steps: ModeratorContext<DevelopMeta>["steps"] = [
plannerStep(),
coderStep(),
reviewerStep(true),
testerStep(false),
];
expect(developModerator(makeCtx(5, steps))).toBe(END);
expect(developModerator(makeCtx(steps))).toBe("coder");
});
test("multiple planner phases → coder until all complete, then reviewer", () => {
@@ -157,13 +152,11 @@ describe("developModerator", () => {
{ hash: "AA000001", title: "first phase" },
{ hash: "AA000002", title: "second phase" },
];
expect(developModerator(makeCtx(20, [plannerStep(phases)]))).toBe("coder");
expect(developModerator(makeCtx(20, [plannerStep(phases), coderStep("AA000001")]))).toBe(
"coder",
);
expect(developModerator(makeCtx([plannerStep(phases)]))).toBe("coder");
expect(developModerator(makeCtx([plannerStep(phases), coderStep("AA000001")]))).toBe("coder");
expect(
developModerator(
makeCtx(20, [plannerStep(phases), coderStep("AA000001"), coderStep("AA000002")]),
makeCtx([plannerStep(phases), coderStep("AA000001"), coderStep("AA000002")]),
),
).toBe("reviewer");
});
@@ -175,7 +168,7 @@ describe("developModerator", () => {
{ hash: "BB000003", title: "verify" },
{ hash: "BB000004", title: "polish" },
];
expect(developModerator(makeCtx(20, [plannerStep(phases), coderStep("BB000004")]))).toBe(
expect(developModerator(makeCtx([plannerStep(phases), coderStep("BB000004")]))).toBe(
"reviewer",
);
});
@@ -185,12 +178,10 @@ describe("developModerator", () => {
{ hash: "CC000001", title: "first phase" },
{ hash: "CC000002", title: "second phase" },
];
expect(developModerator(makeCtx(20, [plannerStep(phases), coderStep("all-done")]))).toBe(
"coder",
);
expect(developModerator(makeCtx([plannerStep(phases), coderStep("all-done")]))).toBe("coder");
});
test("incomplete phases → END when max rounds exhausted", () => {
test("incomplete phases → coder retry (supervisor controls termination)", () => {
const phases: PlannerMeta["phases"] = [
{ hash: "DD000001", title: "first phase" },
{ hash: "DD000002", title: "second phase" },
@@ -199,7 +190,7 @@ describe("developModerator", () => {
plannerStep(phases),
coderStep("DD000001"),
];
expect(developModerator(makeCtx(3, steps))).toBe(END);
expect(developModerator(makeCtx(steps))).toBe("coder");
});
test("committer → END for any committer meta status", () => {
@@ -220,9 +211,9 @@ describe("developModerator", () => {
reviewerStep(true),
testerStep(true),
];
expect(developModerator(makeCtx(20, [...base, committed]))).toBe(END);
expect(developModerator(makeCtx(20, [...base, recoverable]))).toBe(END);
expect(developModerator(makeCtx(20, [...base, unrecoverable]))).toBe(END);
expect(developModerator(makeCtx([...base, committed]))).toBe(END);
expect(developModerator(makeCtx([...base, recoverable]))).toBe(END);
expect(developModerator(makeCtx([...base, unrecoverable]))).toBe(END);
});
});
@@ -1,6 +1,6 @@
{
"name": "@uncaged/workflow-template-develop",
"version": "0.2.0",
"version": "0.3.1",
"type": "module",
"exports": {
".": {
+28
View File
@@ -0,0 +1,28 @@
lockfileVersion: '9.0'
settings:
autoInstallPeers: true
excludeLinksFromLockfile: false
importers:
.:
dependencies:
'@uncaged/workflow-register':
specifier: workspace:*
version: link:../workflow-register
'@uncaged/workflow-runtime':
specifier: workspace:*
version: link:../workflow-runtime
zod:
specifier: ^4.0.0
version: 4.4.3
packages:
zod@4.4.3:
resolution: {integrity: sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==}
snapshots:
zod@4.4.3: {}
@@ -50,12 +50,6 @@ const allPhasesComplete: ModeratorCondition<DevelopMeta> = {
},
};
const hasRoundsRemaining: ModeratorCondition<DevelopMeta> = {
name: "hasRoundsRemaining",
description: "There are rounds remaining before hitting maxRounds",
check: (ctx) => ctx.steps.length < ctx.start.meta.maxRounds - 1,
};
const reviewApproved: ModeratorCondition<DevelopMeta> = {
name: "reviewApproved",
description: "The last reviewer approved the changes",
@@ -81,18 +75,15 @@ const table: ModeratorTable<DevelopMeta> = {
planner: [{ condition: "FALLBACK", role: "coder" }],
coder: [
{ condition: allPhasesComplete, role: "reviewer" },
{ condition: hasRoundsRemaining, role: "coder" },
{ condition: "FALLBACK", role: END },
{ condition: "FALLBACK", role: "coder" },
],
reviewer: [
{ condition: reviewApproved, role: "tester" },
{ condition: hasRoundsRemaining, role: "coder" },
{ condition: "FALLBACK", role: END },
{ condition: "FALLBACK", role: "coder" },
],
tester: [
{ condition: testsPassed, role: "committer" },
{ condition: hasRoundsRemaining, role: "coder" },
{ condition: "FALLBACK", role: END },
{ condition: "FALLBACK", role: "coder" },
],
committer: [{ condition: "FALLBACK", role: END }],
};
@@ -2,7 +2,11 @@ import type { RoleDefinition } from "@uncaged/workflow-runtime";
import * as z from "zod/v4";
export const coderMetaSchema = z.object({
completedPhase: z.string().describe("The planner phase hash finished this round. If multiple phases were completed, use the last finished phase hash."),
completedPhase: z
.string()
.describe(
"The planner phase hash finished this round. If multiple phases were completed, use the last finished phase hash.",
),
filesChanged: z.array(z.string()),
summary: z.string(),
});
@@ -98,23 +98,22 @@ function installMockToolCallCompletions(
};
}
function makeStart(maxRounds: number): ModeratorContext<SolveIssueMeta>["start"] {
function makeStart(): ModeratorContext<SolveIssueMeta>["start"] {
return {
role: START,
content: "Fix the flaky login test",
meta: { maxRounds },
meta: {},
timestamp: 0,
};
}
function makeCtx(
maxRounds: number,
steps: ModeratorContext<SolveIssueMeta>["steps"],
): ModeratorContext<SolveIssueMeta> {
return {
threadId: "01TEST000000000000000000TR",
depth: 0,
start: makeStart(maxRounds),
start: makeStart(),
steps,
};
}
@@ -182,7 +181,7 @@ function makeThread(prompt: string) {
start: {
role: START,
content: prompt,
meta: { maxRounds: 20 },
meta: {},
timestamp: Date.now(),
},
steps: [],
@@ -191,12 +190,12 @@ function makeThread(prompt: string) {
describe("solveIssueModerator", () => {
test("routes initial → preparer → developer → submitter → END", () => {
expect(solveIssueModerator(makeCtx(20, []))).toBe("preparer");
expect(solveIssueModerator(makeCtx(20, [preparerStep()]))).toBe("developer");
expect(solveIssueModerator(makeCtx(20, [preparerStep(), developerStep()]))).toBe("submitter");
expect(solveIssueModerator(makeCtx([]))).toBe("preparer");
expect(solveIssueModerator(makeCtx([preparerStep()]))).toBe("developer");
expect(solveIssueModerator(makeCtx([preparerStep(), developerStep()]))).toBe("submitter");
expect(
solveIssueModerator(
makeCtx(20, [
makeCtx([
preparerStep(),
developerStep(),
submitterStep({
@@ -211,7 +210,7 @@ describe("solveIssueModerator", () => {
test("submitter failed → END", () => {
expect(
solveIssueModerator(
makeCtx(20, [
makeCtx([
preparerStep(),
developerStep(),
submitterStep({ status: "failed", error: "gh not authenticated" }),
@@ -225,7 +224,7 @@ describe("solveIssueModerator", () => {
// routed to END, since the moderator is a closed switch over known roles.
expect(
solveIssueModerator(
makeCtx(20, [
makeCtx([
preparerStep(),
developerStep(),
submitterStep({ status: "submitted", prUrl: "https://example.com/pr/1" }),
@@ -1,6 +1,6 @@
{
"name": "@uncaged/workflow-template-solve-issue",
"version": "0.2.0",
"version": "0.3.1",
"type": "module",
"exports": {
".": {
+35
View File
@@ -0,0 +1,35 @@
lockfileVersion: '9.0'
settings:
autoInstallPeers: true
excludeLinksFromLockfile: false
importers:
.:
dependencies:
'@uncaged/workflow-register':
specifier: workspace:*
version: link:../workflow-register
'@uncaged/workflow-runtime':
specifier: workspace:*
version: link:../workflow-runtime
zod:
specifier: ^4.0.0
version: 4.4.3
devDependencies:
'@uncaged/workflow-cas':
specifier: workspace:*
version: link:../workflow-cas
'@uncaged/workflow-execute':
specifier: workspace:*
version: link:../workflow-execute
packages:
zod@4.4.3:
resolution: {integrity: sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==}
snapshots:
zod@4.4.3: {}
@@ -7,7 +7,7 @@ function startTask(content: string): AgentContext["start"] {
return {
role: START,
content,
meta: { maxRounds: 5 },
meta: {},
timestamp: 1,
};
}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@uncaged/workflow-util-agent",
"version": "0.2.0",
"version": "0.3.1",
"type": "module",
"main": "src/index.ts",
"types": "src/index.ts",
+13
View File
@@ -0,0 +1,13 @@
lockfileVersion: '9.0'
settings:
autoInstallPeers: true
excludeLinksFromLockfile: false
importers:
.:
dependencies:
'@uncaged/workflow-runtime':
specifier: workspace:*
version: link:../workflow-runtime
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@uncaged/workflow-util",
"version": "0.1.0",
"version": "0.3.1",
"type": "module",
"exports": {
".": {
+28
View File
@@ -0,0 +1,28 @@
lockfileVersion: '9.0'
settings:
autoInstallPeers: true
excludeLinksFromLockfile: false
importers:
.:
dependencies:
'@uncaged/workflow-protocol':
specifier: workspace:*
version: link:../workflow-protocol
devDependencies:
typescript:
specifier: ^5.8.3
version: 5.9.3
packages:
typescript@5.9.3:
resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==}
engines: {node: '>=14.17'}
hasBin: true
snapshots:
typescript@5.9.3: {}
+141
View File
@@ -0,0 +1,141 @@
lockfileVersion: '9.0'
settings:
autoInstallPeers: true
excludeLinksFromLockfile: false
importers:
.:
devDependencies:
'@biomejs/biome':
specifier: ^2.4.14
version: 2.4.15
'@types/xxhashjs':
specifier: ^0.2.4
version: 0.2.4
bun-types:
specifier: ^1.3.13
version: 1.3.13
packages:
'@biomejs/biome@2.4.15':
resolution: {integrity: sha512-j5VH3a/h/HXTKBM50MDMxRCzkeLv9S2XJcW2WgnZT1+xyisi+0bISrXR82gCX+8S9lvK0skEvHJRN+3Ktr2hlw==}
engines: {node: '>=14.21.3'}
hasBin: true
'@biomejs/cli-darwin-arm64@2.4.15':
resolution: {integrity: sha512-rF3PPqLq1yoST79zaQbDjVJwsuIeci/O+9bgNmC5QpgOqz6aqYuzA4abyAGx+mgyiDXn4A049xAN8gijbuR1Qg==}
engines: {node: '>=14.21.3'}
cpu: [arm64]
os: [darwin]
'@biomejs/cli-darwin-x64@2.4.15':
resolution: {integrity: sha512-/5KHXYMfSJs1fNXiX30xFtI8JcCFV6zaVVLxOa0M2sfqBKHkpQhRTv94yxQWxeTY2lzo2OuTlNvPC+hDQt2wcQ==}
engines: {node: '>=14.21.3'}
cpu: [x64]
os: [darwin]
'@biomejs/cli-linux-arm64-musl@2.4.15':
resolution: {integrity: sha512-ZPcxznxm0pogHBLZhYntyR3sR+MrZjqJIKEr7ZqVen0Rl+P/4upVmfYXjftizi9RoqZntg33fv/1fbdhbYXpEQ==}
engines: {node: '>=14.21.3'}
cpu: [arm64]
os: [linux]
libc: [musl]
'@biomejs/cli-linux-arm64@2.4.15':
resolution: {integrity: sha512-owaAMZD/T4LrD0ELNCk0Km3qrRHuM0X6EAyVE1FSqGY0rbLoiDLrO4Us2tllm6cAeB2Ioa9C2C08NZPdr8+0Ug==}
engines: {node: '>=14.21.3'}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@biomejs/cli-linux-x64-musl@2.4.15':
resolution: {integrity: sha512-CNq/9W38SYSH023lfcQ4KKU8K0YX8T//FZUhcgtMMRABDojx5XsMV7jlweAvGSl389wJQB29Qo6Zb/a+jdvt+w==}
engines: {node: '>=14.21.3'}
cpu: [x64]
os: [linux]
libc: [musl]
'@biomejs/cli-linux-x64@2.4.15':
resolution: {integrity: sha512-0jj7THz12GbUOLmMibktK6DZjqz2zV64KFxyBtcFTKPiiOIY0a7vns1elpO1dERvxpsZ5ik0oFfz0oGwFde1+g==}
engines: {node: '>=14.21.3'}
cpu: [x64]
os: [linux]
libc: [glibc]
'@biomejs/cli-win32-arm64@2.4.15':
resolution: {integrity: sha512-ouhkYdlhp/1GghEJPdWwD/Vi3gQ1nFxuSpMolWsbq3Lsq3QUR4jl6UdhhscdCugKU5vOEuMiJhvKj66O0OCq+w==}
engines: {node: '>=14.21.3'}
cpu: [arm64]
os: [win32]
'@biomejs/cli-win32-x64@2.4.15':
resolution: {integrity: sha512-zBrGq5mx5wwpnow4+2BxUvleDM+GNd4sLbPaMapsSLQLD0NGRCquqPBTgN+7XkUteHvj7M+BstuI8tmnV7+HgQ==}
engines: {node: '>=14.21.3'}
cpu: [x64]
os: [win32]
'@types/node@25.6.2':
resolution: {integrity: sha512-sokuT28dxf9JT5Kady1fsXOvI4HVpjZa95NKT5y9PNTIrs2AsobR4GFAA90ZG8M+nxVRLysCXsVj6eGC7Vbrlw==}
'@types/xxhashjs@0.2.4':
resolution: {integrity: sha512-E2+ZoJY2JjmVPN0iQM5gJvZkk98O2PYXSi6HrciEk3EKF34+mauEk/HgwTeCz+2r8HXHMKpucrwy4qTT12OPaQ==}
bun-types@1.3.13:
resolution: {integrity: sha512-QXKeHLlOLqQX9LgYaHJfzdBaV21T63HhFJnvuRCcjZiaUDpbs5ED1MgxbMra71CsryN/1dAoXuJJJwIv/2drVA==}
undici-types@7.19.2:
resolution: {integrity: sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg==}
snapshots:
'@biomejs/biome@2.4.15':
optionalDependencies:
'@biomejs/cli-darwin-arm64': 2.4.15
'@biomejs/cli-darwin-x64': 2.4.15
'@biomejs/cli-linux-arm64': 2.4.15
'@biomejs/cli-linux-arm64-musl': 2.4.15
'@biomejs/cli-linux-x64': 2.4.15
'@biomejs/cli-linux-x64-musl': 2.4.15
'@biomejs/cli-win32-arm64': 2.4.15
'@biomejs/cli-win32-x64': 2.4.15
'@biomejs/cli-darwin-arm64@2.4.15':
optional: true
'@biomejs/cli-darwin-x64@2.4.15':
optional: true
'@biomejs/cli-linux-arm64-musl@2.4.15':
optional: true
'@biomejs/cli-linux-arm64@2.4.15':
optional: true
'@biomejs/cli-linux-x64-musl@2.4.15':
optional: true
'@biomejs/cli-linux-x64@2.4.15':
optional: true
'@biomejs/cli-win32-arm64@2.4.15':
optional: true
'@biomejs/cli-win32-x64@2.4.15':
optional: true
'@types/node@25.6.2':
dependencies:
undici-types: 7.19.2
'@types/xxhashjs@0.2.4':
dependencies:
'@types/node': 25.6.2
bun-types@1.3.13:
dependencies:
'@types/node': 25.6.2
undici-types@7.19.2: {}