Compare commits
111 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| cd0a79d72b | |||
| 54631c43c7 | |||
| 655b57c4b5 | |||
| 7faa8184ae | |||
| 816137315e | |||
| 9a111d16c7 | |||
| ea6ceafe51 | |||
| d0dc7b5a19 | |||
| 3b81521e9d | |||
| aa0a23293f | |||
| 187dd036e5 | |||
| 4b45f4e6d1 | |||
| 2a6bce4918 | |||
| 3d6399c0e3 | |||
| b9258f84a5 | |||
| 638329a562 | |||
| 1a06e014f5 | |||
| d5d05334f5 | |||
| 844f5438fe | |||
| e329d74ec0 | |||
| f90614a622 | |||
| 68af555313 | |||
| 025695dbe9 | |||
| 96584e481f | |||
| 766ec7ddc2 | |||
| aeb7180e9d | |||
| 9b56f7b75e | |||
| c9507b8dc1 | |||
| baa2edfa38 | |||
| 4dff320d5c | |||
| d8863ceda2 | |||
| c9fcb15384 | |||
| 5e868a2977 | |||
| 76fab22827 | |||
| 176844d7f5 | |||
| 31695e89a8 | |||
| 669875fb46 | |||
| 6d94be34a9 | |||
| d95fe45a3d | |||
| b9252b5ce2 | |||
| 4d47effd39 | |||
| 7b93ce8f3e | |||
| 67870392ab | |||
| 9316b843f6 | |||
| 6b9ff9781d | |||
| 487c48effa | |||
| 4eca2d533c | |||
| f0f840e6e0 | |||
| 7ff90cef4f | |||
| e62d51d845 | |||
| a803fcb4fc | |||
| d00c93fc19 | |||
| 99a2890be2 | |||
| 3b7d0564bb | |||
| 45dacf540b | |||
| 2eb5ee0666 | |||
| e67932c83c | |||
| 04a12231c3 | |||
| e5ae9a134c | |||
| bdafaf3aa1 | |||
| 02f7f0b708 | |||
| 8ea554bb5e | |||
| 8a425521da | |||
| f174f2fd0a | |||
| 355594d074 | |||
| fd7609fe90 | |||
| dacecfbbb7 | |||
| 3238eaeddf | |||
| 995f273fa5 | |||
| 866154ad73 | |||
| 8efc5050cb | |||
| 3fb60ee649 | |||
| e181f67a2d | |||
| a3114bf840 | |||
| e59ae9aca1 | |||
| c050a38f38 | |||
| c60c310074 | |||
| fe035c065d | |||
| 192ad656a4 | |||
| c0c8d6499e | |||
| 505f85e3c4 | |||
| fc7d482b4f | |||
| f9979c3c89 | |||
| 46def2945a | |||
| 4e89508246 | |||
| 77d799d458 | |||
| 6c14259184 | |||
| 7b9cb6a9c8 | |||
| 68246e20b1 | |||
| d63d58ccb5 | |||
| 2a3a40b9d9 | |||
| 762ecec872 | |||
| c0ac4ade09 | |||
| a991393053 | |||
| 892ccab8d5 | |||
| 70c83c65b0 | |||
| 8a7e756fe3 | |||
| 4a4ddba9f6 | |||
| d5f47d1a18 | |||
| 37c35560e9 | |||
| f174b96028 | |||
| 43978360ff | |||
| 432400ee20 | |||
| dacebe1841 | |||
| c42125946d | |||
| 4c9ce72395 | |||
| 8b43f7993b | |||
| cf9e2cd3d6 | |||
| 7a99c1a9d6 | |||
| 546237db85 | |||
| 1ed7e32067 |
@@ -1,40 +0,0 @@
|
|||||||
# ──────────────────────────────────────────────
|
|
||||||
# Workflow Engine — Environment Variables
|
|
||||||
# ──────────────────────────────────────────────
|
|
||||||
# Copy this file to .env and fill in the values.
|
|
||||||
|
|
||||||
# ── Cursor Agent ──
|
|
||||||
|
|
||||||
# CLI command to invoke the Cursor agent (required for develop workflow)
|
|
||||||
WORKFLOW_CURSOR_COMMAND=
|
|
||||||
|
|
||||||
# Model override for Cursor agent
|
|
||||||
WORKFLOW_CURSOR_MODEL=
|
|
||||||
|
|
||||||
# Timeout in milliseconds for Cursor agent operations
|
|
||||||
WORKFLOW_CURSOR_TIMEOUT=
|
|
||||||
|
|
||||||
# ── Hermes Agent (used by develop tester/committer + solve-issue) ──
|
|
||||||
|
|
||||||
# CLI command to invoke the Hermes agent (absolute path required)
|
|
||||||
WORKFLOW_HERMES_COMMAND=
|
|
||||||
|
|
||||||
# Model override for Hermes agent
|
|
||||||
WORKFLOW_HERMES_MODEL=
|
|
||||||
|
|
||||||
# Timeout in milliseconds for Hermes agent operations
|
|
||||||
WORKFLOW_HERMES_TIMEOUT=
|
|
||||||
|
|
||||||
# ── Storage ──
|
|
||||||
|
|
||||||
# Override the workflow storage root directory
|
|
||||||
# Default: ~/.uncaged/workflow
|
|
||||||
WORKFLOW_STORAGE_ROOT=
|
|
||||||
|
|
||||||
# Gateway secret for the serve command
|
|
||||||
WORKFLOW_DASHBOARD_SECRET=
|
|
||||||
|
|
||||||
# ── Display ──
|
|
||||||
|
|
||||||
# Set to any value to disable colored output
|
|
||||||
# NO_COLOR=1
|
|
||||||
@@ -10,3 +10,6 @@ xiaoju/
|
|||||||
solve-issue-entry.ts
|
solve-issue-entry.ts
|
||||||
packages/workflow-template-develop/develop.esm.js
|
packages/workflow-template-develop/develop.esm.js
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
*.py
|
||||||
|
.claude
|
||||||
|
tmp
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
# Test Spec: uwf setup model connectivity validation (#335)
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
File: `packages/cli-workflow/src/commands/setup.ts`
|
||||||
|
Test file: `packages/cli-workflow/src/__tests__/setup-validate.test.ts`
|
||||||
|
|
||||||
|
After `cmdSetup` writes config, it should send a test chat completion request to verify the configured model is reachable. If validation fails, warn the user (don't abort — config is already saved).
|
||||||
|
|
||||||
|
## Implementation Notes
|
||||||
|
|
||||||
|
- Add a `validateModel(baseUrl, apiKey, model)` function that sends a minimal chat completion request (`POST /chat/completions` with `messages: [{role:"user",content:"hi"}]`, `max_tokens: 1`)
|
||||||
|
- Returns `Result<void, string>` — ok if 2xx response, error with reason string otherwise
|
||||||
|
- Use `AbortSignal.timeout(15_000)` for the request
|
||||||
|
- Both `cmdSetup` and `cmdSetupInteractive` should call it after saving config
|
||||||
|
- `cmdSetup` returns validation result in its return object: `{ ...existing, validation: { ok: true } | { ok: false, error: string } }`
|
||||||
|
- `cmdSetupInteractive` prints a warning to console if validation fails, success message if it passes
|
||||||
|
- Use the project logger (`createLogger`) — no raw `console.log` except in interactive CLI output (per CLAUDE.md)
|
||||||
|
|
||||||
|
## Test Cases (vitest)
|
||||||
|
|
||||||
|
### 1. `validateModel` — success path
|
||||||
|
- Mock `fetch` to return `{ status: 200, ok: true, json: () => ({}) }`
|
||||||
|
- Call `validateModel(baseUrl, apiKey, model)`
|
||||||
|
- Assert returns `{ ok: true, value: undefined }`
|
||||||
|
- Assert fetch was called with correct URL (`${baseUrl}/chat/completions`), correct headers (`Authorization: Bearer ${apiKey}`), correct body (model, messages, max_tokens: 1)
|
||||||
|
|
||||||
|
### 2. `validateModel` — HTTP error (401 unauthorized)
|
||||||
|
- Mock `fetch` to return `{ status: 401, ok: false, statusText: "Unauthorized" }`
|
||||||
|
- Call `validateModel(baseUrl, apiKey, model)`
|
||||||
|
- Assert returns `{ ok: false, error: <string containing "401"> }`
|
||||||
|
|
||||||
|
### 3. `validateModel` — HTTP error (404 model not found)
|
||||||
|
- Mock `fetch` to return `{ status: 404, ok: false, statusText: "Not Found" }`
|
||||||
|
- Assert returns `{ ok: false, error: <string containing "404"> }`
|
||||||
|
|
||||||
|
### 4. `validateModel` — network timeout
|
||||||
|
- Mock `fetch` to throw `DOMException` with name `AbortError`
|
||||||
|
- Assert returns `{ ok: false, error: <string containing "timeout" or "unreachable"> }`
|
||||||
|
|
||||||
|
### 5. `validateModel` — network error (DNS failure, connection refused)
|
||||||
|
- Mock `fetch` to throw `TypeError("fetch failed")`
|
||||||
|
- Assert returns `{ ok: false, error: <string mentioning connectivity> }`
|
||||||
|
|
||||||
|
### 6. `cmdSetup` — includes validation result on success
|
||||||
|
- Mock global `fetch` for `/chat/completions` to succeed
|
||||||
|
- Call `cmdSetup({ provider, baseUrl, apiKey, model, storageRoot })`
|
||||||
|
- Assert returned object has `validation: { ok: true, value: undefined }`
|
||||||
|
- Assert config files are still written (existing behavior preserved)
|
||||||
|
|
||||||
|
### 7. `cmdSetup` — includes validation result on failure (config still saved)
|
||||||
|
- Mock global `fetch` for `/chat/completions` to return 401
|
||||||
|
- Call `cmdSetup({ ... })`
|
||||||
|
- Assert returned object has `validation: { ok: false, error: ... }`
|
||||||
|
- Assert `config.yaml` and `.env` are still written (validation failure doesn't prevent saving)
|
||||||
|
|
||||||
|
### 8. `cmdSetupInteractive` — prints success message on validation pass
|
||||||
|
- Mock `fetch` for both `/models` and `/chat/completions` to succeed
|
||||||
|
- Mock stdin to provide valid selections
|
||||||
|
- Capture console output
|
||||||
|
- Assert output contains a success message like "Model verified" or "✓"
|
||||||
|
|
||||||
|
### 9. `cmdSetupInteractive` — prints warning on validation failure
|
||||||
|
- Mock `fetch`: `/models` succeeds, `/chat/completions` returns 401
|
||||||
|
- Mock stdin for valid selections
|
||||||
|
- Capture console output
|
||||||
|
- Assert output contains a warning about model not being reachable and suggests trying a different model
|
||||||
|
|
||||||
|
### 10. `validateModel` — request body correctness
|
||||||
|
- Mock `fetch` to capture the request body
|
||||||
|
- Call `validateModel(baseUrl, apiKey, "test-model")`
|
||||||
|
- Assert body is `{ model: "test-model", messages: [{role: "user", content: "hi"}], max_tokens: 1 }`
|
||||||
|
|
||||||
|
## Export Requirements
|
||||||
|
|
||||||
|
- `validateModel` must be exported (for direct unit testing)
|
||||||
|
- Signature: `async function validateModel(baseUrl: string, apiKey: string, model: string): Promise<Result<void, string>>`
|
||||||
|
- `Result` type: `{ ok: true; value: T } | { ok: false; error: E }` (project convention)
|
||||||
|
|
||||||
|
## Files to Create/Modify
|
||||||
|
|
||||||
|
- **New**: `packages/cli-workflow/src/__tests__/setup-validate.test.ts` — all test cases above
|
||||||
|
- **Modify**: `packages/cli-workflow/src/commands/setup.ts` — add `validateModel`, integrate into `cmdSetup` and `cmdSetupInteractive`
|
||||||
@@ -0,0 +1,196 @@
|
|||||||
|
name: "solve-issue"
|
||||||
|
description: "TDD-driven issue resolution for small, focused changes. Loop protection relies on engine maxRounds."
|
||||||
|
roles:
|
||||||
|
planner:
|
||||||
|
description: "Analyzes issue and outputs a TDD test spec"
|
||||||
|
goal: "You are a planning agent. You analyze Gitea issues and produce a TDD test specification that downstream roles will implement and verify."
|
||||||
|
capabilities:
|
||||||
|
- issue-analysis
|
||||||
|
- planning
|
||||||
|
procedure: |
|
||||||
|
On first run (no previous steps):
|
||||||
|
1. Read the issue and all comments from Gitea using `tea issues <number> -r <owner/repo>`
|
||||||
|
2. Read CLAUDE.md (or equivalent project conventions file) to understand coding standards
|
||||||
|
3. Assess whether the issue has enough information to produce a test spec
|
||||||
|
4. If insufficient info: comment on the issue via `echo "..." | tea comment <number> -r <owner/repo>` (skip if you already commented), then output status=insufficient_info and terminate
|
||||||
|
5. If sufficient: produce a detailed TDD test spec in markdown covering all scenarios
|
||||||
|
|
||||||
|
On subsequent runs (bounced back by tester with fix_spec):
|
||||||
|
1. Read the tester's output from the previous step to understand what's wrong with the spec
|
||||||
|
2. Revise the test spec accordingly
|
||||||
|
|
||||||
|
After producing the test spec:
|
||||||
|
1. Store it via `uwf cas put-text "<markdown content>"` and capture the returned hash
|
||||||
|
2. Put the hash in frontmatter.plan (required when status=ready)
|
||||||
|
output: "Output a brief summary of the test spec. Frontmatter must include: status (ready or insufficient_info) and plan (CAS hash of the test spec, required when status=ready)."
|
||||||
|
frontmatter:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
status:
|
||||||
|
type: string
|
||||||
|
enum: [ready, insufficient_info]
|
||||||
|
plan:
|
||||||
|
type: string
|
||||||
|
required: [status]
|
||||||
|
developer:
|
||||||
|
description: "TDD implementation per test spec"
|
||||||
|
goal: "You are a developer agent. You implement code changes following TDD — write tests first, then implementation."
|
||||||
|
capabilities:
|
||||||
|
- coding
|
||||||
|
procedure: |
|
||||||
|
Before starting any work, ensure a clean worktree:
|
||||||
|
1. `git checkout main && git pull` to get the latest code
|
||||||
|
2. `git checkout -b fix/<issue-number>-<short-description>` to create a fresh branch
|
||||||
|
- If bounced back from reviewer or tester, reuse the existing branch instead
|
||||||
|
|
||||||
|
Then implement TDD:
|
||||||
|
3. Read the test spec from CAS: `uwf cas get <plan hash>` (find the hash from the latest planner step's frontmatter.plan)
|
||||||
|
4. If bounced back from reviewer or tester: read the previous role's output to understand what needs fixing
|
||||||
|
5. Write tests first based on the spec
|
||||||
|
6. Implement the code to make tests pass
|
||||||
|
7. Ensure `bun run build` passes with no errors
|
||||||
|
8. Run `bun test` to verify all tests pass
|
||||||
|
output: "List all files changed and provide a summary. Frontmatter must include: status (done or failed)."
|
||||||
|
frontmatter:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
status:
|
||||||
|
type: string
|
||||||
|
enum: [done, failed]
|
||||||
|
required: [status]
|
||||||
|
reviewer:
|
||||||
|
description: "Code standards compliance check"
|
||||||
|
goal: "You are a code reviewer. You verify code standards compliance — NOT functionality (that's the tester's job)."
|
||||||
|
capabilities:
|
||||||
|
- code-review
|
||||||
|
- static-analysis
|
||||||
|
procedure: |
|
||||||
|
Before reviewing, verify the git branch:
|
||||||
|
1. Run `git branch --show-current` — confirm the branch name references the issue number being worked on
|
||||||
|
2. If the branch doesn't correspond to the issue, flag it in your output and reject
|
||||||
|
|
||||||
|
Then perform code review:
|
||||||
|
Hard checks (must all pass):
|
||||||
|
3. `bun run build` — no build errors
|
||||||
|
4. `bunx biome check` — no lint violations
|
||||||
|
5. TypeScript strict mode — no type errors
|
||||||
|
|
||||||
|
Soft checks (review against CLAUDE.md conventions):
|
||||||
|
- Functional-first: `function` + `type`, not `class` + `interface`
|
||||||
|
- No optional properties (`?:`) — use `T | null`
|
||||||
|
- Naming conventions (kebab-case files, PascalCase types, camelCase functions)
|
||||||
|
- Module boundary discipline (folder exports via index.ts)
|
||||||
|
- No `console.log` (use structured logger)
|
||||||
|
- No dynamic imports in production code
|
||||||
|
|
||||||
|
Only review standards compliance. Do NOT test functionality.
|
||||||
|
If rejecting, you MUST explain the specific reason in your output.
|
||||||
|
output: "Explain your decision with specific file/line references. Frontmatter must include: approved (true or false)."
|
||||||
|
frontmatter:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
approved:
|
||||||
|
type: boolean
|
||||||
|
required: [approved]
|
||||||
|
tester:
|
||||||
|
description: "Functional correctness verification"
|
||||||
|
goal: "You are a tester agent. You verify that the implementation correctly satisfies every scenario in the test spec."
|
||||||
|
capabilities:
|
||||||
|
- testing
|
||||||
|
procedure: |
|
||||||
|
1. Run `bun test` for automated test verification
|
||||||
|
2. Read the test spec from CAS: `uwf cas get <plan hash>` (find the hash from the latest planner step's frontmatter.plan)
|
||||||
|
3. Verify each scenario in the spec is covered and passing
|
||||||
|
4. Determine outcome:
|
||||||
|
- passed: all scenarios verified, tests pass
|
||||||
|
- fix_code: tests fail or implementation doesn't match spec → send back to developer
|
||||||
|
- fix_spec: the spec itself is wrong or incomplete → send back to planner
|
||||||
|
output: "Report test results per scenario. Frontmatter must include: status (passed, fix_code, or fix_spec)."
|
||||||
|
frontmatter:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
status:
|
||||||
|
type: string
|
||||||
|
enum: [passed, fix_code, fix_spec]
|
||||||
|
required: [status]
|
||||||
|
committer:
|
||||||
|
description: "Commits and creates PR"
|
||||||
|
goal: "You are a committer agent. You create a clean commit and push a PR linking the original issue."
|
||||||
|
capabilities: []
|
||||||
|
procedure: |
|
||||||
|
Note: You inherit the developer's worktree and branch. Do NOT create a new branch.
|
||||||
|
1. Stage all changes: `git add -A`
|
||||||
|
2. Commit with a descriptive message referencing the issue: `git commit -m "type: description\n\nFixes #N"`
|
||||||
|
3. Push the branch: `git push -u origin <branch-name>`
|
||||||
|
- If push hook fails: capture the error log in your output, mark hook_failed
|
||||||
|
4. On push success: create a PR via `tea pr create --title "..." --description "..."`
|
||||||
|
- PR description must follow the project template: What / Why / Changes / Ref sections, with `Fixes #N` in Ref
|
||||||
|
output: "Include PR URL on success or error log on failure. Frontmatter must include: success (true or false)."
|
||||||
|
frontmatter:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
success:
|
||||||
|
type: boolean
|
||||||
|
required: [success]
|
||||||
|
conditions:
|
||||||
|
insufficientInfo:
|
||||||
|
description: "Planner determined there's not enough info to proceed"
|
||||||
|
expression: "$last('planner').status = 'insufficient_info'"
|
||||||
|
devFailed:
|
||||||
|
description: "Developer failed to implement"
|
||||||
|
expression: "$last('developer').status = 'failed'"
|
||||||
|
rejected:
|
||||||
|
description: "Reviewer rejected the implementation"
|
||||||
|
expression: "$last('reviewer').approved = false"
|
||||||
|
fixCode:
|
||||||
|
description: "Tester found code issues"
|
||||||
|
expression: "$last('tester').status = 'fix_code'"
|
||||||
|
fixSpec:
|
||||||
|
description: "Tester found spec issues"
|
||||||
|
expression: "$last('tester').status = 'fix_spec'"
|
||||||
|
hookFailed:
|
||||||
|
description: "Push hook failed"
|
||||||
|
expression: "$last('committer').success = false"
|
||||||
|
graph:
|
||||||
|
$START:
|
||||||
|
- role: "planner"
|
||||||
|
condition: null
|
||||||
|
prompt: "Analyze the issue and produce an implementation plan."
|
||||||
|
planner:
|
||||||
|
- role: "$END"
|
||||||
|
condition: "insufficientInfo"
|
||||||
|
prompt: "Insufficient information to proceed; end the workflow."
|
||||||
|
- role: "developer"
|
||||||
|
condition: null
|
||||||
|
prompt: "Implement the plan from the planner."
|
||||||
|
developer:
|
||||||
|
- role: "$END"
|
||||||
|
condition: "devFailed"
|
||||||
|
prompt: "Development failed; end the workflow."
|
||||||
|
- role: "reviewer"
|
||||||
|
condition: null
|
||||||
|
prompt: "Send the implementation to the reviewer."
|
||||||
|
reviewer:
|
||||||
|
- role: "developer"
|
||||||
|
condition: "rejected"
|
||||||
|
prompt: "Reviewer rejected the implementation; fix the issues."
|
||||||
|
- role: "tester"
|
||||||
|
condition: null
|
||||||
|
prompt: "Review passed; run tests on the implementation."
|
||||||
|
tester:
|
||||||
|
- role: "developer"
|
||||||
|
condition: "fixCode"
|
||||||
|
prompt: "Tests found code issues; return to developer."
|
||||||
|
- role: "planner"
|
||||||
|
condition: "fixSpec"
|
||||||
|
prompt: "Tests found spec issues; return to planner."
|
||||||
|
- role: "committer"
|
||||||
|
condition: null
|
||||||
|
prompt: "Tests passed; commit and push the changes."
|
||||||
|
committer:
|
||||||
|
- role: "developer"
|
||||||
|
condition: "hookFailed"
|
||||||
|
prompt: "Push hook failed; return to developer to fix."
|
||||||
|
- role: "$END"
|
||||||
|
condition: null
|
||||||
|
prompt: "Commit succeeded; complete the workflow."
|
||||||
@@ -2,46 +2,41 @@
|
|||||||
|
|
||||||
## Project Overview
|
## Project Overview
|
||||||
|
|
||||||
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`.
|
This monorepo implements a stateless workflow engine driven by a single-step CLI (`uwf`). Workflows are **YAML definitions** stored as CAS nodes; threads are immutable chains of CAS-linked step nodes. No daemon — each `uwf thread step` invocation runs one moderator→agent→extract cycle and exits.
|
||||||
|
|
||||||
### Key Terms
|
### Key Terms
|
||||||
|
|
||||||
| Concept | What it is |
|
| Concept | What it is |
|
||||||
|---------|-----------|
|
|---------|-----------|
|
||||||
| **Workflow** | A single-file ESM module that exports `run` (workflow function) and `descriptor` (metadata). Identified by its XXH64 hash (Crockford Base32). |
|
| **Workflow** | A YAML definition (`WorkflowPayload`) with roles, conditions, and a routing graph. Stored as a CAS node, identified by its XXH64 hash. |
|
||||||
| **Bundle** | The physical `.esm.js` file stored in `~/.uncaged/workflow/bundles/`. |
|
| **Thread** | A single execution of a workflow, identified by a ULID. State is an immutable CAS chain; active threads indexed in `threads.yaml`; completed threads in `history.jsonl`. |
|
||||||
| **Thread** | A single execution of a workflow, identified by a ULID. State lives in CAS (linked nodes); active threads indexed in `threads.json`; completed rows in `history/*.jsonl`. Debug logs use `.info.jsonl`. |
|
| **Role** | A named actor within a workflow. Each role has a system prompt and a JSON Schema `outputSchema`. |
|
||||||
| **Role** | A named actor within a workflow. Each role produces output with typed `meta`. |
|
| **Moderator** | JSONata-based graph evaluator — determines the next role (or `$END`) with zero LLM cost. |
|
||||||
| **Registry** | `workflow.yaml` — maps workflow names to current/historical bundle hashes. |
|
| **Agent** | An external CLI command (`uwf-hermes`, etc.) spawned by `uwf thread step`. Produces frontmatter markdown output. |
|
||||||
|
| **CAS** | Content-Addressed Storage via `@uncaged/json-cas` — all workflow definitions, thread nodes, and outputs are immutable CAS nodes. |
|
||||||
|
| **Registry** | `~/.uncaged/workflow/registry.yaml` — maps workflow names to current CAS hashes. |
|
||||||
|
|
||||||
### Monorepo Structure
|
### Monorepo Structure
|
||||||
|
|
||||||
```
|
```
|
||||||
workflow/
|
workflow/
|
||||||
packages/
|
packages/
|
||||||
workflow-protocol/ # @uncaged/workflow-protocol — shared types + Result
|
workflow-protocol/ # @uncaged/workflow-protocol — shared types (WorkflowPayload, StepNodePayload, WorkflowConfig, etc.)
|
||||||
workflow-runtime/ # @uncaged/workflow-runtime — createWorkflow, type re-exports
|
workflow-util/ # @uncaged/workflow-util — Crockford Base32, ULID, logger, frontmatter parsing/validation
|
||||||
workflow-util/ # @uncaged/workflow-util — Base32, ULID, logger, storage paths, refs helpers
|
workflow-moderator/ # @uncaged/workflow-moderator — JSONata graph evaluator
|
||||||
workflow-reactor/ # @uncaged/workflow-reactor — LLM fn + thread reactor (tool calls)
|
workflow-agent-kit/ # @uncaged/workflow-agent-kit — createAgent factory, context builder, extract pipeline
|
||||||
workflow-cas/ # @uncaged/workflow-cas — CAS store, hash, Merkle
|
workflow-agent-hermes/ # @uncaged/workflow-agent-hermes — uwf-hermes CLI binary (spawns hermes chat)
|
||||||
workflow-register/ # @uncaged/workflow-register — bundle validation, registry YAML, model resolution
|
cli-workflow/ # @uncaged/cli-workflow — uwf CLI binary
|
||||||
workflow-execute/ # @uncaged/workflow-execute — engine, extract, fork, GC, workflowAsAgent
|
legacy-packages/ # Archived packages (preserved for reference, not active)
|
||||||
cli-workflow/ # @uncaged/cli-workflow — uncaged-workflow CLI
|
examples/ # Workflow YAML examples (solve-issue.yaml)
|
||||||
workflow-agent-cursor/ # @uncaged/workflow-agent-cursor
|
docs/ # Architecture docs
|
||||||
workflow-agent-hermes/ # @uncaged/workflow-agent-hermes
|
biome.json # root Biome config
|
||||||
workflow-agent-llm/ # @uncaged/workflow-agent-llm
|
tsconfig.json # root TypeScript config
|
||||||
workflow-agent-react/ # @uncaged/workflow-agent-react
|
|
||||||
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
|
|
||||||
biome.json # root Biome config
|
|
||||||
tsconfig.json # root TypeScript config
|
|
||||||
```
|
```
|
||||||
|
|
||||||
- Execution stack layers: `workflow-protocol` → (`workflow-runtime`, `workflow-util`, `workflow-reactor`) → (`workflow-cas`, `workflow-register`) → `workflow-execute` → `cli-workflow`
|
- Dependency layers: `workflow-protocol` → (`workflow-util`, `workflow-moderator`) → `workflow-agent-kit` → `workflow-agent-hermes` / `cli-workflow`
|
||||||
- Packages use `workspace:^` protocol (resolves to `^x.y.z` on publish)
|
- Packages use `workspace:^` protocol (resolves to `^x.y.z` on publish)
|
||||||
|
- External CAS: `@uncaged/json-cas` (store API, hashing, schema validation) + `@uncaged/json-cas-fs` (filesystem backend)
|
||||||
|
|
||||||
## Language & Paradigm
|
## Language & Paradigm
|
||||||
|
|
||||||
@@ -109,8 +104,6 @@ type WorkflowEntry = {
|
|||||||
- Always named exports, never default exports
|
- Always named exports, never default exports
|
||||||
- One module = one responsibility, filename = purpose
|
- One module = one responsibility, filename = purpose
|
||||||
|
|
||||||
Workflow bundles (`.esm.js`) follow the same rule: export `const run` and `const descriptor`, not `export default`.
|
|
||||||
|
|
||||||
### Folder Module Discipline
|
### Folder Module Discipline
|
||||||
|
|
||||||
Every folder under `src/` is a **module boundary**. Four rules:
|
Every folder under `src/` is a **module boundary**. Four rules:
|
||||||
@@ -136,10 +129,10 @@ export { createCasStore } from "../cas/cas.js";
|
|||||||
|
|
||||||
// ❌ Bad — types defined in index.ts
|
// ❌ Bad — types defined in index.ts
|
||||||
// in cas/index.ts:
|
// in cas/index.ts:
|
||||||
export type CasStore = { ... }; // should be in cas/types.ts
|
export type CasStore = { ... }; // should be in cas/types.ts
|
||||||
```
|
```
|
||||||
|
|
||||||
**Exception**: The package-level `src/index.ts` is the public API surface and re-exports from folder `index.ts` files. Files that remain at `src/` root (e.g. `types.ts`, `workflow-as-agent.ts`) are not inside a folder module and follow normal rules.
|
**Exception**: The package-level `src/index.ts` is the public API surface and re-exports from folder `index.ts` files. Files that remain at `src/` root (e.g. `types.ts`) are not inside a folder module and follow normal rules.
|
||||||
|
|
||||||
## Naming
|
## Naming
|
||||||
|
|
||||||
@@ -160,7 +153,7 @@ Workflow names use **verb-first** kebab-case:
|
|||||||
### ID Encoding
|
### ID Encoding
|
||||||
|
|
||||||
All IDs use **Crockford Base32**:
|
All IDs use **Crockford Base32**:
|
||||||
- Bundle hash: XXH64 → 13-char Crockford Base32
|
- CAS hash: XXH64 → 13-char Crockford Base32
|
||||||
- Thread ID: ULID → 26-char Crockford Base32 (10 timestamp + 16 random)
|
- Thread ID: ULID → 26-char Crockford Base32 (10 timestamp + 16 random)
|
||||||
|
|
||||||
## Error Handling
|
## Error Handling
|
||||||
@@ -189,7 +182,7 @@ import { createLogger } from "@uncaged/workflow-util";
|
|||||||
const log = createLogger();
|
const log = createLogger();
|
||||||
|
|
||||||
// Each call site has a fixed 8-char Crockford Base32 tag
|
// Each call site has a fixed 8-char Crockford Base32 tag
|
||||||
log("4KNMR2PX", "Loading workflow bundle...");
|
log("4KNMR2PX", "Loading workflow...");
|
||||||
log("7BQST3VW", `Role ${role} started`);
|
log("7BQST3VW", `Role ${role} started`);
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -204,7 +197,7 @@ log("7BQST3VW", `Role ${role} started`);
|
|||||||
|
|
||||||
### Why fixed tags?
|
### Why fixed tags?
|
||||||
|
|
||||||
- `grep "4KNMR2PX"` in `.info.jsonl` → instant code location
|
- `grep "4KNMR2PX"` in logs → instant code location
|
||||||
- No need for file/line info in the log — tag is the locator
|
- No need for file/line info in the log — tag is the locator
|
||||||
- Survives refactoring (tag stays the same when code moves)
|
- Survives refactoring (tag stays the same when code moves)
|
||||||
|
|
||||||
@@ -221,74 +214,76 @@ console.log(result);
|
|||||||
|
|
||||||
Do NOT use `await import()` in production code. Always use static top-level `import`.
|
Do NOT use `await import()` in production code. Always use static top-level `import`.
|
||||||
|
|
||||||
**Exception**: The bundle loader and `extractBundleExports` dynamically import user workflow files at runtime.
|
|
||||||
|
|
||||||
```ts
|
|
||||||
// Dynamic import required: user bundle path resolved at runtime
|
|
||||||
const mod = await import(bundlePath);
|
|
||||||
```
|
|
||||||
|
|
||||||
Test files (`__tests__/**`) are exempt.
|
Test files (`__tests__/**`) are exempt.
|
||||||
|
|
||||||
## Toolchain
|
## Toolchain
|
||||||
|
|
||||||
| Tool | Purpose |
|
| Tool | Purpose |
|
||||||
|------|---------|
|
|------|---------|
|
||||||
| **bun** | Package manager + runtime + test runner |
|
| **bun** | Package manager + runtime |
|
||||||
| **TypeScript** | Type checking (strict mode) |
|
| **TypeScript** | Type checking (strict mode) |
|
||||||
| **Biome** | Lint + format (replaces ESLint + Prettier) |
|
| **Biome** | Lint + format (replaces ESLint + Prettier) |
|
||||||
|
| **vitest** | Test runner (`cli-workflow` uses vitest; other packages use `bun test`) |
|
||||||
|
|
||||||
### Commands
|
### Development Workflow
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
bun run check # tsc --build + biome check
|
# ── Setup ──
|
||||||
bun run format # biome format --write
|
bun install # install all workspace dependencies
|
||||||
bun test # run tests
|
|
||||||
|
# ── Daily development ──
|
||||||
|
bun run build # tsc --build (all packages, dependency order)
|
||||||
|
bun run check # tsc --build + biome check + lint-log-tags
|
||||||
|
bun run format # biome format --write
|
||||||
|
bun test # run tests across all packages
|
||||||
|
|
||||||
|
# ── Before committing ──
|
||||||
|
bun run check # must pass — typecheck + lint + log tag validation
|
||||||
|
bun test # must pass — all package tests
|
||||||
```
|
```
|
||||||
|
|
||||||
### Version Management & Publishing
|
### Publishing
|
||||||
|
|
||||||
All public `@uncaged/*` packages are published to **npmjs.org** via `@changesets/cli` with **fixed mode** (all packages share the same version number). `workflow-dashboard` is private and excluded.
|
All public `@uncaged/*` packages are published to **npmjs.org** with **fixed mode** (all packages share the same version number).
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 1. After making changes, add a changeset describing the change
|
# 1. Add a changeset describing the change
|
||||||
bun changeset
|
bun changeset
|
||||||
|
|
||||||
# 2. Before release, bump all package versions + generate CHANGELOGs
|
# 2. Bump all package versions + generate CHANGELOGs
|
||||||
bun version
|
bun version
|
||||||
|
|
||||||
# 3. Build, test, and publish to npmjs
|
# 3. Build, test, and publish (runs scripts/publish-all.mjs)
|
||||||
bun release
|
bun release
|
||||||
|
|
||||||
|
# Or publish manually with a tag:
|
||||||
|
node scripts/publish-all.mjs --tag alpha
|
||||||
|
node scripts/publish-all.mjs --dry-run # preview without publishing
|
||||||
```
|
```
|
||||||
|
|
||||||
- `workspace:^` dependencies resolve to `^x.y.z` on publish
|
- `workspace:^` dependencies resolve to `^x.y.z` on publish
|
||||||
|
- Publish order defined in `scripts/publish-all.mjs` (dependency order)
|
||||||
- Changesets config: `.changeset/config.json` (fixed mode, public access)
|
- Changesets config: `.changeset/config.json` (fixed mode, public access)
|
||||||
- Each package has auto-generated `CHANGELOG.md`
|
|
||||||
|
|
||||||
### Consuming @uncaged/* Packages
|
### End-to-end: Author → Register → Run
|
||||||
|
|
||||||
External workflow repos just `bun install` — packages come from npmjs like any other dependency. No special registry config needed.
|
|
||||||
|
|
||||||
### End-to-end: Monorepo → Registry → Workspace → Bundle
|
|
||||||
|
|
||||||
```
|
```
|
||||||
workflow/ (monorepo) — engine, runtime, templates, agents
|
examples/solve-issue.yaml — write a workflow YAML definition
|
||||||
│ bun release — build + test + changeset publish
|
│ uwf workflow put
|
||||||
▼
|
▼
|
||||||
npmjs.org — @uncaged/* scoped packages (public)
|
~/.uncaged/workflow/cas/ — Workflow stored as CAS node
|
||||||
│ bun install
|
~/.uncaged/workflow/registry.yaml — name → hash mapping updated
|
||||||
|
│ uwf thread start <name> -p "..."
|
||||||
▼
|
▼
|
||||||
my-workflows/ (workspace) — normal package.json
|
~/.uncaged/workflow/threads.yaml — new thread head pointer
|
||||||
│ bun run build:develop — bun build → single .esm.js
|
│ uwf thread step <thread-id>
|
||||||
▼
|
▼
|
||||||
uncaged-workflow workflow add — register bundle locally
|
moderator → agent → extract — one step per invocation, repeat until $END
|
||||||
uncaged-workflow run — execute workflow
|
|
||||||
```
|
```
|
||||||
|
|
||||||
1. **Monorepo changes** → `bun changeset` (describe change) → `bun version` (bump) → `bun release` (publish)
|
1. **Author** — write a workflow YAML file with roles, conditions, and graph
|
||||||
2. **Workspace** → `bun install` fetches latest from npmjs
|
2. **Register** — `uwf workflow put <file.yaml>` parses YAML, registers output schemas, stores `WorkflowPayload` in CAS
|
||||||
3. **Build** → produces single-file ESM bundle with `@uncaged/*` as externals
|
3. **Run** — `uwf thread start` creates a thread, `uwf thread step` executes one cycle per invocation
|
||||||
4. **Register & Run** → `uncaged-workflow workflow add <name> <bundle>` then `uncaged-workflow run <name>`
|
|
||||||
|
|
||||||
## Commit Convention
|
## Commit Convention
|
||||||
|
|
||||||
@@ -296,5 +291,5 @@ uncaged-workflow run — execute workflow
|
|||||||
<type>(<scope>): <description>
|
<type>(<scope>): <description>
|
||||||
|
|
||||||
type: feat | fix | refactor | docs | chore | test
|
type: feat | fix | refactor | docs | chore | test
|
||||||
scope: workflow | cli | rfc-001 | ...
|
scope: workflow | cli | moderator | agent-kit | hermes | util | protocol | ...
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -1,71 +1,93 @@
|
|||||||
# @uncaged/workflow
|
# @uncaged/workflow
|
||||||
|
|
||||||
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).
|
A stateless workflow engine driven by a single-step CLI. Workflows are YAML definitions with roles, JSONata routing conditions, and a directed graph. Threads are immutable CAS-linked chains — each `uwf thread step` runs one moderator→agent→extract cycle and exits.
|
||||||
|
|
||||||
## Core Concepts
|
## Package Map
|
||||||
|
|
||||||
| Concept | Description |
|
| Package | npm | Role |
|
||||||
|---------|-------------|
|
|---------|-----|------|
|
||||||
| **Workflow** | A single-file ESM module exporting `run` (workflow function) and `descriptor` (metadata). Identified by its XXH64 hash. |
|
| `cli-workflow` | `@uncaged/cli-workflow` | `uwf` CLI binary — thread lifecycle, workflow registry, CAS inspection, setup |
|
||||||
| **Bundle** | The physical `.esm.js` file stored in `~/.uncaged/workflow/bundles/`. |
|
| `workflow-protocol` | `@uncaged/workflow-protocol` | Shared TypeScript types (`WorkflowPayload`, `StepNodePayload`, `WorkflowConfig`, etc.) |
|
||||||
| **Thread** | A single execution of a workflow, identified by a ULID. CAS-backed chain plus `threads.json` / `history/*.jsonl`; `.info.jsonl` for debug logs. |
|
| `workflow-moderator` | `@uncaged/workflow-moderator` | JSONata graph evaluator — determines next role or `$END` |
|
||||||
| **Role** | A named actor within a workflow. Each role produces output with typed `meta`. Roles live inside template packages (`src/roles/`). |
|
| `workflow-agent-kit` | `@uncaged/workflow-agent-kit` | `createAgent` factory, context builder, two-layer extract pipeline |
|
||||||
| **Registry** | `workflow.yaml` — maps workflow names to current/historical bundle hashes. |
|
| `workflow-agent-hermes` | `@uncaged/workflow-agent-hermes` | `uwf-hermes` agent — spawns Hermes chat, captures session |
|
||||||
| **CAS** | Content-Addressed Storage — bundles are immutable and addressed by hash. |
|
| `workflow-util` | `@uncaged/workflow-util` | Crockford Base32, ULID, logger, frontmatter parsing |
|
||||||
|
|
||||||
## Monorepo Packages
|
External: [`@uncaged/json-cas`](https://www.npmjs.com/package/@uncaged/json-cas) (CAS store + JSON Schema validation) + `@uncaged/json-cas-fs` (filesystem backend).
|
||||||
|
|
||||||
```
|
|
||||||
packages/
|
|
||||||
workflow/ # @uncaged/workflow — core lib (types, engine, hash, ULID, registry)
|
|
||||||
cli-workflow/ # @uncaged/cli-workflow — CLI (`uncaged-workflow` command)
|
|
||||||
workflow-template-develop/ # @uncaged/workflow-template-develop — develop workflow template (includes roles)
|
|
||||||
workflow-template-solve-issue/ # @uncaged/workflow-template-solve-issue — solve-issue workflow template (includes roles)
|
|
||||||
workflow-agent-hermes/ # @uncaged/workflow-agent-hermes — Hermes agent adapter
|
|
||||||
workflow-agent-cursor/ # @uncaged/workflow-agent-cursor — Cursor agent adapter
|
|
||||||
workflow-agent-llm/ # @uncaged/workflow-agent-llm — LLM agent adapter
|
|
||||||
workflow-util-agent/ # @uncaged/workflow-util-agent — agent utilities (buildAgentPrompt, spawnCli)
|
|
||||||
```
|
|
||||||
|
|
||||||
Managed with **bun workspace** using the `workspace:*` protocol.
|
|
||||||
|
|
||||||
## Quick Start
|
## Quick Start
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Install dependencies
|
# 1. Configure provider and model
|
||||||
bun install
|
uwf setup
|
||||||
|
|
||||||
# Build all packages
|
# 2. Register a workflow from YAML
|
||||||
bun run build
|
uwf workflow put examples/solve-issue.yaml
|
||||||
|
|
||||||
# Register a workflow bundle
|
# 3. Start a thread
|
||||||
uncaged-workflow workflow add solve-issue dist/packages/workflow-template-solve-issue/solve-issue.esm.js
|
uwf thread start solve-issue -p "Fix the login redirect bug"
|
||||||
|
|
||||||
# Run a workflow
|
# 4. Execute steps (one at a time, until done)
|
||||||
uncaged-workflow run solve-issue --prompt "Fix bug #42"
|
uwf thread step <thread-id>
|
||||||
```
|
```
|
||||||
|
|
||||||
## CLI Usage
|
## CLI Commands
|
||||||
|
|
||||||
```bash
|
### Thread
|
||||||
uncaged-workflow # Print full command usage (exits with status 1)
|
|
||||||
uncaged-workflow workflow list # List registered workflows
|
|
||||||
uncaged-workflow run <name> # Start a workflow thread
|
|
||||||
uncaged-workflow thread list # List all threads
|
|
||||||
uncaged-workflow thread show <id> # Inspect a thread
|
|
||||||
uncaged-workflow skill # Agent-consumable reference docs
|
|
||||||
```
|
|
||||||
|
|
||||||
Run `uncaged-workflow` with no arguments to print usage, or `uncaged-workflow skill cli` for the full CLI skill reference.
|
| Command | Description |
|
||||||
|
|---------|-------------|
|
||||||
|
| `uwf thread start <workflow> -p <prompt>` | Create a thread (no execution) |
|
||||||
|
| `uwf thread step <thread-id> [--agent <cmd>]` | Execute one moderator→agent→extract cycle |
|
||||||
|
| `uwf thread show <thread-id>` | Show head pointer and done status |
|
||||||
|
| `uwf thread list [--all]` | List threads (`--all` includes archived) |
|
||||||
|
| `uwf thread steps <thread-id>` | List all steps chronologically |
|
||||||
|
| `uwf thread read <thread-id> [--quota N]` | Render thread as readable markdown |
|
||||||
|
| `uwf thread fork <step-hash>` | Fork from a specific step |
|
||||||
|
| `uwf thread step-details <step-hash>` | Dump full detail node |
|
||||||
|
| `uwf thread kill <thread-id>` | Terminate and archive |
|
||||||
|
|
||||||
|
### Workflow
|
||||||
|
|
||||||
|
| Command | Description |
|
||||||
|
|---------|-------------|
|
||||||
|
| `uwf workflow put <file.yaml>` | Register a workflow from YAML |
|
||||||
|
| `uwf workflow show <name-or-hash>` | Show workflow definition |
|
||||||
|
| `uwf workflow list` | List registered workflows |
|
||||||
|
|
||||||
|
### CAS
|
||||||
|
|
||||||
|
| Command | Description |
|
||||||
|
|---------|-------------|
|
||||||
|
| `uwf cas get <hash>` | Read a CAS node |
|
||||||
|
| `uwf cas put <type-hash> <data>` | Store a node |
|
||||||
|
| `uwf cas has <hash>` | Check existence |
|
||||||
|
| `uwf cas refs <hash>` | List direct references |
|
||||||
|
| `uwf cas walk <hash>` | Recursive traversal |
|
||||||
|
| `uwf cas reindex` | Rebuild type index |
|
||||||
|
| `uwf cas schema list` | List schemas |
|
||||||
|
| `uwf cas schema get <hash>` | Show a schema |
|
||||||
|
|
||||||
|
### Setup
|
||||||
|
|
||||||
|
| Command | Description |
|
||||||
|
|---------|-------------|
|
||||||
|
| `uwf setup` | Interactive provider/model/agent configuration |
|
||||||
|
| `uwf setup --provider ... --base-url ... --api-key ... --model ...` | Non-interactive setup |
|
||||||
|
|
||||||
|
Config stored in `~/.uncaged/workflow/config.yaml`. API keys in `~/.uncaged/workflow/.env`.
|
||||||
|
|
||||||
## Development
|
## Development
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
bun run check # Biome lint + format check
|
bun install --no-cache # Install dependencies
|
||||||
bun run format # Auto-format with Biome
|
bun run check # tsc + biome + lint-log-tags
|
||||||
bun test # Run tests
|
bun run format # Auto-format with Biome
|
||||||
|
bun test # Run all tests
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Managed with **bun workspace**. See [CLAUDE.md](CLAUDE.md) for coding conventions.
|
||||||
|
|
||||||
## Architecture
|
## Architecture
|
||||||
|
|
||||||
See [docs/architecture.md](docs/architecture.md) for the full design — three-phase engine loop, bundle contract, storage layout, and design decisions.
|
See [docs/architecture.md](docs/architecture.md) for the full design — three-phase engine loop, CAS node types, storage layout, agent CLI protocol, and design decisions.
|
||||||
|
|||||||
+13
-1
@@ -5,6 +5,8 @@
|
|||||||
"**",
|
"**",
|
||||||
"!**/dist",
|
"!**/dist",
|
||||||
"!**/node_modules",
|
"!**/node_modules",
|
||||||
|
"!**/legacy-packages",
|
||||||
|
"!scripts",
|
||||||
"!packages/workflow/workflow",
|
"!packages/workflow/workflow",
|
||||||
"!xiaoju/scripts/bundle.ts"
|
"!xiaoju/scripts/bundle.ts"
|
||||||
]
|
]
|
||||||
@@ -36,7 +38,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"includes": ["**/*.d.ts"],
|
"includes": ["**/*.d.ts", "**/vitest.config.*"],
|
||||||
"linter": {
|
"linter": {
|
||||||
"rules": {
|
"rules": {
|
||||||
"style": {
|
"style": {
|
||||||
@@ -44,6 +46,16 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"includes": ["**/cli.ts", "**/setup.ts"],
|
||||||
|
"linter": {
|
||||||
|
"rules": {
|
||||||
|
"suspicious": {
|
||||||
|
"noConsole": "off"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"linter": {
|
"linter": {
|
||||||
|
|||||||
+406
-182
@@ -1,271 +1,495 @@
|
|||||||
# Uncaged workflow — Architecture
|
# Workflow Engine — Architecture
|
||||||
|
|
||||||
**Last updated:** 2026-05-09
|
**Last updated:** 2026-05-19
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Overview
|
## Overview
|
||||||
|
|
||||||
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 stateless workflow engine driven by a single-step CLI. Workflows are YAML definitions stored as CAS nodes; threads are immutable chains of CAS-linked step nodes. No daemon — each `uwf thread step` invocation runs one moderator→agent→extract cycle and exits.
|
||||||
|
|
||||||
The implementation lives in **21** Bun workspace packages under `packages/`, using the `workspace:*` protocol.
|
The implementation lives in **6** active packages under `packages/`, plus two external CAS packages (`@uncaged/json-cas`, `@uncaged/json-cas-fs`). Legacy packages reside in `legacy-packages/` and are not part of the active stack.
|
||||||
|
|
||||||
## Package map
|
## Package map
|
||||||
|
|
||||||
Grouped by responsibility (npm name → folder).
|
|
||||||
|
|
||||||
| Layer | Package | One-line role |
|
| Layer | Package | One-line role |
|
||||||
|-------|---------|----------------|
|
|-------|---------|---------------|
|
||||||
| Contract | `@uncaged/workflow-protocol` → `workflow-protocol` | Shared TypeScript types and `Result` helpers; peer `zod` only — no other workspace deps. |
|
| Contract | `@uncaged/workflow-protocol` → `workflow-protocol` | Shared TypeScript types (`WorkflowPayload`, `StepNodePayload`, `ModeratorContext`, `WorkflowConfig`, etc.). No runtime deps beyond `@uncaged/json-cas-fs`. |
|
||||||
| 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` | Crockford Base32, ULID generation, `createLogger`, frontmatter parsing/validation. |
|
||||||
| Shared infra | `@uncaged/workflow-util` → `workflow-util` | Base32/ULID, logger, storage root paths, global CAS dir, ref-field helpers. |
|
| Moderator | `@uncaged/workflow-moderator` → `workflow-moderator` | JSONata-based graph evaluator: given a `WorkflowPayload` and `ModeratorContext`, returns the next role or `$END`. |
|
||||||
| LLM plumbing | `@uncaged/workflow-reactor` → `workflow-reactor` | `createLlmFn`, `createThreadReactor`, and related tool-call types for threaded LLM invocation. |
|
| Agent framework | `@uncaged/workflow-agent-kit` → `workflow-agent-kit` | `createAgent` entrypoint factory, context builder, frontmatter fast-path extractor, LLM extract fallback, output format instruction builder. |
|
||||||
| CAS | `@uncaged/workflow-cas` → `workflow-cas` | `CasStore` implementation, XXH64 hashing, Merkle helpers over CAS payloads. |
|
| Agent: Hermes | `@uncaged/workflow-agent-hermes` → `workflow-agent-hermes` | `uwf-hermes` CLI binary — spawns `hermes chat`, pipes prompt, captures session detail. |
|
||||||
| Registry / bundles | `@uncaged/workflow-register` → `workflow-register` | Bundle validation & dynamic export extraction, `workflow.yaml` registry I/O, provider/model resolution. |
|
| CLI | `@uncaged/cli-workflow` → `cli-workflow` | `uwf` binary — thread lifecycle, workflow registry, CAS inspection, setup. |
|
||||||
| 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-office` → `workflow-agent-office` | `AdapterFn` via `office-agent` CLI; generates or edits Word documents, stores outputs per threadId. |
|
|
||||||
| | `@uncaged/workflow-agent-docx-diff` → `workflow-agent-docx-diff` | `AdapterFn` via `docx-diff` CLI; produces Word-format diff reports for document edit workflows. |
|
|
||||||
| | `@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. |
|
|
||||||
| | `@uncaged/workflow-template-document` → `workflow-template-document` | Document generation/editing workflow definition (writer + differ roles, moderator table, descriptor). |
|
|
||||||
| Dashboard | `@uncaged/workflow-dashboard` → `workflow-dashboard` | Private Vite + React app (`src/main.tsx`); only `react` / `react-dom` dependencies — no workspace packages. |
|
|
||||||
|
|
||||||
## Dependency graph (workspace packages)
|
### External dependencies
|
||||||
|
|
||||||
Bottom-up layering for the execution stack:
|
| Package | Role |
|
||||||
|
|---------|------|
|
||||||
|
| `@uncaged/json-cas` | Content-addressed store API, XXH64 hashing, JSON Schema registration and validation. |
|
||||||
|
| `@uncaged/json-cas-fs` | Filesystem backend for `json-cas`. |
|
||||||
|
| `jsonata` | JSONata expression evaluator (used by `workflow-moderator`). |
|
||||||
|
| `commander` | CLI argument parsing (used by `cli-workflow`). |
|
||||||
|
| `dotenv` | Loads `.env` files for API keys. |
|
||||||
|
| `yaml` | YAML parse/stringify. |
|
||||||
|
|
||||||
|
## Dependency graph
|
||||||
|
|
||||||
```mermaid
|
```mermaid
|
||||||
flowchart BT
|
flowchart BT
|
||||||
|
subgraph External
|
||||||
|
jcas["@uncaged/json-cas"]
|
||||||
|
jcasfs["@uncaged/json-cas-fs"]
|
||||||
|
end
|
||||||
subgraph L0["Layer 0 — contract"]
|
subgraph L0["Layer 0 — contract"]
|
||||||
protocol["@uncaged/workflow-protocol"]
|
protocol["@uncaged/workflow-protocol"]
|
||||||
end
|
end
|
||||||
subgraph L1["Layer 1 — on protocol"]
|
subgraph L1["Layer 1 — shared"]
|
||||||
runtime["@uncaged/workflow-runtime"]
|
|
||||||
util["@uncaged/workflow-util"]
|
util["@uncaged/workflow-util"]
|
||||||
reactor["@uncaged/workflow-reactor"]
|
moderator["@uncaged/workflow-moderator"]
|
||||||
end
|
end
|
||||||
subgraph L2["Layer 2 — protocol + util"]
|
subgraph L2["Layer 2 — agent framework"]
|
||||||
cas["@uncaged/workflow-cas"]
|
kit["@uncaged/workflow-agent-kit"]
|
||||||
register["@uncaged/workflow-register"]
|
|
||||||
end
|
end
|
||||||
subgraph L3["Layer 3 — engine"]
|
subgraph L3["Layer 3 — agent implementations"]
|
||||||
execute["@uncaged/workflow-execute"]
|
hermes["@uncaged/workflow-agent-hermes"]
|
||||||
end
|
end
|
||||||
subgraph L4["Layer 4 — CLI"]
|
subgraph L4["Layer 4 — CLI"]
|
||||||
cli["@uncaged/cli-workflow"]
|
cli["@uncaged/cli-workflow"]
|
||||||
end
|
end
|
||||||
runtime --> protocol
|
protocol --> jcasfs
|
||||||
util --> protocol
|
util --> protocol
|
||||||
reactor --> protocol
|
moderator --> protocol
|
||||||
cas --> protocol
|
kit --> protocol
|
||||||
cas --> util
|
kit --> util
|
||||||
register --> protocol
|
kit --> jcas
|
||||||
register --> util
|
kit --> jcasfs
|
||||||
execute --> protocol
|
hermes --> kit
|
||||||
execute --> runtime
|
hermes --> jcas
|
||||||
execute --> util
|
|
||||||
execute --> cas
|
|
||||||
execute --> reactor
|
|
||||||
execute --> register
|
|
||||||
cli --> protocol
|
cli --> protocol
|
||||||
cli --> util
|
cli --> util
|
||||||
cli --> cas
|
cli --> kit
|
||||||
cli --> execute
|
cli --> moderator
|
||||||
cli --> register
|
cli --> jcas
|
||||||
cli --> runtime
|
cli --> jcasfs
|
||||||
```
|
```
|
||||||
|
|
||||||
**Adjacent consumers** (not in the main CLI stack):
|
## Workflow definition
|
||||||
|
|
||||||
- `@uncaged/workflow-util-agent` → `@uncaged/workflow-runtime`
|
Workflows are **YAML files** (not ESM bundles). `uwf workflow put <file.yaml>` parses the YAML, registers output schemas as JSON Schema CAS nodes, and stores the `WorkflowPayload` as a CAS node.
|
||||||
- `@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)
|
Example (`examples/solve-issue.yaml`):
|
||||||
|
|
||||||
- **`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.
|
```yaml
|
||||||
- **`workflow-runtime`** — Workflow author surface: `createWorkflow` from `src/create-workflow.js`, re-exports protocol types/constants used when authoring bundles.
|
name: "solve-issue"
|
||||||
- **`workflow-util`** — Cross-cutting utilities: Crockford Base32, ULID, `createLogger`, `getDefaultWorkflowStorageRoot`, `getGlobalCasDir`, ref normalization; re-exports `ok`/`err` from protocol.
|
description: "End-to-end issue resolution"
|
||||||
- **`workflow-cas`** — Filesystem CAS (`createCasStore`), `hashString` / `hashWorkflowBundleBytes`, Merkle node serialization and helpers (`merkle.js`).
|
roles:
|
||||||
- **`workflow-register`** — Bundle pipeline (`validateWorkflowBundle`, `extractBundleExports`, descriptor builders), registry YAML read/write, `resolveModel` / `splitProviderModelRef`.
|
planner:
|
||||||
- **`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`).
|
description: "Creates implementation plan"
|
||||||
- **`workflow-reactor`** — `createLlmFn`, `createThreadReactor`, and thread tool-invocation types — consumed by `workflow-execute`.
|
goal: "You are a planning agent. Analyze the issue and create a step-by-step plan."
|
||||||
- **`cli-workflow`** — CLI commands and HTTP/dashboard-related wiring (`hono`, `yaml`); composes register + execute + CAS + util.
|
capabilities:
|
||||||
- **`workflow-agent-*`** — Replaceable `AgentFn` implementations (Cursor / Hermes CLIs, or HTTP LLM).
|
- issue-analysis
|
||||||
- **`workflow-util-agent`** — Shared prompt assembly and subprocess spawning for CLI agents.
|
- planning
|
||||||
- **`workflow-template-*`** — Concrete `WorkflowDefinition` graphs + Zod role schemas + descriptor builders for publishing bundles.
|
procedure: "Analyze the issue and create a detailed, actionable implementation plan."
|
||||||
- **`workflow-dashboard`** — Standalone React UI; no published library entry matching `src/index.ts`.
|
output: "Output the plan summary and list of concrete steps."
|
||||||
|
meta:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
plan: { type: string }
|
||||||
|
steps: { type: array, items: { type: string } }
|
||||||
|
required: [plan, steps]
|
||||||
|
developer:
|
||||||
|
description: "Implements code changes"
|
||||||
|
goal: "You are a developer agent. Implement the plan."
|
||||||
|
capabilities:
|
||||||
|
- file-edit
|
||||||
|
- shell
|
||||||
|
procedure: "Implement the plan. Write code, tests, and ensure existing tests pass."
|
||||||
|
output: "List all files changed and provide a summary of the implementation."
|
||||||
|
meta:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
filesChanged: { type: array, items: { type: string } }
|
||||||
|
summary: { type: string }
|
||||||
|
required: [filesChanged, summary]
|
||||||
|
reviewer:
|
||||||
|
description: "Reviews code changes"
|
||||||
|
goal: "You are a code reviewer. Review the implementation."
|
||||||
|
capabilities:
|
||||||
|
- code-review
|
||||||
|
procedure: "Review the implementation against the plan."
|
||||||
|
output: "Approve or reject with detailed comments."
|
||||||
|
meta:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
approved: { type: boolean }
|
||||||
|
comments: { type: string }
|
||||||
|
required: [approved, comments]
|
||||||
|
conditions:
|
||||||
|
notApproved:
|
||||||
|
description: "Reviewer rejected the implementation"
|
||||||
|
expression: "steps[-1].output.approved = false"
|
||||||
|
graph:
|
||||||
|
$START:
|
||||||
|
- role: "planner"
|
||||||
|
condition: null
|
||||||
|
planner:
|
||||||
|
- role: "developer"
|
||||||
|
condition: null
|
||||||
|
developer:
|
||||||
|
- role: "reviewer"
|
||||||
|
condition: null
|
||||||
|
reviewer:
|
||||||
|
- role: "developer"
|
||||||
|
condition: "notApproved"
|
||||||
|
- role: "$END"
|
||||||
|
condition: null
|
||||||
|
```
|
||||||
|
|
||||||
|
Key properties:
|
||||||
|
|
||||||
|
- **`roles`** — inline role definitions; each `meta` is a JSON Schema (stored as its own CAS node on registration)
|
||||||
|
- **`conditions`** — named JSONata expressions evaluated against the `ModeratorContext`
|
||||||
|
- **`graph`** — `Record<Role | "$START", Transition[]>` — first matching transition wins; `condition: null` = fallback
|
||||||
|
- **No agent binding** — agent selection is a deployment concern, configured in `config.yaml`
|
||||||
|
- **No Zod** — all schemas are JSON Schema, validated through `@uncaged/json-cas`
|
||||||
|
|
||||||
## Three-phase engine loop
|
## 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`.
|
Each `uwf thread step` runs exactly one cycle: moderator → agent → extract. The CLI orchestrates this in `packages/cli-workflow/src/commands/thread.ts` (`cmdThreadStep`).
|
||||||
|
|
||||||
```
|
```
|
||||||
┌─→ Phase 1: MODERATOR
|
┌─→ Phase 1: MODERATOR
|
||||||
│ Context: ModeratorContext { threadId, depth, start, steps }
|
│ Input: WorkflowPayload + ModeratorContext { start, steps[] }
|
||||||
│ Action: moderator(ctx) → role name | END
|
│ Engine: JSONata conditions evaluated against the graph
|
||||||
|
│ Output: next role name | $END
|
||||||
│
|
│
|
||||||
│ Phase 2: AGENT
|
│ Phase 2: AGENT
|
||||||
│ Context: AgentContext = ModeratorCtx + { currentRole: { name, systemPrompt } }
|
│ Input: thread-id + role (via argv)
|
||||||
│ Action: agent(ctx) → raw string
|
│ Engine: agent-kit builds context from CAS chain, prepends
|
||||||
|
│ output format instruction to system prompt, spawns agent
|
||||||
|
│ Output: raw string (frontmatter markdown)
|
||||||
│
|
│
|
||||||
│ Phase 3: EXTRACTOR
|
│ Phase 3: EXTRACT
|
||||||
│ Context: ExtractContext = AgentCtx + { agentContent }
|
│ Input: raw agent output + role's meta schema
|
||||||
│ Action: runtime.extract(schema, extractPrompt, ctx) → typed meta
|
│ Engine: two-layer extract (frontmatter fast path → LLM fallback)
|
||||||
|
│ Output: CasRef to structured output node
|
||||||
│
|
│
|
||||||
│ Merge: RoleStep { role, contentHash, meta, refs, timestamp }
|
│ Persist: StepNode { start, prev, role, output, detail, agent }
|
||||||
│ Append to steps
|
│ Update: threads.yaml head pointer
|
||||||
└─────────────────────────────────────────────────────┘
|
└─────────────────────────────────────────────────────────────────┘
|
||||||
```
|
```
|
||||||
|
|
||||||
### Context types (progressive)
|
### Context types
|
||||||
|
|
||||||
Defined in `packages/workflow-protocol/src/types.ts`:
|
Defined in `packages/workflow-protocol/src/types.ts`:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
type ModeratorContext<M> = ThreadContext<M>;
|
type StepContext = {
|
||||||
type AgentContext<M> = ModeratorContext<M> & {
|
role: string;
|
||||||
currentRole: { name: string; systemPrompt: string };
|
output: unknown; // CAS node payload, expanded (not hash)
|
||||||
|
detail: CasRef;
|
||||||
|
agent: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type ModeratorContext = {
|
||||||
|
start: StartNodePayload; // { workflow: CasRef, prompt: string }
|
||||||
|
steps: StepContext[]; // chronological, oldest first
|
||||||
|
};
|
||||||
|
|
||||||
|
type AgentContext = ModeratorContext & {
|
||||||
|
threadId: ThreadId;
|
||||||
|
role: string;
|
||||||
|
store: Store;
|
||||||
|
workflow: WorkflowPayload;
|
||||||
|
outputFormatInstruction: string;
|
||||||
};
|
};
|
||||||
type ExtractContext<M> = AgentContext<M> & { agentContent: string };
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Key properties
|
### Key properties
|
||||||
|
|
||||||
- **Moderator is synchronous and pure** — no I/O, no state mutation inside `createWorkflow`’s moderator call path.
|
- **Moderator** — pure JSONata evaluation; no LLM call, no I/O beyond CAS reads. Evaluates `workflow.graph[currentRole]` transitions in order, returns first match.
|
||||||
- **Agent receives `AgentContext`** — reads `ctx.currentRole.systemPrompt`; raw output becomes `agentContent` for extract.
|
- **Agent** — receives `AgentContext` with thread history + role system prompt + output format instruction. Raw output is frontmatter markdown.
|
||||||
- **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`).
|
- **Extractor** — two-layer: tries frontmatter fast-path first (zero LLM cost), falls back to LLM extract if frontmatter is absent or invalid.
|
||||||
- **`extractPrompt` is a call parameter** on `RoleDefinition`, not implicit context state.
|
- **Stateless** — each `uwf thread step` is an atomic, self-contained operation. No in-memory state between steps.
|
||||||
|
|
||||||
## Agent information sources
|
## Agent CLI protocol
|
||||||
|
|
||||||
An agent has exactly three information sources:
|
Each agent is an external command invoked by `uwf thread step`:
|
||||||
|
|
||||||
1. **Prior knowledge** — LLM training, agent memory, agent skills
|
```bash
|
||||||
2. **Thread context** — `AgentContext` (`start`, `steps`, `currentRole`)
|
<agent-cmd> <thread-id> <role>
|
||||||
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 obtains it via `ExtractFn` (e.g. Cursor agent).
|
|
||||||
|
|
||||||
## Bundle contract
|
|
||||||
|
|
||||||
A workflow bundle is a single `.esm.js` file with two named exports (see `WorkflowFn` / `WorkflowDescriptor` in `packages/workflow-protocol/src/types.ts`):
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
export const descriptor: WorkflowDescriptor;
|
|
||||||
export const run: WorkflowFn;
|
|
||||||
|
|
||||||
type WorkflowFn = (
|
|
||||||
thread: ThreadContext,
|
|
||||||
runtime: WorkflowRuntime,
|
|
||||||
) => AsyncGenerator<RoleOutput, WorkflowCompletion>;
|
|
||||||
```
|
```
|
||||||
|
|
||||||
`RoleOutput` carries `contentHash`, `meta`, and `refs` (agent text lives in CAS, addressed by hash).
|
Contract:
|
||||||
|
1. `uwf thread step` determines the next role via the moderator
|
||||||
|
2. Agent CLI is spawned with `(thread-id, role)` as positional args
|
||||||
|
3. `workflow-agent-kit` (`createAgent`) handles the boilerplate:
|
||||||
|
- Parses argv
|
||||||
|
- Loads `.env` from storage root
|
||||||
|
- Builds `AgentContext` by walking the CAS chain from `threads.yaml` head
|
||||||
|
- Resolves the role's `meta` schema and builds `outputFormatInstruction`
|
||||||
|
- Calls the agent's `run` function
|
||||||
|
- Runs two-layer extract on the raw output
|
||||||
|
- Writes `StepNode` to CAS (output + detail + prev link)
|
||||||
|
- Prints the new `StepNode` CAS hash to stdout
|
||||||
|
4. `uwf thread step` reads stdout, updates `threads.yaml` head pointer, re-evaluates moderator for `done`
|
||||||
|
5. Exit 0 = success, non-zero = failure
|
||||||
|
|
||||||
### Constraints
|
Agent resolution priority: `--agent` CLI override → `config.yaml` per-workflow/role override → `config.yaml` `defaultAgent`.
|
||||||
|
|
||||||
- Single `.esm.js` file
|
## Agent output format: frontmatter markdown (RFC #351)
|
||||||
- No dynamic `import()` in bundles (loader exempt in engine)
|
|
||||||
- Portable bundle static imports are constrained by validation in `@uncaged/workflow-register` (`validateWorkflowBundle`)
|
|
||||||
- XXH64 hash (Crockford Base32) = version ID
|
|
||||||
|
|
||||||
### Why AsyncGenerator?
|
Agents produce **frontmatter markdown** — YAML frontmatter for structured meta, followed by a markdown body for content:
|
||||||
|
|
||||||
- Each `yield` lets `workflow-execute` persist state, CAS rows, and enforce pause/abort
|
```markdown
|
||||||
- `return` supplies `WorkflowCompletion`
|
---
|
||||||
- Fork replays historical steps into a new thread context
|
status: done
|
||||||
- Bundle does not import the engine — only protocol/runtime types at build time
|
next: reviewer
|
||||||
|
confidence: 0.9
|
||||||
|
artifacts:
|
||||||
|
- src/auth.ts
|
||||||
|
scope: role
|
||||||
|
---
|
||||||
|
|
||||||
|
## Implementation
|
||||||
|
|
||||||
|
Fixed the login redirect by updating the auth middleware...
|
||||||
|
```
|
||||||
|
|
||||||
|
The `outputFormatInstruction` (built by `buildOutputFormatInstruction` in `workflow-agent-kit`) is prepended to the role's system prompt, so the deliverable format is the first thing the agent sees. It lists the expected frontmatter fields derived from the role's `meta` JSON Schema.
|
||||||
|
|
||||||
|
## Two-layer extract
|
||||||
|
|
||||||
|
Structured output extraction uses a two-layer strategy (`workflow-agent-kit`):
|
||||||
|
|
||||||
|
### Layer 1: frontmatter fast path (`frontmatter.ts`)
|
||||||
|
|
||||||
|
1. Parse YAML frontmatter from raw agent output (`parseFrontmatterMarkdown`)
|
||||||
|
2. Validate required fields (`validateFrontmatter`)
|
||||||
|
3. Build a candidate object from frontmatter fields (`status`, `next`, `confidence`, `artifacts`, `scope`)
|
||||||
|
4. `store.put()` the candidate against the role's `meta` schema
|
||||||
|
5. Validate with `json-cas` schema validation
|
||||||
|
6. If valid → return `outputHash` (zero LLM cost)
|
||||||
|
|
||||||
|
### Layer 2: LLM extract fallback (`extract.ts`)
|
||||||
|
|
||||||
|
If the fast path returns `null` (no frontmatter, invalid, or doesn't satisfy schema):
|
||||||
|
|
||||||
|
1. Resolve extract model alias from config (`modelOverrides.extract` → `models.extract` → `defaultModel`)
|
||||||
|
2. Call OpenAI-compatible chat completion with JSON mode
|
||||||
|
3. System prompt: "Extract structured data matching this JSON Schema: ..."
|
||||||
|
4. User message: the raw agent output
|
||||||
|
5. Parse response, `store.put()`, validate
|
||||||
|
6. Return `outputHash`
|
||||||
|
|
||||||
|
## Prompt injection
|
||||||
|
|
||||||
|
`workflow-agent-kit` prepends two pieces of context to the agent's system prompt:
|
||||||
|
|
||||||
|
1. **Deliverable format instruction** — generated from the role's `meta` schema, tells the agent exactly what frontmatter fields to produce and the expected format
|
||||||
|
2. **Scope constraint** — "Focus exclusively on YOUR role's deliverable. Do not perform actions outside your role's scope."
|
||||||
|
|
||||||
|
This ensures agents produce parseable frontmatter output without requiring per-agent format knowledge.
|
||||||
|
|
||||||
|
## CAS node types
|
||||||
|
|
||||||
|
### Workflow
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
type: <workflow-schema-hash>
|
||||||
|
payload:
|
||||||
|
name: "solve-issue"
|
||||||
|
description: "End-to-end issue resolution"
|
||||||
|
roles:
|
||||||
|
planner:
|
||||||
|
description: "Creates implementation plan"
|
||||||
|
goal: "You are a planning agent..."
|
||||||
|
capabilities: [planning, issue-analysis]
|
||||||
|
procedure: "Analyze the issue and create a plan."
|
||||||
|
output: "Output the plan summary."
|
||||||
|
meta: "5GWKR8TN1V3JA" # cas_ref → JSON Schema node
|
||||||
|
conditions:
|
||||||
|
notApproved:
|
||||||
|
description: "Reviewer rejected"
|
||||||
|
expression: "steps[-1].output.approved = false"
|
||||||
|
graph:
|
||||||
|
$START:
|
||||||
|
- role: "planner"
|
||||||
|
condition: null
|
||||||
|
```
|
||||||
|
|
||||||
|
### StartNode
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
type: <start-node-schema-hash>
|
||||||
|
payload:
|
||||||
|
workflow: "4KNM2PXR3B1QW" # cas_ref → Workflow
|
||||||
|
prompt: "Fix the login bug..."
|
||||||
|
```
|
||||||
|
|
||||||
|
### StepNode
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
type: <step-node-schema-hash>
|
||||||
|
payload:
|
||||||
|
start: "4TNVW8KR2B3MA" # cas_ref → StartNode
|
||||||
|
prev: "2MXBG6PN4A8JR" # cas_ref → previous StepNode (null for first step)
|
||||||
|
role: "developer"
|
||||||
|
output: "9KRVW3TN5F1QA" # cas_ref → structured output (validated against meta schema)
|
||||||
|
detail: "7BQST3VW9F2MA" # cas_ref → execution detail (raw turns, session data)
|
||||||
|
agent: "uwf-hermes" # agent command used (plain string)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Chain structure
|
||||||
|
|
||||||
|
```
|
||||||
|
threads.yaml: { "01J7K9...4T": "8FWKR3TN5V1QA" }
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
StepNode (step 3)
|
||||||
|
├── start ──→ StartNode
|
||||||
|
│ ├── workflow → Workflow (CAS)
|
||||||
|
│ └── prompt: "Fix..."
|
||||||
|
├── prev ──→ StepNode (step 2)
|
||||||
|
│ ├── prev ──→ StepNode (step 1)
|
||||||
|
│ │ └── prev: null
|
||||||
|
│ └── ...
|
||||||
|
├── role: "reviewer"
|
||||||
|
├── output → CAS({ approved: true })
|
||||||
|
├── detail → CAS(session turns)
|
||||||
|
└── agent: "uwf-hermes"
|
||||||
|
```
|
||||||
|
|
||||||
## Storage layout
|
## Storage layout
|
||||||
|
|
||||||
```
|
```
|
||||||
~/.uncaged/workflow/
|
~/.uncaged/workflow/
|
||||||
├── cas/ # Global content-addressed blobs (see getGlobalCasDir)
|
├── cas/ # json-cas filesystem store (all CAS nodes)
|
||||||
├── bundles/
|
├── config.yaml # Provider, model, agent configuration
|
||||||
│ ├── C9NMV6V2TQT81.esm.js # Crockford Base32 of XXH64
|
├── threads.yaml # Active thread head pointers: threadId → CasRef
|
||||||
│ ├── C9NMV6V2TQT81.yaml # Role descriptor sidecar (when present)
|
├── history.jsonl # Archived thread records
|
||||||
│ └── C9NMV6V2TQT81/ # Per-hash bundle dir (alongside or instead of loose files)
|
├── registry.yaml # Workflow name → CAS hash mapping
|
||||||
│ ├── threads.json # Active threads: threadId → { head, start, updatedAt }
|
└── .env # API keys (loaded by dotenv)
|
||||||
│ └── history/
|
|
||||||
│ └── 2026-05-09.jsonl # Completed threads (one JSON object per line)
|
|
||||||
├── logs/ # One folder per bundle hash
|
|
||||||
│ └── C9NMV6V2TQT81/
|
|
||||||
│ ├── 01KQXKW…YG.running # Present while worker executes this thread (optional)
|
|
||||||
│ └── 01KQXKW…YG.info.jsonl # Debug log
|
|
||||||
└── workflow.yaml # Registry
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Mutable state
|
||||||
|
|
||||||
|
Only three files carry mutable state:
|
||||||
|
|
||||||
|
| File | Contents |
|
||||||
|
|------|----------|
|
||||||
|
| `threads.yaml` | `Record<ThreadId, CasRef>` — maps active thread IDs to head node hash |
|
||||||
|
| `history.jsonl` | Append-only log of completed threads (`thread`, `workflow`, `head`, `completedAt`) |
|
||||||
|
| `registry.yaml` | Workflow name → current CAS hash |
|
||||||
|
|
||||||
|
Everything else is immutable CAS content.
|
||||||
|
|
||||||
### 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
|
- CAS hash: XXH64 → 13-char Crockford Base32
|
||||||
- Thread ID: ULID → 26-char (10 timestamp + 16 random)
|
- Thread ID: ULID → 26-char Crockford Base32 (10 timestamp + 16 random)
|
||||||
|
|
||||||
### Registry (`workflow.yaml`)
|
### Config (`config.yaml`)
|
||||||
|
|
||||||
Managed by `@uncaged/workflow-register` (`readWorkflowRegistry`, `writeWorkflowRegistry`, …). Shape includes workflow entries and a top-level `config` section used for extract/supervisor model resolution.
|
```yaml
|
||||||
|
providers:
|
||||||
|
openrouter:
|
||||||
|
baseUrl: "https://openrouter.ai/api/v1"
|
||||||
|
apiKeyEnv: "OPENROUTER_API_KEY"
|
||||||
|
|
||||||
### Thread storage (CAS + index)
|
models:
|
||||||
|
sonnet:
|
||||||
|
provider: "openrouter"
|
||||||
|
name: "anthropic/claude-sonnet-4"
|
||||||
|
gpt4o-mini:
|
||||||
|
provider: "openai"
|
||||||
|
name: "gpt-4o-mini"
|
||||||
|
|
||||||
Thread execution state is a chain of immutable CAS nodes (`StartNode`, `StateNode`, content Merkle blobs). Per bundle:
|
agents:
|
||||||
|
hermes:
|
||||||
|
command: "uwf-hermes"
|
||||||
|
args: []
|
||||||
|
cursor:
|
||||||
|
command: "uwf-cursor"
|
||||||
|
args: []
|
||||||
|
|
||||||
- **`threads.json`** — only in-flight threads (`head`, `start`, `updatedAt`).
|
defaultAgent: "hermes"
|
||||||
- **`history/{YYYY-MM-DD}.jsonl`** — completed threads (`threadId`, `head`, `start`, `completedAt`).
|
agentOverrides:
|
||||||
- **CAS (`cas/`)** — payloads and refs for replay, GC, and fork sharing.
|
solve-issue:
|
||||||
|
developer: "cursor"
|
||||||
|
|
||||||
**`.info.jsonl`** — Structured debug log via `@uncaged/workflow-util` `createLogger`:
|
defaultModel: "sonnet"
|
||||||
|
modelOverrides:
|
||||||
```jsonc
|
extract: "gpt4o-mini"
|
||||||
{ "tag": "4KNMR2PX", "content": "Loading bundle...", "timestamp": ... }
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Tags are 8-char Crockford Base32 (40-bit random), one per call site. `grep "4KNMR2PX"` → code location.
|
|
||||||
|
|
||||||
## Execution model
|
|
||||||
|
|
||||||
- **No daemon.** `uncaged-workflow run <name>` starts a worker process (`workflow-execute` worker entry via `getWorkerHostScriptPath`)
|
|
||||||
- Threads share bundle-scoped workers as implemented in CLI/engine
|
|
||||||
- Pause/resume/abort via engine IPC and pause gate (`createThreadPauseGate`)
|
|
||||||
|
|
||||||
## CLI commands
|
## CLI commands
|
||||||
|
|
||||||
| Priority | Command | Description |
|
Binary: `uwf`
|
||||||
|----------|---------|-------------|
|
|
||||||
| P1 | `add <name> <file.esm.js>` | Register a bundle |
|
### Thread commands
|
||||||
| P1 | `list` | List registered workflows |
|
|
||||||
| P1 | `show <name>` | Show workflow details |
|
| Command | Description |
|
||||||
| P1 | `remove <name>` | Remove a workflow |
|
|---------|-------------|
|
||||||
| P1 | `run <name> [--prompt] [--max-rounds]` | Start a thread |
|
| `uwf thread start <workflow> -p <prompt>` | Create a thread (StartNode → CAS, head → threads.yaml). No execution. |
|
||||||
| P1 | `threads [name]` | List threads |
|
| `uwf thread step <thread-id> [--agent <cmd>]` | Execute one moderator→agent→extract cycle. |
|
||||||
| P1 | `thread <id>` | Show thread state |
|
| `uwf thread show <thread-id>` | Show thread head pointer and done status. |
|
||||||
| P1 | `thread rm <id>` | Delete a thread |
|
| `uwf thread list [--all]` | List active threads (`--all` includes archived). |
|
||||||
| P1 | `ps` | List running threads |
|
| `uwf thread steps <thread-id>` | List all steps in chronological order. |
|
||||||
| P1 | `kill <thread-id>` | Terminate a running thread |
|
| `uwf thread read <thread-id> [--quota <chars>] [--before <hash>]` | Render thread as human-readable markdown. |
|
||||||
| P2 | `history <name>` | Show version history |
|
| `uwf thread fork <step-hash>` | Fork a thread from a specific CAS node. |
|
||||||
| P2 | `rollback <name> [hash]` | Switch to a previous version |
|
| `uwf thread step-details <step-hash>` | Dump full detail node as YAML. |
|
||||||
| P2 | `pause <thread-id>` | Pause a running thread |
|
| `uwf thread kill <thread-id>` | Terminate and archive a thread. |
|
||||||
| P2 | `resume <thread-id>` | Resume a paused thread |
|
|
||||||
| P3 | `fork <thread-id> [--from-role <role>]` | Fork from historical state |
|
### Workflow commands
|
||||||
|
|
||||||
|
| Command | Description |
|
||||||
|
|---------|-------------|
|
||||||
|
| `uwf workflow put <file.yaml>` | Register a workflow from YAML definition. |
|
||||||
|
| `uwf workflow show <id>` | Show workflow by name or CAS hash. |
|
||||||
|
| `uwf workflow list` | List registered workflows. |
|
||||||
|
|
||||||
|
### CAS commands
|
||||||
|
|
||||||
|
| Command | Description |
|
||||||
|
|---------|-------------|
|
||||||
|
| `uwf cas get <hash>` | Read a CAS node. |
|
||||||
|
| `uwf cas put <type-hash> <data>` | Store a node, print its hash. |
|
||||||
|
| `uwf cas has <hash>` | Check if a hash exists. |
|
||||||
|
| `uwf cas refs <hash>` | List direct CAS references. |
|
||||||
|
| `uwf cas walk <hash>` | Recursive traversal from a node. |
|
||||||
|
| `uwf cas reindex` | Rebuild type index from all nodes. |
|
||||||
|
| `uwf cas schema list` | List registered schemas. |
|
||||||
|
| `uwf cas schema get <hash>` | Show a schema by type hash. |
|
||||||
|
|
||||||
|
### Setup
|
||||||
|
|
||||||
|
| Command | Description |
|
||||||
|
|---------|-------------|
|
||||||
|
| `uwf setup [--provider --base-url --api-key --model --agent]` | Configure provider/model/agent (interactive if no flags). |
|
||||||
|
|
||||||
|
## Toolchain
|
||||||
|
|
||||||
|
| Tool | Purpose |
|
||||||
|
|------|---------|
|
||||||
|
| **bun** | Package manager + runtime |
|
||||||
|
| **TypeScript** | Type checking (strict mode) |
|
||||||
|
| **Biome** | Lint + format |
|
||||||
|
| **vitest** | Test runner |
|
||||||
|
|
||||||
## Design decisions
|
## Design decisions
|
||||||
|
|
||||||
| Decision | Rationale |
|
| Decision | Rationale |
|
||||||
|----------|-----------|
|
|----------|-----------|
|
||||||
| **Role = pure data** | Decouples definition from execution; same role with different agents |
|
| **YAML workflow definitions** | Human-readable, versionable, no build step required. JSON Schema inline in YAML, registered as CAS nodes on `workflow put`. |
|
||||||
| **Agent bound at runtime** | `WorkflowDefinition` is reusable; agent choice is deployment concern |
|
| **Stateless single-step CLI** | Each `uwf thread step` is atomic — no in-memory state, no daemon, no long-running process. OS handles lifecycle. |
|
||||||
| **Three-phase context** | Each phase sees only what it needs; types live in `workflow-protocol` |
|
| **CAS-backed thread state** | Immutable linked nodes enable fork, replay, and GC without copying data. Content-addressed deduplication across threads. |
|
||||||
| **`WorkflowRuntime.extract` + CAS `contentHash`** | Large agent bodies deduplicated globally; Merkle roots summarize threads |
|
| **JSONata moderator** | Declarative condition expressions evaluated against thread history. No LLM cost for routing decisions. |
|
||||||
| **`workflow-reactor` split** | LLM tool-calling loop isolated from filesystem/registry concerns |
|
| **Frontmatter markdown output** | Agents produce structured meta (YAML frontmatter) alongside free-form content (markdown body). Enables zero-cost extraction when frontmatter is well-formed. |
|
||||||
| **Single-file ESM** | Hash = version, self-contained bundle |
|
| **Two-layer extract** | Fast path avoids LLM calls when agents follow the format; LLM fallback handles messy output gracefully. |
|
||||||
| **No daemon** | OS handles process lifecycle |
|
| **Prompt injection for format** | Output format instruction prepended to system prompt ensures agents produce parseable output without per-agent configuration. |
|
||||||
| **Crockford Base32** | Filesystem-safe, readable, compact |
|
| **JSON Schema (not Zod)** | Schemas are CAS-native data — storable, hashable, validatable through `json-cas`. No code generation, no runtime library dependency. |
|
||||||
| **21-package split** | Clear boundaries: protocol ↔ runtime author API ↔ util/CAS/register ↔ execute ↔ CLI ↔ agents/templates/UI |
|
| **Agent as external command** | Agents are independent CLI binaries (`uwf-hermes`, `uwf-cursor`). Swappable per workflow/role via config. No tight coupling to the engine. |
|
||||||
|
| **No daemon** | Process starts, does one step, exits. Simpler failure model, no connection management. |
|
||||||
|
| **Crockford Base32** | Filesystem-safe, case-insensitive, readable, compact. |
|
||||||
|
|||||||
+33
-21
@@ -112,8 +112,8 @@ uwf-hermes <thread-id> <role>
|
|||||||
|
|
||||||
**约定:**
|
**约定:**
|
||||||
- `uwf step` 负责 moderator 决策,将 role 传给 agent CLI
|
- `uwf step` 负责 moderator 决策,将 role 传给 agent CLI
|
||||||
- agent-kit 根据 thread + role 从 CAS 读 systemPrompt / outputSchema
|
- agent-kit 根据 thread + role 从 CAS 读 goal / capabilities / procedure / output / meta
|
||||||
- agent-kit 组装完整 prompt(role systemPrompt + thread context + user prompt from StartNode)
|
- agent-kit 组装完整 prompt(role goal/capabilities/procedure/output + thread context + user prompt from StartNode)
|
||||||
- agent 执行实际逻辑,agent-kit 负责 extract
|
- agent 执行实际逻辑,agent-kit 负责 extract
|
||||||
- agent 将 StepNode 写入 CAS(含 output、detail、agent、prev),但**不挪链头指针**
|
- agent 将 StepNode 写入 CAS(含 output、detail、agent、prev),但**不挪链头指针**
|
||||||
- stdout 输出新 StepNode 的 CAS hash(纯文本,一行)
|
- stdout 输出新 StepNode 的 CAS hash(纯文本,一行)
|
||||||
@@ -143,7 +143,7 @@ uwf-hermes <thread-id> <role>
|
|||||||
|
|
||||||
#### `Workflow`
|
#### `Workflow`
|
||||||
|
|
||||||
Roles 和 moderator 内联在 Workflow 中,只有 outputSchema 独立为 CAS 节点(方便 json-cas 校验)。
|
Roles 和 moderator 内联在 Workflow 中,只有 meta 独立为 CAS 节点(方便 json-cas 校验)。
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
type: <workflow-schema-hash>
|
type: <workflow-schema-hash>
|
||||||
@@ -153,16 +153,25 @@ payload:
|
|||||||
roles:
|
roles:
|
||||||
planner:
|
planner:
|
||||||
description: "Creates implementation plan"
|
description: "Creates implementation plan"
|
||||||
systemPrompt: "You are a planning agent..."
|
goal: "You are a planning agent..."
|
||||||
outputSchema: "5GWKR8TN1V3JA" # cas_ref → JSON Schema 节点(json-cas 内置)
|
capabilities: [planning, issue-analysis]
|
||||||
|
procedure: "Analyze the issue and create a plan."
|
||||||
|
output: "Output the plan summary."
|
||||||
|
meta: "5GWKR8TN1V3JA" # cas_ref → JSON Schema 节点(json-cas 内置)
|
||||||
developer:
|
developer:
|
||||||
description: "Implements code changes"
|
description: "Implements code changes"
|
||||||
systemPrompt: "You are a developer agent..."
|
goal: "You are a developer agent..."
|
||||||
outputSchema: "8CNWT4KR6D1HV" # cas_ref → JSON Schema 节点
|
capabilities: [file-edit, shell]
|
||||||
|
procedure: "Implement the plan."
|
||||||
|
output: "List all files changed."
|
||||||
|
meta: "8CNWT4KR6D1HV" # cas_ref → JSON Schema 节点
|
||||||
reviewer:
|
reviewer:
|
||||||
description: "Reviews code changes"
|
description: "Reviews code changes"
|
||||||
systemPrompt: "You are a code reviewer..."
|
goal: "You are a code reviewer..."
|
||||||
outputSchema: "1VPBG9SM5E7WK" # cas_ref → JSON Schema 节点
|
capabilities: [code-review]
|
||||||
|
procedure: "Review the implementation."
|
||||||
|
output: "Approve or reject with comments."
|
||||||
|
meta: "1VPBG9SM5E7WK" # cas_ref → JSON Schema 节点
|
||||||
conditions:
|
conditions:
|
||||||
needsClarification:
|
needsClarification:
|
||||||
description: "Planner requests clarification from user"
|
description: "Planner requests clarification from user"
|
||||||
@@ -189,7 +198,7 @@ payload:
|
|||||||
condition: null
|
condition: null
|
||||||
```
|
```
|
||||||
|
|
||||||
- `roles` — 内联定义,每个 role 的 `outputSchema` 是独立的 cas_ref(指向 json-cas 内置 JSON Schema 节点)
|
- `roles` — 内联定义,每个 role 的 `meta` 是独立的 cas_ref(指向 json-cas 内置 JSON Schema 节点)
|
||||||
- `conditions` — `Record<Name, JSONata>`,命名条件,方便画图描述
|
- `conditions` — `Record<Name, JSONata>`,命名条件,方便画图描述
|
||||||
- `graph` — `Record<Role | "$START", Transition[]>`,每个 Transition = `{ role, condition }`
|
- `graph` — `Record<Role | "$START", Transition[]>`,每个 Transition = `{ role, condition }`
|
||||||
- `condition` 引用 conditions 中的 key,`null` = fallback
|
- `condition` 引用 conditions 中的 key,`null` = fallback
|
||||||
@@ -234,14 +243,14 @@ payload:
|
|||||||
start: "4TNVW8KR2B3MA" # cas_ref → StartNode(每个 step 都引用)
|
start: "4TNVW8KR2B3MA" # cas_ref → StartNode(每个 step 都引用)
|
||||||
prev: "2MXBG6PN4A8JR" # cas_ref → 前一个 StepNode,第一步为 null
|
prev: "2MXBG6PN4A8JR" # cas_ref → 前一个 StepNode,第一步为 null
|
||||||
role: "developer"
|
role: "developer"
|
||||||
output: "9KRVW3TN5F1QA" # cas_ref → 结构化输出节点(符合 role 的 outputSchema)
|
output: "9KRVW3TN5F1QA" # cas_ref → 结构化输出节点(符合 role 的 meta schema)
|
||||||
detail: "7BQST3VW9F2MA" # cas_ref → 执行详情(content node / 子 workflow terminal StepNode / ...)
|
detail: "7BQST3VW9F2MA" # cas_ref → 执行详情(content node / 子 workflow terminal StepNode / ...)
|
||||||
agent: "uwf-cursor" # 实际使用的 agent 命令(纯字符串)
|
agent: "uwf-cursor" # 实际使用的 agent 命令(纯字符串)
|
||||||
```
|
```
|
||||||
|
|
||||||
- `start` — 每个 StepNode 都直接引用 StartNode,方便随机访问
|
- `start` — 每个 StepNode 都直接引用 StartNode,方便随机访问
|
||||||
- `prev` — 前一个 StepNode 的 cas_ref,第一步为 `null`(不指向 StartNode)
|
- `prev` — 前一个 StepNode 的 cas_ref,第一步为 `null`(不指向 StartNode)
|
||||||
- `output` — cas_ref,指向符合 role outputSchema 的 CAS 节点,可用 json-cas 校验
|
- `output` — cas_ref,指向符合 role meta schema 的 CAS 节点,可用 json-cas 校验
|
||||||
- `detail` — cas_ref,指向执行详情。可以是原始 agent 输出(content node),也可以是子 workflow thread 的 terminal StepNode(workflowAsAgent 场景)
|
- `detail` — cas_ref,指向执行详情。可以是原始 agent 输出(content node),也可以是子 workflow thread 的 terminal StepNode(workflowAsAgent 场景)
|
||||||
- `agent` — 纯字符串,不是 CAS 节点
|
- `agent` — 纯字符串,不是 CAS 节点
|
||||||
|
|
||||||
@@ -340,12 +349,12 @@ OPENROUTER_API_KEY=sk-or-...
|
|||||||
|
|
||||||
```
|
```
|
||||||
packages/
|
packages/
|
||||||
├── cli-uwf/ # @uncaged/cli-uwf — uwf CLI(thread/workflow 命令)
|
├── cli-workflow/ # @uncaged/cli-workflow — uwf CLI(thread/workflow 命令)
|
||||||
├── uwf-moderator/ # @uncaged/uwf-moderator — JSONata moderator 引擎
|
├── workflow-moderator/ # @uncaged/workflow-moderator — JSONata moderator 引擎
|
||||||
├── uwf-agent-kit/ # @uncaged/uwf-agent-kit — Agent CLI 框架(含 extractor)
|
├── workflow-agent-kit/ # @uncaged/workflow-agent-kit — Agent CLI 框架(含 extractor)
|
||||||
├── uwf-agent-hermes/ # @uncaged/uwf-agent-hermes — uwf-hermes CLI
|
├── workflow-agent-hermes/ # @uncaged/workflow-agent-hermes — uwf-hermes CLI
|
||||||
├── uwf-agent-cursor/ # @uncaged/uwf-agent-cursor — uwf-cursor CLI
|
├── workflow-agent-cursor/ # @uncaged/workflow-agent-cursor — uwf-cursor CLI
|
||||||
└── uwf-protocol/ # @uncaged/uwf-protocol — 共享类型定义
|
└── workflow-protocol/ # @uncaged/workflow-protocol — 共享类型定义
|
||||||
```
|
```
|
||||||
|
|
||||||
**外部依赖:**
|
**外部依赖:**
|
||||||
@@ -372,7 +381,7 @@ type ThreadId = string;
|
|||||||
/** 一个 step 的核心数据,被 StepNode payload 和 JSONata 上下文共享 */
|
/** 一个 step 的核心数据,被 StepNode payload 和 JSONata 上下文共享 */
|
||||||
type StepRecord = {
|
type StepRecord = {
|
||||||
role: string;
|
role: string;
|
||||||
output: CasRef; // cas_ref → 结构化输出节点(符合 role outputSchema)
|
output: CasRef; // cas_ref → 结构化输出节点(符合 role meta schema)
|
||||||
detail: CasRef; // cas_ref → 执行详情(content node / 子 workflow terminal StepNode)
|
detail: CasRef; // cas_ref → 执行详情(content node / 子 workflow terminal StepNode)
|
||||||
agent: string; // 实际使用的 agent 命令(纯字符串)
|
agent: string; // 实际使用的 agent 命令(纯字符串)
|
||||||
};
|
};
|
||||||
@@ -383,8 +392,11 @@ type StepRecord = {
|
|||||||
```typescript
|
```typescript
|
||||||
type RoleDefinition = {
|
type RoleDefinition = {
|
||||||
description: string;
|
description: string;
|
||||||
systemPrompt: string;
|
goal: string;
|
||||||
outputSchema: CasRef; // cas_ref → json-cas 内置 JSON Schema 节点
|
capabilities: string[];
|
||||||
|
procedure: string;
|
||||||
|
output: string;
|
||||||
|
meta: CasRef; // cas_ref → json-cas 内置 JSON Schema 节点
|
||||||
};
|
};
|
||||||
|
|
||||||
type Transition = {
|
type Transition = {
|
||||||
|
|||||||
@@ -0,0 +1,43 @@
|
|||||||
|
name: "analyze-topic"
|
||||||
|
description: "Single-role topic analysis using four-phase role description"
|
||||||
|
roles:
|
||||||
|
analyst:
|
||||||
|
description: "Analyzes a given topic and produces a structured summary"
|
||||||
|
goal: |
|
||||||
|
You are a research analyst with expertise in breaking down complex topics
|
||||||
|
into clear, structured summaries. You think critically and cite key points.
|
||||||
|
capabilities:
|
||||||
|
- research
|
||||||
|
- critical-thinking
|
||||||
|
- structured-writing
|
||||||
|
procedure: |
|
||||||
|
Analyze the topic by:
|
||||||
|
1. Identifying the main thesis or question
|
||||||
|
2. Listing 3-5 key points with brief explanations
|
||||||
|
3. Noting any counterarguments or caveats
|
||||||
|
Keep your analysis concise (under 500 words).
|
||||||
|
output: |
|
||||||
|
Provide your analysis as markdown under the frontmatter.
|
||||||
|
The frontmatter must include your structured findings.
|
||||||
|
frontmatter:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
thesis:
|
||||||
|
type: string
|
||||||
|
keyPoints:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
caveats:
|
||||||
|
type: string
|
||||||
|
required: [thesis, keyPoints]
|
||||||
|
conditions: {}
|
||||||
|
graph:
|
||||||
|
$START:
|
||||||
|
- role: "analyst"
|
||||||
|
condition: null
|
||||||
|
prompt: "Analyze the topic in the task and produce a structured summary with key points."
|
||||||
|
analyst:
|
||||||
|
- role: "$END"
|
||||||
|
condition: null
|
||||||
|
prompt: "Analysis complete. Finish the workflow."
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
name: "solve-issue"
|
||||||
|
description: "End-to-end issue resolution"
|
||||||
|
roles:
|
||||||
|
planner:
|
||||||
|
description: "Creates implementation plan"
|
||||||
|
goal: "You are a planning agent. You analyze issues and create step-by-step plans."
|
||||||
|
capabilities:
|
||||||
|
- issue-analysis
|
||||||
|
- planning
|
||||||
|
procedure: "Analyze the issue and create a detailed, actionable implementation plan."
|
||||||
|
output: "Output the plan summary and list of concrete steps."
|
||||||
|
frontmatter:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
plan:
|
||||||
|
type: string
|
||||||
|
steps:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
required: [plan, steps]
|
||||||
|
developer:
|
||||||
|
description: "Implements code changes"
|
||||||
|
goal: "You are a developer agent. You implement code changes according to plans."
|
||||||
|
capabilities:
|
||||||
|
- file-edit
|
||||||
|
- shell
|
||||||
|
- testing
|
||||||
|
procedure: "Implement the plan. Write code, tests, and ensure existing tests pass."
|
||||||
|
output: "List all files changed and provide a summary of the implementation."
|
||||||
|
frontmatter:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
filesChanged:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
summary:
|
||||||
|
type: string
|
||||||
|
required: [filesChanged, summary]
|
||||||
|
reviewer:
|
||||||
|
description: "Reviews code changes"
|
||||||
|
goal: "You are a code reviewer. You review implementations for correctness and quality."
|
||||||
|
capabilities:
|
||||||
|
- code-review
|
||||||
|
- static-analysis
|
||||||
|
procedure: "Review the implementation against the plan. Check for bugs, edge cases, and style."
|
||||||
|
output: "Approve or reject with detailed comments explaining your decision."
|
||||||
|
frontmatter:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
approved:
|
||||||
|
type: boolean
|
||||||
|
comments:
|
||||||
|
type: string
|
||||||
|
required: [approved, comments]
|
||||||
|
conditions:
|
||||||
|
notApproved:
|
||||||
|
description: "Reviewer rejected the implementation"
|
||||||
|
expression: "$last('reviewer').approved = false"
|
||||||
|
graph:
|
||||||
|
$START:
|
||||||
|
- role: "planner"
|
||||||
|
condition: null
|
||||||
|
prompt: "Analyze the issue described in the task and produce a detailed implementation plan."
|
||||||
|
planner:
|
||||||
|
- role: "developer"
|
||||||
|
condition: null
|
||||||
|
prompt: "Implement the plan from the planner. Write code, tests, and ensure existing tests pass."
|
||||||
|
developer:
|
||||||
|
- role: "reviewer"
|
||||||
|
condition: null
|
||||||
|
prompt: "Review the developer's implementation against the plan for correctness and quality."
|
||||||
|
reviewer:
|
||||||
|
- role: "developer"
|
||||||
|
condition: "notApproved"
|
||||||
|
prompt: "The reviewer rejected your implementation. Read their feedback and fix the issues."
|
||||||
|
- role: "$END"
|
||||||
|
condition: null
|
||||||
|
prompt: "The review passed. Complete the workflow."
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
{
|
||||||
|
"name": "@uncaged/cli-workflow",
|
||||||
|
"version": "0.5.0-alpha.4",
|
||||||
|
"files": [
|
||||||
|
"src",
|
||||||
|
"dist",
|
||||||
|
"package.json"
|
||||||
|
],
|
||||||
|
"type": "module",
|
||||||
|
"bin": {
|
||||||
|
"uncaged-workflow": "src/cli.ts"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@uncaged/workflow-gateway": "workspace:^",
|
||||||
|
"@uncaged/workflow-protocol": "workspace:^",
|
||||||
|
"@uncaged/workflow-util": "workspace:^",
|
||||||
|
"@uncaged/workflow-cas": "workspace:^",
|
||||||
|
"@uncaged/workflow-execute": "workspace:^",
|
||||||
|
"@uncaged/workflow-register": "workspace:^",
|
||||||
|
"@uncaged/workflow-runtime": "workspace:^",
|
||||||
|
"hono": "^4.12.18",
|
||||||
|
"yaml": "^2.8.4"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"test": "bun test"
|
||||||
|
},
|
||||||
|
"publishConfig": {
|
||||||
|
"access": "public"
|
||||||
|
}
|
||||||
|
}
|
||||||
Executable
+9
@@ -0,0 +1,9 @@
|
|||||||
|
#!/usr/bin/env bun
|
||||||
|
|
||||||
|
import { runCli } from "./cli-dispatch.js";
|
||||||
|
import { resolveWorkflowStorageRoot } from "./storage-env.js";
|
||||||
|
|
||||||
|
const argv = process.argv.slice(2);
|
||||||
|
const storageRoot = resolveWorkflowStorageRoot();
|
||||||
|
const code = await runCli(storageRoot, argv);
|
||||||
|
process.exit(code);
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user