docs: update architecture docs and package READMEs for post-split structure

- Rewrite docs/architecture.md with 15-package map, dependency graph, updated engine paths
- Update CLAUDE.md monorepo structure section
- Add READMEs for: workflow-protocol, workflow-runtime, workflow-util, workflow-cas, workflow-register, workflow-execute, workflow-reactor
- Fix agent READMEs: update deps from @uncaged/workflow to actual packages
- Mark workflow-as-agent plan as outdated

Fixes #153

小橘 <xiaoju@shazhou.work>
This commit is contained in:
2026-05-09 04:39:57 +00:00
parent 0f28e9b61a
commit 064696c558
13 changed files with 389 additions and 146 deletions
+19 -6
View File
@@ -2,7 +2,7 @@
## Project Overview ## Project Overview
**@uncaged/workflow** is a workflow engine that executes single-file ESM bundles. Each workflow is a self-contained `.esm.js` file with an XXH64 hash as its version identifier. This monorepo implements a workflow engine that executes single-file ESM bundles. Each workflow is a self-contained `.esm.js` file with an XXH64 hash as its version identifier. Shared types live in `@uncaged/workflow-protocol`; bundle authors typically depend on `@uncaged/workflow-runtime`.
### Key Terms ### Key Terms
@@ -19,14 +19,27 @@
``` ```
workflow/ workflow/
packages/ packages/
workflow/ # @uncaged/workflow — core lib (types, hash, ULID, JSONL, registry) workflow-protocol/ # @uncaged/workflow-protocol — shared types + Result
cli-workflow/ # @uncaged/cli-workflow — CLI (uncaged-workflow command) workflow-runtime/ # @uncaged/workflow-runtime — createWorkflow, type re-exports
workflow-util/ # @uncaged/workflow-util — Base32, ULID, logger, storage paths, refs helpers
workflow-reactor/ # @uncaged/workflow-reactor — LLM fn + thread reactor (tool calls)
workflow-cas/ # @uncaged/workflow-cas — CAS store, hash, Merkle
workflow-register/ # @uncaged/workflow-register — bundle validation, registry YAML, model resolution
workflow-execute/ # @uncaged/workflow-execute — engine, extract, fork, GC, workflowAsAgent
cli-workflow/ # @uncaged/cli-workflow — uncaged-workflow CLI
workflow-agent-cursor/ # @uncaged/workflow-agent-cursor
workflow-agent-hermes/ # @uncaged/workflow-agent-hermes
workflow-agent-llm/ # @uncaged/workflow-agent-llm
workflow-util-agent/ # @uncaged/workflow-util-agent — buildAgentPrompt, spawnCli
workflow-template-develop/ # @uncaged/workflow-template-develop
workflow-template-solve-issue/ # @uncaged/workflow-template-solve-issue
workflow-dashboard/ # @uncaged/workflow-dashboard — React dashboard (private app)
docs/ # RFCs, conventions docs/ # RFCs, conventions
biome.json # root Biome config biome.json # root Biome config
tsconfig.json # root TypeScript config tsconfig.json # root TypeScript config
``` ```
- `workflow` is the core; `cli-workflow` depends on it - Execution stack layers: `workflow-protocol` → (`workflow-runtime`, `workflow-util`, `workflow-reactor`) → (`workflow-cas`, `workflow-register`) → `workflow-execute``cli-workflow`
- Packages use `workspace:*` protocol - Packages use `workspace:*` protocol
## Language & Paradigm ## Language & Paradigm
@@ -167,10 +180,10 @@ type Result<T, E = Error> = { ok: true; value: T } | { ok: false; error: E };
Never use `console.log/warn/error` directly — Biome's `noConsole` rule enforces this. Never use `console.log/warn/error` directly — Biome's `noConsole` rule enforces this.
All logging goes through the structured logger from `@uncaged/workflow`: All logging goes through the structured logger from `@uncaged/workflow-util`:
```typescript ```typescript
import { createLogger } from "@uncaged/workflow"; import { createLogger } from "@uncaged/workflow-util";
const log = createLogger(); const log = createLogger();
+143 -130
View File
@@ -1,6 +1,6 @@
# @uncaged/workflow — Architecture # Uncaged workflow — Architecture
**Last updated:** 2026-05-06 by 小橘 🍊(NEKO Team) **Last updated:** 2026-05-09
--- ---
@@ -8,72 +8,106 @@
A workflow engine that executes single-file ESM bundles. Each workflow is a self-contained `.esm.js` file identified by its XXH64 hash (Crockford Base32). No daemon — processes start on demand and exit when done. A workflow engine that executes single-file ESM bundles. Each workflow is a self-contained `.esm.js` file identified by its XXH64 hash (Crockford Base32). No daemon — processes start on demand and exit when done.
## Package Structure The implementation lives in **15** Bun workspace packages under `packages/`, using the `workspace:*` protocol.
| Package | npm Name | Purpose | ## Package map
|---------|----------|---------|
| `workflow` | `@uncaged/workflow` | Core: types, engine, ExtractFn, hash/ULID/registry |
| `cli-workflow` | `@uncaged/cli-workflow` | CLI: `uncaged-workflow` command |
| `workflow-agent-cursor` | `@uncaged/workflow-agent-cursor` | Cursor CLI agent (extracts workspace from ctx) |
| `workflow-agent-hermes` | `@uncaged/workflow-agent-hermes` | Hermes CLI agent |
| `workflow-agent-llm` | `@uncaged/workflow-agent-llm` | OpenAI-compatible LLM agent |
| `workflow-template-develop` | `@uncaged/workflow-template-develop` | Develop workflow template (roles in `src/roles/`) |
| `workflow-template-solve-issue` | `@uncaged/workflow-template-solve-issue` | Solve-issue workflow template (roles in `src/roles/`) |
| `workflow-util-agent` | `@uncaged/workflow-util-agent` | `buildAgentPrompt` + `spawnCli` utilities |
Monorepo with **bun workspace**, `workspace:*` protocol. Grouped by responsibility (npm name → folder).
## Core Types | Layer | Package | One-line role |
|-------|---------|----------------|
| Contract | `@uncaged/workflow-protocol``workflow-protocol` | Shared TypeScript types and `Result` helpers; peer `zod` only — no other workspace deps. |
| Author API | `@uncaged/workflow-runtime``workflow-runtime` | `createWorkflow` and re-exports of protocol workflow types for bundle authors. |
| Shared infra | `@uncaged/workflow-util``workflow-util` | Base32/ULID, logger, storage root paths, global CAS dir, ref-field helpers. |
| LLM plumbing | `@uncaged/workflow-reactor``workflow-reactor` | `createLlmFn`, `createThreadReactor`, and related tool-call types for threaded LLM invocation. |
| CAS | `@uncaged/workflow-cas``workflow-cas` | `CasStore` implementation, XXH64 hashing, Merkle helpers over CAS payloads. |
| Registry / bundles | `@uncaged/workflow-register``workflow-register` | Bundle validation & dynamic export extraction, `workflow.yaml` registry I/O, provider/model resolution. |
| Engine | `@uncaged/workflow-execute``workflow-execute` | Thread execution, worker entry path, fork/GC, extract pipeline, `workflowAsAgent`. |
| CLI | `@uncaged/cli-workflow``cli-workflow` | `uncaged-workflow` binary (depends on engine, registry, CAS, protocol, util, runtime). |
| Agent adapters | `@uncaged/workflow-agent-cursor``workflow-agent-cursor` | `AgentFn` via `cursor-agent` CLI + workspace extraction. |
| | `@uncaged/workflow-agent-hermes``workflow-agent-hermes` | `AgentFn` via `hermes chat` CLI. |
| | `@uncaged/workflow-agent-llm``workflow-agent-llm` | `AgentFn` via OpenAI-compatible HTTP (`LlmProvider` from runtime). |
| Agent shared | `@uncaged/workflow-util-agent``workflow-util-agent` | `buildAgentPrompt`, `spawnCli` for CLI-backed agents. |
| Templates | `@uncaged/workflow-template-develop``workflow-template-develop` | Develop workflow definition, roles, descriptor builder. |
| | `@uncaged/workflow-template-solve-issue``workflow-template-solve-issue` | Solve-issue workflow definition, roles, descriptor builder. |
| Dashboard | `@uncaged/workflow-dashboard``workflow-dashboard` | Private Vite + React app (`src/main.tsx`); only `react` / `react-dom` dependencies — no workspace packages. |
```typescript ## Dependency graph (workspace packages)
// --- Sentinel values ---
const START = "__start__";
const END = "__end__";
// --- RoleMeta: maps role names → their meta types --- Bottom-up layering for the execution stack:
type RoleMeta = Record<string, Record<string, unknown>>;
// --- Role Definition: pure data, no execution logic --- ```mermaid
type RoleDefinition<Meta> = { flowchart BT
description: string; // human-readable subgraph L0["Layer 0 — contract"]
systemPrompt: string; // given to agent protocol["@uncaged/workflow-protocol"]
extractPrompt: string; // given to extractor end
schema: z.ZodType<Meta>; // meta shape (Zod v4) subgraph L1["Layer 1 — on protocol"]
}; runtime["@uncaged/workflow-runtime"]
util["@uncaged/workflow-util"]
// --- Workflow Definition: pure data, no agent binding --- reactor["@uncaged/workflow-reactor"]
type WorkflowDefinition<M extends RoleMeta> = { end
description: string; subgraph L2["Layer 2 — protocol + util"]
roles: { [K in keyof M & string]: RoleDefinition<M[K]> }; cas["@uncaged/workflow-cas"]
moderator: Moderator<M>; register["@uncaged/workflow-register"]
}; end
subgraph L3["Layer 3 — engine"]
// --- Agent: raw string output, reads role info from context --- execute["@uncaged/workflow-execute"]
type AgentFn = (ctx: AgentContext) => Promise<string>; end
subgraph L4["Layer 4 — CLI"]
// --- Agent Binding: runtime assignment --- cli["@uncaged/cli-workflow"]
type AgentBinding = { end
agent: AgentFn; runtime --> protocol
overrides?: Partial<Record<string, AgentFn>>; util --> protocol
}; reactor --> protocol
cas --> protocol
// --- Extract: structured data from context --- cas --> util
type ExtractFn = <T>(schema: z.ZodType<T>, prompt: string, ctx: ExtractContext) => Promise<T>; register --> protocol
register --> util
// --- Moderator: pure routing function --- execute --> protocol
type Moderator<M extends RoleMeta> = (ctx: ModeratorContext<M>) => (keyof M & string) | typeof END; execute --> runtime
execute --> util
// --- Composition --- execute --> cas
// createWorkflow(def, binding, extract) => WorkflowFn execute --> reactor
execute --> register
cli --> protocol
cli --> util
cli --> cas
cli --> execute
cli --> register
cli --> runtime
``` ```
## Three-Phase Engine Loop **Adjacent consumers** (not in the main CLI stack):
Each role execution has three distinct phases with progressive context: - `@uncaged/workflow-util-agent``@uncaged/workflow-runtime`
- `@uncaged/workflow-agent-llm``@uncaged/workflow-runtime`
- `@uncaged/workflow-agent-cursor``@uncaged/workflow-runtime`, `@uncaged/workflow-util-agent`, `zod`
- `@uncaged/workflow-agent-hermes``@uncaged/workflow-runtime`, `@uncaged/workflow-util-agent`
- `@uncaged/workflow-template-develop``@uncaged/workflow-register`, `@uncaged/workflow-runtime`, `zod`
- `@uncaged/workflow-template-solve-issue``@uncaged/workflow-register`, `@uncaged/workflow-runtime`, `zod` (dev-only workspace deps: `@uncaged/workflow-cas`, `@uncaged/workflow-execute` for tests/tooling per `package.json`)
## Package roles (detail)
- **`workflow-protocol`** — Pure types (`WorkflowFn`, contexts, `CasStore` interface, descriptor shapes), `START` / `END`, `ok` / `err`. Depends only on peer `zod` for schema-related types in signatures.
- **`workflow-runtime`** — Workflow author surface: `createWorkflow` from `src/create-workflow.js`, re-exports protocol types/constants used when authoring bundles.
- **`workflow-util`** — Cross-cutting utilities: Crockford Base32, ULID, `createLogger`, `getDefaultWorkflowStorageRoot`, `getGlobalCasDir`, ref normalization; re-exports `ok`/`err` from protocol.
- **`workflow-cas`** — Filesystem CAS (`createCasStore`), `hashString` / `hashWorkflowBundleBytes`, Merkle node serialization and helpers (`merkle.js`).
- **`workflow-register`** — Bundle pipeline (`validateWorkflowBundle`, `extractBundleExports`, descriptor builders), registry YAML read/write, `resolveModel` / `splitProviderModelRef`.
- **`workflow-execute`** — `executeThread`, supervisor/worker wiring (`engine/`), fork/GC/pause gate, `createExtract` + LLM extract helpers (`extract/`), `workflowAsAgent`. Imports `@uncaged/workflow-reactor` for LLM-backed extract/supervisor paths (`extract-fn.ts`, `supervisor.ts`).
- **`workflow-reactor`** — `createLlmFn`, `createThreadReactor`, and thread tool-invocation types — consumed by `workflow-execute`.
- **`cli-workflow`** — CLI commands and HTTP/dashboard-related wiring (`hono`, `yaml`); composes register + execute + CAS + util.
- **`workflow-agent-*`** — Replaceable `AgentFn` implementations (Cursor / Hermes CLIs, or HTTP LLM).
- **`workflow-util-agent`** — Shared prompt assembly and subprocess spawning for CLI agents.
- **`workflow-template-*`** — Concrete `WorkflowDefinition` graphs + Zod role schemas + descriptor builders for publishing bundles.
- **`workflow-dashboard`** — Standalone React UI; no published library entry matching `src/index.ts`.
## Three-phase engine loop
Each role round is implemented in `packages/workflow-runtime/src/create-workflow.ts` (`advanceOneRound`): moderator → agent → extractor, with progressive context types from `@uncaged/workflow-protocol`.
``` ```
┌─→ Phase 1: MODERATOR ┌─→ Phase 1: MODERATOR
│ Context: ModeratorContext { threadId, start, steps } │ Context: ModeratorContext { threadId, depth, start, steps }
│ Action: moderator(ctx) → role name | END │ Action: moderator(ctx) → role name | END
│ Phase 2: AGENT │ Phase 2: AGENT
@@ -82,90 +116,80 @@ Each role execution has three distinct phases with progressive context:
│ Phase 3: EXTRACTOR │ Phase 3: EXTRACTOR
│ Context: ExtractContext = AgentCtx + { agentContent } │ Context: ExtractContext = AgentCtx + { agentContent }
│ Action: extract(schema, extractPrompt, ctx) → typed meta │ Action: runtime.extract(schema, extractPrompt, ctx) → typed meta
│ Merge: RoleStep { role, content, meta, timestamp } │ Merge: RoleStep { role, contentHash, meta, refs, timestamp }
│ Append to steps │ Append to steps
└─────────────────────────────────────────────────────┘ └─────────────────────────────────────────────────────┘
``` ```
### Context Types (progressive) ### Context types (progressive)
Defined in `packages/workflow-protocol/src/types.ts`:
```typescript ```typescript
// Phase 1: Moderator sees accumulated state only type ModeratorContext<M> = ThreadContext<M>;
type ModeratorContext<M> = {
threadId: string;
start: StartStep;
steps: RoleStep<M>[];
};
// Phase 2: Agent knows its identity
type AgentContext<M> = ModeratorContext<M> & { type AgentContext<M> = ModeratorContext<M> & {
currentRole: { name: string; systemPrompt: string }; currentRole: { name: string; systemPrompt: string };
}; };
type ExtractContext<M> = AgentContext<M> & { agentContent: string };
// Phase 3: Extractor has agent output
type ExtractContext<M> = AgentContext<M> & {
agentContent: string;
};
// ThreadContext is an alias for AgentContext (backward compat)
type ThreadContext<M> = AgentContext<M>;
``` ```
### Key Properties ### Key properties
- **Moderator is synchronous and pure** — no I/O, no state mutation - **Moderator is synchronous and pure** — no I/O, no state mutation inside `createWorkflow`’s moderator call path.
- **Agent gets context, not instructions** — reads `ctx.currentRole.systemPrompt` - **Agent receives `AgentContext`** — reads `ctx.currentRole.systemPrompt`; raw output becomes `agentContent` for extract.
- **Extractor is a general tool** — not limited to post-agent extraction; agents can use it too (e.g. Cursor agent extracts workspace path before execution) - **Extractor is `WorkflowRuntime.extract`** — supplied by the engine from registry-resolved LLM config (`workflow-execute`); stores agent body in CAS and yields `contentHash` + `refs` on each step (`create-workflow.ts`).
- **extractPrompt is a call parameter**, not context state — different callers use different prompts - **`extractPrompt` is a call parameter** on `RoleDefinition`, not implicit context state.
## Agent Information Sources ## Agent information sources
An agent has exactly three information sources: An agent has exactly three information sources:
1. **Prior knowledge** — LLM training, agent memory, agent skills 1. **Prior knowledge** — LLM training, agent memory, agent skills
2. **Thread context**`AgentContext` (start, steps, currentRole) 2. **Thread context**`AgentContext` (`start`, `steps`, `currentRole`)
3. **Derived information** — from 1 & 2 (e.g. tool calls, shell commands) 3. **Derived information** — from 1 & 2 (e.g. tool calls, shell commands)
No hidden environment parameters. If an agent needs something (like a workspace path), it extracts it from context using `ExtractFn`. No hidden environment parameters. If an agent needs something (like a workspace path), it obtains it via `ExtractFn` (e.g. Cursor agent).
## Bundle Contract ## Bundle contract
A workflow bundle is a single `.esm.js` file with two named exports: A workflow bundle is a single `.esm.js` file with two named exports (see `WorkflowFn` / `WorkflowDescriptor` in `packages/workflow-protocol/src/types.ts`):
```typescript ```typescript
// Named exports (no default export)
export const descriptor: WorkflowDescriptor; export const descriptor: WorkflowDescriptor;
export const run: WorkflowFn; export const run: WorkflowFn;
type WorkflowFn = ( type WorkflowFn = (
input: { prompt: string; steps: RoleOutput[] }, thread: ThreadContext,
options: { threadId: string; maxRounds: number }, runtime: WorkflowRuntime,
) => AsyncGenerator<RoleOutput, WorkflowResult>; ) => AsyncGenerator<RoleOutput, WorkflowCompletion>;
``` ```
`RoleOutput` carries `contentHash`, `meta`, and `refs` (agent text lives in CAS, addressed by hash).
### Constraints ### Constraints
- Single `.esm.js` file - Single `.esm.js` file
- No dynamic `import()` - No dynamic `import()` in bundles (loader exempt in engine)
- All static imports must be Node built-in modules only - Portable bundle static imports are constrained by validation in `@uncaged/workflow-register` (`validateWorkflowBundle`)
- XXH64 hash (Crockford Base32) = globally unique version ID - XXH64 hash (Crockford Base32) = version ID
### Why AsyncGenerator? ### Why AsyncGenerator?
- Each `yield` → engine writes to `.data.jsonl`, checks abort/pause - Each `yield` lets `workflow-execute` persist state, CAS rows, and enforce pause/abort
- `return` → engine marks thread complete - `return` supplies `WorkflowCompletion`
- Fork = pass historical steps as `input.steps` to a new generator - Fork replays historical steps into a new thread context
- Zero injection — bundle doesn't import from the engine - Bundle does not import the engine — only protocol/runtime types at build time
## Storage Layout ## Storage layout
``` ```
~/.uncaged/workflow/ ~/.uncaged/workflow/
├── cas/ # Global content-addressed blobs (see getGlobalCasDir)
├── bundles/ ├── bundles/
│ ├── C9NMV6V2TQT81.esm.js # Crockford Base32 of XXH64 │ ├── C9NMV6V2TQT81.esm.js # Crockford Base32 of XXH64
│ └── C9NMV6V2TQT81.yaml # Role descriptor │ └── C9NMV6V2TQT81.yaml # Role descriptor sidecar (when present)
├── logs/ # One folder per bundle hash ├── logs/ # One folder per bundle hash
│ └── C9NMV6V2TQT81/ │ └── C9NMV6V2TQT81/
│ ├── 01KQXKW…YG.data.jsonl # Thread state │ ├── 01KQXKW…YG.data.jsonl # Thread state
@@ -173,7 +197,7 @@ type WorkflowFn = (
└── workflow.yaml # Registry └── workflow.yaml # Registry
``` ```
### ID Encoding: Crockford Base32 ### ID encoding: Crockford Base32
- Case-insensitive, filesystem-safe, no ambiguous chars (0/O, 1/I/L) - Case-insensitive, filesystem-safe, no ambiguous chars (0/O, 1/I/L)
- Bundle hash: XXH64 → 13-char - Bundle hash: XXH64 → 13-char
@@ -181,45 +205,36 @@ type WorkflowFn = (
### Registry (`workflow.yaml`) ### Registry (`workflow.yaml`)
```yaml Managed by `@uncaged/workflow-register` (`readWorkflowRegistry`, `writeWorkflowRegistry`, …). Shape includes workflow entries and a top-level `config` section used for extract/supervisor model resolution.
workflows:
solve-issue:
hash: "C9NMV6V2TQT81"
timestamp: 1714963200000
history:
- hash: "A7BKR3M1NPQ40"
timestamp: 1714876800000
```
### Thread JSONL ### Thread JSONL
**`.data.jsonl`** — Line 1: start record, Line 2+: role outputs **`.data.jsonl`** — Line 1: start record; following lines: role steps with CAS-backed content.
```jsonc ```jsonc
// Start record // Start record
{ "name": "solve-issue", "hash": "C9NMV6V2TQT81", "threadId": "01KQXKW…", { "name": "solve-issue", "hash": "C9NMV6V2TQT81", "threadId": "01KQXKW…",
"parameters": { "prompt": "Fix bug #3", "options": { "maxRounds": 5 } }, "parameters": { "prompt": "Fix bug #3", "options": { "maxRounds": 5 } },
"timestamp": 1714963200000 } "timestamp": 1714963200000 }
// Role output // Role output (engine persists contentHash + refs; body in ~/.uncaged/workflow/cas/)
{ "role": "planner", "content": "...", "meta": { "phases": [...] }, "timestamp": ... } { "role": "planner", "contentHash": "", "meta": { "phases": [...] }, "refs": ["…"], "timestamp": ... }
``` ```
**`.info.jsonl`** — Structured debug log **`.info.jsonl`** — Structured debug log via `@uncaged/workflow-util` `createLogger`:
```jsonc ```jsonc
{ "tag": "4KNMR2PX", "content": "Loading bundle...", "timestamp": ... } { "tag": "4KNMR2PX", "content": "Loading bundle...", "timestamp": ... }
``` ```
Tags are 8-char Crockford Base32 (40-bit random), one per call site. `grep "4KNMR2PX"` instant code location. Tags are 8-char Crockford Base32 (40-bit random), one per call site. `grep "4KNMR2PX"` → code location.
## Execution Model ## Execution model
- **No daemon.** `uncaged-workflow run <name>` starts a worker process - **No daemon.** `uncaged-workflow run <name>` starts a worker process (`workflow-execute` worker entry via `getWorkerHostScriptPath`)
- Same bundle's threads share one process (memory efficiency) - Threads share bundle-scoped workers as implemented in CLI/engine
- Process exits when all threads complete - Pause/resume/abort via engine IPC and pause gate (`createThreadPauseGate`)
- Thread termination via IPC within the process
## CLI Commands ## CLI commands
| Priority | Command | Description | | Priority | Command | Description |
|----------|---------|-------------| |----------|---------|-------------|
@@ -239,18 +254,16 @@ Tags are 8-char Crockford Base32 (40-bit random), one per call site. `grep "4KNM
| P2 | `resume <thread-id>` | Resume a paused thread | | P2 | `resume <thread-id>` | Resume a paused thread |
| P3 | `fork <thread-id> [--from-role <role>]` | Fork from historical state | | P3 | `fork <thread-id> [--from-role <role>]` | Fork from historical state |
All commands implemented and tested. ✅ ## Design decisions
## Design Decisions
| Decision | Rationale | | Decision | Rationale |
|----------|-----------| |----------|-----------|
| **Role = pure data** | Decouples definition from execution; same role with different agents | | **Role = pure data** | Decouples definition from execution; same role with different agents |
| **Agent bound at runtime** | WorkflowDefinition is reusable; agent choice is deployment concern | | **Agent bound at runtime** | `WorkflowDefinition` is reusable; agent choice is deployment concern |
| **Three-phase context** | Each phase sees only what it needs; clean separation | | **Three-phase context** | Each phase sees only what it needs; types live in `workflow-protocol` |
| **ExtractFn as general tool** | Agents use it for pre-execution extraction; engine uses it for meta | | **`WorkflowRuntime.extract` + CAS `contentHash`** | Large agent bodies deduplicated globally; Merkle roots summarize threads |
| **Single-file ESM** | Hash = version, no dependency hell, self-contained | | **`workflow-reactor` split** | LLM tool-calling loop isolated from filesystem/registry concerns |
| **No daemon** | OS handles process lifecycle; unnecessary complexity | | **Single-file ESM** | Hash = version, self-contained bundle |
| **No daemon** | OS handles process lifecycle |
| **Crockford Base32** | Filesystem-safe, readable, compact | | **Crockford Base32** | Filesystem-safe, readable, compact |
| **No concurrency in registry** | Different workflows have different constraints; belongs at workflow/role level | | **15-package split** | Clear boundaries: protocol ↔ runtime author API ↔ util/CAS/register ↔ execute ↔ CLI ↔ agents/templates/UI |
| **No dryRun** | Tests use mock agents + mock fetch; simpler architecture |
@@ -1,5 +1,7 @@
# Workflow-as-Agent Implementation Plan # Workflow-as-Agent Implementation Plan
> ⚠️ This plan references the pre-split package structure. File paths have changed.
> **For Hermes:** Use subagent-driven-development skill to implement this plan task-by-task. > **For Hermes:** Use subagent-driven-development skill to implement this plan task-by-task.
**Goal:** Enable workflows to invoke other workflows as agents, backed by global CAS and refs tracking. **Goal:** Enable workflows to invoke other workflows as agents, backed by global CAS and refs tracking.
+3 -4
View File
@@ -7,10 +7,10 @@ The agent builds a full prompt (system + task + step history via `@uncaged/workf
## Install ## Install
```bash ```bash
bun add @uncaged/workflow-agent-cursor @uncaged/workflow @uncaged/workflow-util-agent zod bun add @uncaged/workflow-agent-cursor @uncaged/workflow-runtime @uncaged/workflow-util-agent zod
``` ```
In this monorepo: `"@uncaged/workflow-agent-cursor": "workspace:*"` plus `workspace:*` for `@uncaged/workflow` and `@uncaged/workflow-util-agent`. In this monorepo: `"@uncaged/workflow-agent-cursor": "workspace:*"` plus `workspace:*` for `@uncaged/workflow-runtime` and `@uncaged/workflow-util-agent`, and `zod` ^4.
## Usage ## Usage
@@ -28,9 +28,8 @@ const agent = createCursorAgent({
| Export | Description | | Export | Description |
|--------|-------------| |--------|-------------|
| `createCursorAgent(config)` | Returns `AgentFn` that runs `cursor-agent` with `buildAgentPrompt(ctx)` | | `createCursorAgent(config)` | Returns `AgentFn` that runs `cursor-agent` with `buildAgentPrompt(ctx)` from `@uncaged/workflow-util-agent` |
| `CursorAgentConfig` | `model`, `timeout`, `extract` (must supply workspace path) | | `CursorAgentConfig` | `model`, `timeout`, `extract` (must supply workspace path) |
| `validateCursorAgentConfig` | Config validation result | | `validateCursorAgentConfig` | Config validation result |
| `buildAgentPrompt` | Re-exported from `@uncaged/workflow-util-agent` |
Requires `cursor-agent` on `PATH` at runtime. Requires `cursor-agent` on `PATH` at runtime.
+2 -3
View File
@@ -7,10 +7,10 @@ The agent composes the same thread-aware prompt as other CLI-backed agents via `
## Install ## Install
```bash ```bash
bun add @uncaged/workflow-agent-hermes @uncaged/workflow @uncaged/workflow-util-agent bun add @uncaged/workflow-agent-hermes @uncaged/workflow-runtime @uncaged/workflow-util-agent
``` ```
In this monorepo: use `workspace:*` for all three `@uncaged/*` packages. In this monorepo: use `workspace:*` for `@uncaged/workflow-agent-hermes`, `@uncaged/workflow-runtime`, and `@uncaged/workflow-util-agent`.
## Usage ## Usage
@@ -30,6 +30,5 @@ const agent = createHermesAgent({
| `createHermesAgent(config)` | Returns `AgentFn` wrapping `hermes chat -q ...` | | `createHermesAgent(config)` | Returns `AgentFn` wrapping `hermes chat -q ...` |
| `HermesAgentConfig` | `model`, `timeout` | | `HermesAgentConfig` | `model`, `timeout` |
| `validateHermesAgentConfig` | Config validation result | | `validateHermesAgentConfig` | Config validation result |
| `buildAgentPrompt` | Re-exported from `@uncaged/workflow-util-agent` |
Requires `hermes` on `PATH` at runtime. Requires `hermes` on `PATH` at runtime.
+3 -3
View File
@@ -1,16 +1,16 @@
# @uncaged/workflow-agent-llm # @uncaged/workflow-agent-llm
`AgentFn` adapter that calls an OpenAI-compatible `POST /chat/completions` endpoint using `@uncaged/workflow`’s `LlmProvider` (base URL, API key, model). `AgentFn` adapter that calls an OpenAI-compatible `POST /chat/completions` endpoint using `LlmProvider` from `@uncaged/workflow-runtime`.
Single-turn: system text is the current role’s `systemPrompt`, user text is the thread’s initial prompt (`ctx.start.content`). Errors from HTTP, JSON, or empty choices are thrown as `Error` with a JSON payload string. Single-turn: system text is the current role’s `systemPrompt`, user text is the thread’s initial prompt (`ctx.start.content`). Errors from HTTP, JSON, or empty choices are thrown as `Error` with a JSON payload string.
## Install ## Install
```bash ```bash
bun add @uncaged/workflow-agent-llm @uncaged/workflow bun add @uncaged/workflow-agent-llm @uncaged/workflow-runtime zod
``` ```
In this monorepo: `"@uncaged/workflow-agent-llm": "workspace:*"`, `"@uncaged/workflow": "workspace:*"`. In this monorepo: `"@uncaged/workflow-agent-llm": "workspace:*"`, `"@uncaged/workflow-runtime": "workspace:*"` (and satisfy `zod` ^4 as required by `@uncaged/workflow-runtime`).
## Usage ## Usage
+31
View File
@@ -0,0 +1,31 @@
# @uncaged/workflow-cas
Content-addressable storage implementation, bundle hashing, and Merkle helpers.
## What This Package Does
It implements `CasStore` from `@uncaged/workflow-protocol`, hashes workflow bundle bytes and strings with XXH64, and builds serializable Merkle nodes for thread/step/content payloads used when persisting execution artifacts.
## Key Exports
From `src/index.ts`:
- **CAS:** `createCasStore`
- **Hash:** `hashString`, `hashWorkflowBundleBytes`
- **Merkle:** `createContentMerkleNode`, `getContentMerklePayload`, `parseMerkleNode`, `putContentMerkleNode`, `putStepMerkleNode`, `putThreadMerkleNode`, `serializeMerkleNode`
- **Types:** `CasStore`, `MerkleNode`, `MerkleNodeType`, `StepMerklePayload`, `ThreadMerklePayload`
## Dependencies
- **Workspace:** `@uncaged/workflow-protocol` (`CasStore` contract), `@uncaged/workflow-util`
- **npm:** `xxhashjs`, `yaml`
## Usage
```typescript
import { createCasStore, hashWorkflowBundleBytes } from "@uncaged/workflow-cas";
import { getGlobalCasDir } from "@uncaged/workflow-util";
const store = createCasStore(getGlobalCasDir());
const hash = await hashWorkflowBundleBytes(esmJsBytes);
```
+33
View File
@@ -0,0 +1,33 @@
# @uncaged/workflow-execute
Thread engine: execution, fork/GC, extract pipeline, supervisor/worker wiring, and workflow-as-agent.
## What This Package Does
It runs `WorkflowFn` generators against disk-backed threads, integrates CAS and registry-backed extract (`createExtract`), coordinates LLM tool usage via `@uncaged/workflow-reactor`, handles fork plans and garbage collection, and exposes `workflowAsAgent` for nesting workflows.
## Key Exports
From `src/index.ts`:
- **Engine:** `createWorkflow` (engine-local re-export), `executeThread`, `getWorkerHostScriptPath`
- **Fork / parse:** `buildForkPlan`, `parseThreadDataJsonl`, `selectForkHistoricalSteps`, `tryParseRoleStepRecord`, `tryParseWorkflowResultRecord`
- **GC / pause:** `garbageCollectCas`, `createThreadPauseGate`
- **Engine types:** `ExecuteThreadIo`, `ExecuteThreadOptions`, `ForkHistoricalStep`, `ForkPlan`, `GcResult`, `ParsedThreadStartRecord`, `PrefilledDiskStep`, `SupervisorDecision`, `ThreadPauseGate`
- **Extract:** `buildExtractUserContent`, `createExtract`, `extractFunctionToolFromZodSchema`, `llmErrorToCause`, `llmExtract`, types `ExtractFn`, `ExtractThreadContext`, `LlmError`, `LlmExtractArgs`
- **Agent composition:** `workflowAsAgent`, `WorkflowAsAgentOptions`
## Dependencies
- **Workspace:** `@uncaged/workflow-protocol`, `@uncaged/workflow-runtime`, `@uncaged/workflow-util`, `@uncaged/workflow-cas`, `@uncaged/workflow-reactor`, `@uncaged/workflow-register`
- **npm:** `yaml`
- **Peer:** `zod` ^4
`@uncaged/workflow-reactor` is used for LLM-backed extract and supervisor flows (`extract-fn.ts`, `supervisor.ts`).
## Usage
```typescript
import { executeThread } from "@uncaged/workflow-execute";
// Typical callers are CLI/tests that supply ExecuteThreadIo (paths, CAS, abort, logger, …).
```
+29
View File
@@ -0,0 +1,29 @@
# @uncaged/workflow-protocol
Shared workflow types, sentinel constants, and `Result` helpers.
## What This Package Does
It defines the cross-package contract for bundles and the engine: thread/step shapes, `WorkflowFn`, agent/extract contexts, descriptor types, and `CasStore` as an interface. Implementations (CAS store, CLI, extract) depend on these types so bundles stay decoupled from Node APIs.
## Key Exports
From `src/index.ts`:
- **Types:** `Result`, `CasStore`, `WorkflowRoleSchema`, `WorkflowRoleDescriptor`, `WorkflowDescriptor`, `RoleMeta`, `RoleOutput`, `StartStep`, `RoleStep`, `ThreadContext`, `ModeratorContext`, `AgentContext`, `ExtractContext`, `WorkflowCompletion`, `WorkflowResult`, `LlmProvider`, `ProviderConfig`, `ResolvedModel`, `WorkflowConfig`, `ExtractFn`, `AgentFn`, `AgentBinding`, `WorkflowRuntime`, `WorkflowFn`, `RoleDefinition`, `Moderator`, `WorkflowDefinition`, `AdvanceOutcome`
- **Constants:** `START`, `END`
- **Functions:** `ok`, `err`
## Dependencies
- **Peer:** `zod` ^4 — used in type positions for schemas (`ExtractFn`, `RoleDefinition`, etc.)
No workspace packages; this is the bottom layer.
## Usage
```typescript
import { END, START, type WorkflowFn, type ThreadContext } from "@uncaged/workflow-protocol";
```
Concrete `WorkflowFn` implementations are built with `@uncaged/workflow-runtime` (`createWorkflow`).
+26
View File
@@ -0,0 +1,26 @@
# @uncaged/workflow-reactor
LLM calling abstraction and thread “reactor” for structured tool invocation.
## What This Package Does
It exposes `createLlmFn` (chat completion wrapper) and `createThreadReactor` (multi-turn tool loop configuration) plus supporting message/tool types. `@uncaged/workflow-execute` consumes this for extractor and supervisor paths that talk to OpenAI-style APIs with tools.
## Key Exports
From `src/index.ts`:
- **Functions:** `createLlmFn`, `createThreadReactor`
- **Types:** `ChatMessage`, `LlmFn`, `StructuredToolSpec`, `ThreadReactorConfig`, `ThreadReactorFn`, `ThreadReactorInvokeArgs`, `ToolCall`, `ToolDefinition`
## Dependencies
- **Workspace:** `@uncaged/workflow-protocol`
- **Peer:** `zod` ^4
## Usage
```typescript
import { createLlmFn, createThreadReactor } from "@uncaged/workflow-reactor";
// Usually composed inside @uncaged/workflow-execute rather than directly by applications.
```
+38
View File
@@ -0,0 +1,38 @@
# @uncaged/workflow-register
Bundle validation, dynamic export extraction, registry YAML, and model/provider resolution.
## What This Package Does
It validates workflow `.esm.js` bundles, extracts `descriptor` / `run` exports at runtime, reads and writes `workflow.yaml`, and resolves which LLM endpoint/model to use from registry config (`resolveModel`, `splitProviderModelRef`).
## Key Exports
From `src/index.ts`:
- **Bundle:** `buildDescriptor`, `importWorkflowBundleModule`, `validateWorkflowBundle`, `ensureUncagedWorkflowSymlink`, `extractBundleExports`, `stringifyWorkflowDescriptor`, `validateWorkflowDescriptor`
- **Bundle types:** `ExtractBundleExportsOptions`, `ExtractedBundleExports`, `WorkflowBundleValidationInput`, `WorkflowDescriptor`, `WorkflowRoleDescriptor`, `WorkflowRoleSchema`
- **Registry:** `getRegisteredWorkflow`, `listRegisteredWorkflowNames`, `parseWorkflowRegistryYaml`, `readWorkflowRegistry`, `registerWorkflowVersion`, `rollbackWorkflowToHistoryHash`, `stringifyWorkflowRegistryYaml`, `unregisterWorkflow`, `workflowRegistryPath`, `writeWorkflowRegistry`
- **Registry types:** `WorkflowConfig`, `WorkflowHistoryEntry`, `WorkflowRegistryEntry`, `WorkflowRegistryFile`
- **Config:** `resolveModel`, `splitProviderModelRef`, types `ProviderConfig`, `ResolvedModel`
## Dependencies
- **Workspace:** `@uncaged/workflow-protocol`, `@uncaged/workflow-util`
- **Peer:** `acorn`, `yaml`, `zod` ^4 — parsing/validation at runtime for consumers
## Usage
```typescript
import { readFile } from "node:fs/promises";
import { readWorkflowRegistry, validateWorkflowBundle } from "@uncaged/workflow-register";
import { getDefaultWorkflowStorageRoot } from "@uncaged/workflow-util";
const reg = await readWorkflowRegistry(getDefaultWorkflowStorageRoot());
if (!reg.ok) throw new Error(reg.error.message);
const path = "./my.esm.js";
const source = await readFile(path, "utf8");
const v = validateWorkflowBundle({ filePath: path, source });
if (!v.ok) throw new Error(v.error);
```
+28
View File
@@ -0,0 +1,28 @@
# @uncaged/workflow-runtime
Workflow author API: `createWorkflow` plus re-exports of protocol workflow types.
## What This Package Does
Bundle code imports `createWorkflow` to turn a `WorkflowDefinition` plus `AgentBinding` into a `WorkflowFn` generator. It re-exports the protocol types and constants most authors need so workflows rarely import `@uncaged/workflow-protocol` directly.
## Key Exports
From `src/index.ts`:
- **Functions:** `createWorkflow`, `ok`, `err`
- **Types:** `AgentBinding`, `AgentContext`, `AgentFn`, `CasStore`, `ExtractContext`, `ExtractFn`, `LlmProvider`, `Moderator`, `ModeratorContext`, `Result`, `RoleDefinition`, `RoleMeta`, `RoleOutput`, `RoleStep`, `StartStep`, `ThreadContext`, `WorkflowCompletion`, `WorkflowDefinition`, `WorkflowDescriptor`, `WorkflowFn`, `WorkflowResult`, `WorkflowRoleDescriptor`, `WorkflowRoleSchema`, `WorkflowRuntime`
- **Constants:** `END`, `START`
## Dependencies
- **Workspace:** `@uncaged/workflow-protocol` — contract types and helpers
- **Peer:** `zod` ^4 — matches schema usage on role definitions
## Usage
```typescript
import { createWorkflow, type WorkflowDefinition, type AgentBinding } from "@uncaged/workflow-runtime";
export const run = createWorkflow(myDefinition, myBinding);
```
+32
View File
@@ -0,0 +1,32 @@
# @uncaged/workflow-util
Shared utilities: encoding, IDs, logging, storage paths, and ref-field normalization.
## What This Package Does
It provides filesystem-safe Base32 and ULID generation, the structured logger used across packages, helpers for the default workflow data directory and global CAS path, and utilities to merge/normalize `refs` on steps. It re-exports `ok`/`err` from protocol for convenience.
## Key Exports
From `src/index.ts`:
- **Base32:** `CROCKFORD_BASE32_ALPHABET`, `decodeCrockfordBase32Bits`, `decodeCrockfordToUint64`, `encodeCrockfordBase32Bits`, `encodeUint64AsCrockford`
- **Logger:** `createLogger`
- **Refs:** `mergeRefsWithContentHash`, `normalizeRefsField`
- **Result:** `ok`, `err` (from `@uncaged/workflow-protocol`)
- **Paths:** `getDefaultWorkflowStorageRoot`, `getGlobalCasDir`
- **ULID:** `generateUlid`
- **Types:** `CreateLoggerOptions`, `LogFn`, `LoggerSink`, `Result`
## Dependencies
- **Workspace:** `@uncaged/workflow-protocol``Result` and shared types used by helpers
## Usage
```typescript
import { createLogger, getDefaultWorkflowStorageRoot, generateUlid } from "@uncaged/workflow-util";
const log = createLogger();
log("4KNMR2PX", "example");
```