Compare commits
29 Commits
fde87b6274
...
eval@0.1.4
| Author | SHA1 | Date | |
|---|---|---|---|
| e354fc4341 | |||
| 0e7e3ea44b | |||
| aa454c85dd | |||
| 6dd7d521be | |||
| 950dc056d8 | |||
| d360b85374 | |||
| 509dfad857 | |||
| 58b84e3b3c | |||
| f821ac99f4 | |||
| 2c4700c49f | |||
| 4410afcd4a | |||
| a0e254a681 | |||
| dd77b40f6c | |||
| 5ed6f68e4b | |||
| 1ed0bf1f76 | |||
| d97840cf8d | |||
| b560818f1a | |||
| f989dee85b | |||
| 7e4a59de7e | |||
| 68079cc003 | |||
| 1a37928bb9 | |||
| 57511a93fe | |||
| adc3982a4a | |||
| 4580388270 | |||
| caba82fe36 | |||
| 6aee2ed5ef | |||
| 709b9dc1e5 | |||
| 7a788a9d90 | |||
| e5af5e9027 |
@@ -1,9 +0,0 @@
|
||||
---
|
||||
"@united-workforce/cli": patch
|
||||
---
|
||||
|
||||
fix: expand bootstrap prompt with full onboarding and upgrade guide
|
||||
|
||||
Bootstrap now covers two scenarios:
|
||||
- Fresh install: CLI + adapter installation, `uwf setup` configuration, skill installation, end-to-end verification
|
||||
- Upgrade: package update, skill regeneration, breaking change migrations (e.g. $START new/resume)
|
||||
@@ -1,8 +0,0 @@
|
||||
---
|
||||
"@united-workforce/cli": patch
|
||||
---
|
||||
|
||||
fix: bootstrap adds Step 0 environment pre-flight check
|
||||
|
||||
- Pre-flight checks for node, pnpm/npm, global bin PATH, hermes CLI with FIX instructions (#112)
|
||||
- Install commands changed from npm to pnpm (with npm fallback)
|
||||
@@ -1,9 +0,0 @@
|
||||
---
|
||||
"@united-workforce/cli": patch
|
||||
"@united-workforce/util": patch
|
||||
---
|
||||
|
||||
fix: workflow-authoring flat schema example uses enum, bootstrap adds PATH guidance
|
||||
|
||||
- workflow-authoring: flat schema example uses `enum: [done]` instead of bare `const` (#110.3)
|
||||
- bootstrap: adds `which hermes` check and PATH guidance for venv installs (#110.4)
|
||||
@@ -1,10 +0,0 @@
|
||||
---
|
||||
"@united-workforce/cli": patch
|
||||
---
|
||||
|
||||
fix: preset provider base-url auto-fill, bootstrap ACP docs, friendlier name mismatch error
|
||||
|
||||
- `uwf setup --provider dashscope` now auto-fills `--base-url` from preset list (#106)
|
||||
- Bootstrap guide documents uwf-hermes ACP dependency (`pip install hermes-agent[acp]`) (#107)
|
||||
- Bootstrap verify step uses inline workflow instead of missing `examples/eval-simple.yaml` (#107)
|
||||
- Workflow filename mismatch error now suggests how to fix it (#108)
|
||||
@@ -1,12 +0,0 @@
|
||||
---
|
||||
"@united-workforce/cli": patch
|
||||
---
|
||||
|
||||
fix: setup UX improvements (#114)
|
||||
|
||||
- Setup validates adapter availability and prints install command if missing
|
||||
- Setup prints "Config saved to <path> ✓" on success
|
||||
- Spawn ENOENT gives actionable error ("not found in PATH" + which command)
|
||||
- SQLite ExperimentalWarning suppressed via NODE_OPTIONS in spawned processes
|
||||
- Bootstrap VERSION reads cli package version (was reading util version)
|
||||
- Bootstrap PATH guidance is shell-agnostic (no hardcoded .bashrc/.profile)
|
||||
@@ -1,9 +0,0 @@
|
||||
---
|
||||
"@united-workforce/cli": minor
|
||||
"@united-workforce/util": patch
|
||||
---
|
||||
|
||||
feat: replace $START `_` status with `new`/`resume` semantics
|
||||
|
||||
BREAKING: All workflow YAML files must update `$START._` to `$START.new` + `$START.resume`.
|
||||
The `resume` edge prompt replaces the previously hardcoded resume message in the CLI.
|
||||
+124
-56
@@ -1,63 +1,131 @@
|
||||
name: "debate"
|
||||
description: "Structured debate between two sides. Tests cross-process session resume."
|
||||
name: debate
|
||||
description: "Multi-role structured debate with critical thinking framework and host summary."
|
||||
|
||||
# Shared frontmatter schema for debater roles (YAML anchor)
|
||||
x-debater-frontmatter: &debater-frontmatter
|
||||
type: object
|
||||
oneOf:
|
||||
- properties:
|
||||
$status: { const: speak }
|
||||
argument: { type: string }
|
||||
required: [$status, argument]
|
||||
- properties:
|
||||
$status: { const: conceded }
|
||||
reason: { type: string }
|
||||
required: [$status, reason]
|
||||
- properties:
|
||||
$status: { const: final }
|
||||
closing: { type: string }
|
||||
required: [$status, closing]
|
||||
|
||||
roles:
|
||||
against:
|
||||
description: "Argues against the proposition"
|
||||
goal: |
|
||||
You are a skilled debater arguing AGAINST the proposition.
|
||||
Be logical, cite evidence, and directly address your opponent's points.
|
||||
Keep each argument concise (under 200 words).
|
||||
capabilities:
|
||||
- argumentation
|
||||
- critical-thinking
|
||||
proponent:
|
||||
description: "Argues FOR the proposition"
|
||||
goal: "Build a compelling case for the proposition through logical reasoning and evidence"
|
||||
capabilities: []
|
||||
procedure: |
|
||||
1. If this is the opening, present your strongest argument against the proposition.
|
||||
2. If responding to the other side, directly counter their points with evidence and logic.
|
||||
3. If you find yourself genuinely convinced by the other side, you may concede.
|
||||
output: |
|
||||
Provide your argument in the frontmatter.
|
||||
Set status to "conceded" ONLY if you are genuinely convinced and wish to stop debating.
|
||||
Otherwise set status to "continue".
|
||||
You are an experienced scholar arguing FOR the proposition.
|
||||
|
||||
## Critical Thinking Framework (execute before every speech)
|
||||
|
||||
### A. Pre-speech reflection (internal, do not output)
|
||||
- Does every step in my argument chain hold? Any hidden assumptions or logical gaps?
|
||||
- If I were my opponent, how would I attack this? Where am I weakest?
|
||||
- Does my evidence actually support my claim, or could it backfire?
|
||||
- Should I go on offense or defense this round?
|
||||
|
||||
### B. Evidence discipline
|
||||
- Verify key numbers — watch for order-of-magnitude errors
|
||||
- Assess data freshness — fast-moving fields have short half-lives
|
||||
- Distinguish primary data from secondary citations, expert opinion, and common assumptions
|
||||
|
||||
### C. Anti-fragility
|
||||
- Anticipate counterarguments; preemptively strengthen or strategically abandon weak points
|
||||
- Catch logical gaps, data misuse, or outdated claims in your opponent's reasoning
|
||||
|
||||
## Rules
|
||||
1. Check Thread Progress to see how many times you have spoken.
|
||||
2. On your 3rd speech, you MUST output $status: final (closing statement).
|
||||
3. If genuinely convinced by the opponent, output $status: conceded.
|
||||
4. Otherwise output $status: speak and counter the opponent's points.
|
||||
5. Be rigorous, cite evidence, stay concise.
|
||||
output: "Debate argument"
|
||||
frontmatter: *debater-frontmatter
|
||||
|
||||
opponent:
|
||||
description: "Argues AGAINST the proposition"
|
||||
goal: "Build a compelling case against the proposition through logical reasoning and evidence"
|
||||
capabilities: []
|
||||
procedure: |
|
||||
You are an experienced scholar arguing AGAINST the proposition.
|
||||
|
||||
## Critical Thinking Framework (execute before every speech)
|
||||
|
||||
### A. Pre-speech reflection (internal, do not output)
|
||||
- Does every step in my argument chain hold? Any hidden assumptions or logical gaps?
|
||||
- If I were my opponent, how would I attack this? Where am I weakest?
|
||||
- Does my evidence actually support my claim, or could it backfire?
|
||||
- Should I go on offense or defense this round?
|
||||
|
||||
### B. Evidence discipline
|
||||
- Verify key numbers — watch for order-of-magnitude errors
|
||||
- Assess data freshness — fast-moving fields have short half-lives
|
||||
- Distinguish primary data from secondary citations, expert opinion, and common assumptions
|
||||
|
||||
### C. Anti-fragility
|
||||
- Anticipate counterarguments; preemptively strengthen or strategically abandon weak points
|
||||
- Catch logical gaps, data misuse, or outdated claims in your opponent's reasoning
|
||||
|
||||
## Rules
|
||||
1. Check Thread Progress to see how many times you have spoken.
|
||||
2. On your 3rd speech, or when the proponent has issued a final statement, you MUST output $status: final.
|
||||
3. If genuinely convinced by the proponent, output $status: conceded.
|
||||
4. Otherwise output $status: speak and counter the proponent's points.
|
||||
5. Be rigorous, cite evidence, stay concise.
|
||||
output: "Debate argument"
|
||||
frontmatter: *debater-frontmatter
|
||||
|
||||
host:
|
||||
description: "Debate moderator — delivers impartial summary and verdict"
|
||||
goal: "Objectively review the debate, analyze both sides, and deliver a verdict"
|
||||
capabilities: []
|
||||
procedure: |
|
||||
You are an experienced academic debate moderator.
|
||||
|
||||
## Task
|
||||
1. Outline each side's core arguments
|
||||
2. Evaluate reasoning quality and evidence use
|
||||
3. Highlight the most impactful exchanges
|
||||
4. Analyze the deeper significance of the topic
|
||||
5. Deliver an overall verdict
|
||||
|
||||
## Style
|
||||
- Impartial but with independent judgment
|
||||
- Substantive, not superficial
|
||||
output: "Debate summary report"
|
||||
frontmatter:
|
||||
type: object
|
||||
properties:
|
||||
$status:
|
||||
enum: ["continue", "conceded"]
|
||||
argument:
|
||||
type: string
|
||||
required: [$status, argument]
|
||||
for:
|
||||
description: "Argues for the proposition"
|
||||
goal: |
|
||||
You are a skilled debater arguing FOR the proposition.
|
||||
Be logical, cite evidence, and directly address your opponent's points.
|
||||
Keep each argument concise (under 200 words).
|
||||
capabilities:
|
||||
- argumentation
|
||||
- critical-thinking
|
||||
procedure: |
|
||||
1. Read the opposing side's latest argument carefully.
|
||||
2. Counter their points with evidence and logic.
|
||||
3. If you find yourself genuinely convinced by the other side, you may concede.
|
||||
output: |
|
||||
Provide your argument in the frontmatter.
|
||||
Set status to "conceded" ONLY if you are genuinely convinced and wish to stop debating.
|
||||
Otherwise set status to "continue".
|
||||
frontmatter:
|
||||
type: object
|
||||
properties:
|
||||
$status:
|
||||
enum: ["continue", "conceded"]
|
||||
argument:
|
||||
type: string
|
||||
required: [$status, argument]
|
||||
$status: { const: done }
|
||||
summary: { type: string }
|
||||
highlights: { type: string }
|
||||
verdict: { type: string }
|
||||
required: [$status, summary, highlights, verdict]
|
||||
|
||||
graph:
|
||||
$START:
|
||||
new: { role: "against", prompt: "Present your opening argument against the proposition." }
|
||||
resume: { role: "against", prompt: "Review the previous debate output and continue the argument against the proposition." }
|
||||
against:
|
||||
conceded: { role: "$END", prompt: "The against side conceded. Debate over." }
|
||||
continue: { role: "for", prompt: "Counter the opposing argument: {{{argument}}}" }
|
||||
for:
|
||||
conceded: { role: "$END", prompt: "The for side conceded. Debate over." }
|
||||
continue: { role: "against", prompt: "Counter the opposing argument: {{{argument}}}" }
|
||||
new: { role: proponent, prompt: "The debate begins. You are arguing FOR the proposition. Present your opening argument." }
|
||||
resume: { role: proponent, prompt: "The debate continues." }
|
||||
|
||||
proponent:
|
||||
speak: { role: opponent, prompt: "Proponent argues:\n\n{{{argument}}}\n\nYou are the opponent. Counter this argument." }
|
||||
conceded: { role: host, prompt: "The proponent conceded: {{{reason}}}\n\nPlease summarize the debate." }
|
||||
final: { role: opponent, prompt: "Proponent's closing statement:\n\n{{{closing}}}\n\nYou are the opponent. Deliver your final response." }
|
||||
|
||||
opponent:
|
||||
speak: { role: proponent, prompt: "Opponent argues:\n\n{{{argument}}}\n\nYou are the proponent. Counter this argument." }
|
||||
conceded: { role: host, prompt: "The opponent conceded: {{{reason}}}\n\nPlease summarize the debate." }
|
||||
final: { role: host, prompt: "Opponent's closing statement:\n\n{{{closing}}}\n\nThe debate is over. Please summarize." }
|
||||
|
||||
host:
|
||||
done: { role: "$END", prompt: "Summary complete." }
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@united-workforce/agent-builtin",
|
||||
"version": "0.1.1",
|
||||
"version": "0.1.2",
|
||||
"files": [
|
||||
"src",
|
||||
"dist",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/usr/bin/env node
|
||||
#!/usr/bin/env -S node --disable-warning=ExperimentalWarning
|
||||
|
||||
// eslint-disable-next-line -- dynamic import for version
|
||||
const pkg = await import("../package.json", { with: { type: "json" } });
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@united-workforce/agent-claude-code",
|
||||
"version": "0.1.1",
|
||||
"version": "0.1.3",
|
||||
"files": [
|
||||
"src",
|
||||
"dist",
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
type AgentRunResult,
|
||||
buildContinuationPrompt,
|
||||
buildRolePrompt,
|
||||
buildThreadProgress,
|
||||
createAgent,
|
||||
getCachedSessionId,
|
||||
setCachedSessionId,
|
||||
@@ -27,6 +28,10 @@ export function buildClaudeCodePrompt(ctx: AgentContext): string {
|
||||
if (ctx.outputFormatInstruction !== undefined && ctx.outputFormatInstruction !== "") {
|
||||
parts.push(ctx.outputFormatInstruction, "");
|
||||
}
|
||||
|
||||
// Inject thread progress so the agent knows step count and role visit count
|
||||
parts.push(buildThreadProgress(ctx.steps, ctx.role), "");
|
||||
|
||||
parts.push(rolePrompt, "", "## Task", ctx.start.prompt);
|
||||
|
||||
if (!ctx.isFirstVisit) {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/usr/bin/env node
|
||||
#!/usr/bin/env -S node --disable-warning=ExperimentalWarning
|
||||
|
||||
// eslint-disable-next-line -- dynamic import for version
|
||||
const pkg = await import("../package.json", { with: { type: "json" } });
|
||||
|
||||
@@ -15,7 +15,8 @@ describe("Issue #551 — bin entry & engines", () => {
|
||||
const pkg = JSON.parse(readFileSync(join(PKG_ROOT, "package.json"), "utf-8"));
|
||||
const binPath = pkg.bin["uwf-hermes"];
|
||||
const content = readFileSync(join(PKG_ROOT, binPath), "utf-8");
|
||||
expect(content.startsWith("#!/usr/bin/env node")).toBe(true);
|
||||
expect(content.startsWith("#!/usr/bin/env")).toBe(true);
|
||||
expect(content).toContain("node");
|
||||
});
|
||||
|
||||
test("README.md explains uwf-hermes is an adapter", () => {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@united-workforce/agent-hermes",
|
||||
"version": "0.1.2",
|
||||
"version": "0.1.4",
|
||||
"files": [
|
||||
"src",
|
||||
"dist",
|
||||
|
||||
@@ -12,7 +12,11 @@ const OWN_VERSION = (
|
||||
}
|
||||
).version;
|
||||
|
||||
const HERMES_COMMAND = "hermes";
|
||||
/** Resolve hermes binary: `UWF_HERMES_BIN` override → default `"hermes"` via PATH. */
|
||||
function resolveHermesCommand(): string {
|
||||
const override = process.env.UWF_HERMES_BIN;
|
||||
return override !== undefined && override !== "" ? override : "hermes";
|
||||
}
|
||||
const PROTOCOL_VERSION = 1;
|
||||
|
||||
type JsonRpcResponse = {
|
||||
@@ -271,7 +275,8 @@ export class HermesAcpClient {
|
||||
return;
|
||||
}
|
||||
|
||||
const child = spawn(HERMES_COMMAND, ["acp"], {
|
||||
const hermesCommand = resolveHermesCommand();
|
||||
const child = spawn(hermesCommand, ["acp"], {
|
||||
env: process.env,
|
||||
shell: false,
|
||||
stdio: ["pipe", "pipe", "pipe"],
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/usr/bin/env node
|
||||
#!/usr/bin/env -S node --disable-warning=ExperimentalWarning
|
||||
|
||||
// eslint-disable-next-line -- dynamic import for version
|
||||
const pkg = await import("../package.json", { with: { type: "json" } });
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
type AgentRunResult,
|
||||
buildContinuationPrompt,
|
||||
buildRolePrompt,
|
||||
buildThreadProgress,
|
||||
createAgent,
|
||||
} from "@united-workforce/util-agent";
|
||||
import type { AcpUsage } from "./acp-client.js";
|
||||
@@ -60,6 +61,9 @@ export function buildHermesPrompt(ctx: AgentContext): string {
|
||||
parts.push(ctx.outputFormatInstruction, "");
|
||||
}
|
||||
|
||||
// Inject thread progress so the agent knows step count and role visit count
|
||||
parts.push(buildThreadProgress(ctx.steps, ctx.role), "");
|
||||
|
||||
if (!ctx.isFirstVisit) {
|
||||
// Re-entry: show only steps since last visit, meta only
|
||||
parts.push(buildContinuationPrompt(ctx.steps, ctx.role, ctx.edgePrompt));
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@united-workforce/agent-mock",
|
||||
"version": "0.1.1",
|
||||
"version": "0.1.2",
|
||||
"files": [
|
||||
"src",
|
||||
"dist",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/usr/bin/env node
|
||||
#!/usr/bin/env -S node --disable-warning=ExperimentalWarning
|
||||
|
||||
// eslint-disable-next-line -- dynamic import for version
|
||||
const pkg = await import("../package.json", { with: { type: "json" } });
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@united-workforce/cli",
|
||||
"version": "0.2.0",
|
||||
"version": "0.3.0",
|
||||
"files": [
|
||||
"src",
|
||||
"dist",
|
||||
|
||||
@@ -28,9 +28,13 @@ roles:
|
||||
$status: "ready"
|
||||
frontmatter:
|
||||
type: object
|
||||
required: ["$status"]
|
||||
properties:
|
||||
$status: { type: string, enum: ["ready", "not-ready"] }
|
||||
oneOf:
|
||||
- properties:
|
||||
$status: { const: "ready" }
|
||||
required: ["$status"]
|
||||
- properties:
|
||||
$status: { const: "not-ready" }
|
||||
required: ["$status"]
|
||||
roleB:
|
||||
description: Second role
|
||||
goal: Do B
|
||||
@@ -42,7 +46,7 @@ roles:
|
||||
type: object
|
||||
required: ["$status"]
|
||||
properties:
|
||||
$status: { type: string, enum: ["done"] }
|
||||
$status: { const: "done" }
|
||||
graph:
|
||||
$START:
|
||||
new:
|
||||
@@ -82,9 +86,13 @@ roles:
|
||||
$status: "pass"
|
||||
frontmatter:
|
||||
type: object
|
||||
required: ["$status"]
|
||||
properties:
|
||||
$status: { type: string, enum: ["pass", "fail"] }
|
||||
oneOf:
|
||||
- properties:
|
||||
$status: { const: "pass" }
|
||||
required: ["$status"]
|
||||
- properties:
|
||||
$status: { const: "fail" }
|
||||
required: ["$status"]
|
||||
roleB:
|
||||
description: Pass role
|
||||
goal: Do B
|
||||
@@ -96,7 +104,7 @@ roles:
|
||||
type: object
|
||||
required: ["$status"]
|
||||
properties:
|
||||
$status: { type: string, enum: ["done"] }
|
||||
$status: { const: "done" }
|
||||
roleC:
|
||||
description: Fail role
|
||||
goal: Do C
|
||||
@@ -108,7 +116,7 @@ roles:
|
||||
type: object
|
||||
required: ["$status"]
|
||||
properties:
|
||||
$status: { type: string, enum: ["done"] }
|
||||
$status: { const: "done" }
|
||||
graph:
|
||||
$START:
|
||||
new:
|
||||
@@ -155,7 +163,7 @@ roles:
|
||||
type: object
|
||||
required: ["$status"]
|
||||
properties:
|
||||
$status: { type: string, enum: ["done"] }
|
||||
$status: { const: "done" }
|
||||
graph:
|
||||
$START:
|
||||
new:
|
||||
|
||||
@@ -54,7 +54,7 @@ roles:
|
||||
type: object
|
||||
required: ["$status"]
|
||||
properties:
|
||||
$status: { type: string, enum: ["ready"] }
|
||||
$status: { const: "ready" }
|
||||
graph:
|
||||
$START:
|
||||
new:
|
||||
@@ -114,7 +114,7 @@ roles:
|
||||
type: object
|
||||
required: ["$status"]
|
||||
properties:
|
||||
$status: { type: string, enum: ["ready"] }
|
||||
$status: { const: "ready" }
|
||||
graph:
|
||||
$START:
|
||||
new:
|
||||
@@ -161,7 +161,7 @@ roles:
|
||||
type: object
|
||||
required: ["$status"]
|
||||
properties:
|
||||
$status: { type: string, enum: ["ready"] }
|
||||
$status: { const: "ready" }
|
||||
graph:
|
||||
$START:
|
||||
new:
|
||||
|
||||
@@ -31,7 +31,7 @@ roles:
|
||||
type: object
|
||||
required: ["$status"]
|
||||
properties:
|
||||
$status: { type: string, enum: ["ready"] }
|
||||
$status: { const: "ready" }
|
||||
graph:
|
||||
$START:
|
||||
new:
|
||||
|
||||
@@ -54,7 +54,7 @@ roles:
|
||||
type: object
|
||||
required: ["$status"]
|
||||
properties:
|
||||
$status: { type: string, enum: ["ready"] }
|
||||
$status: { const: "ready" }
|
||||
graph:
|
||||
$START:
|
||||
new:
|
||||
|
||||
@@ -17,7 +17,7 @@ function makeWorkflow(overrides?: Partial<WorkflowPayload>): WorkflowPayload {
|
||||
frontmatter: {
|
||||
type: "object",
|
||||
properties: {
|
||||
$status: { enum: ["done"] },
|
||||
$status: { const: "done" },
|
||||
plan: { type: "string" },
|
||||
},
|
||||
required: ["$status", "plan"],
|
||||
@@ -85,7 +85,7 @@ describe("Suite 1: Role Reference Integrity", () => {
|
||||
output: "None",
|
||||
frontmatter: {
|
||||
type: "object",
|
||||
properties: { $status: { enum: ["done"] } },
|
||||
properties: { $status: { const: "done" } },
|
||||
required: ["$status"],
|
||||
} as unknown as string,
|
||||
};
|
||||
@@ -187,7 +187,7 @@ describe("Suite 2: Graph Structure", () => {
|
||||
output: "Isolated",
|
||||
frontmatter: {
|
||||
type: "object",
|
||||
properties: { $status: { enum: ["done"] } },
|
||||
properties: { $status: { const: "done" } },
|
||||
required: ["$status"],
|
||||
} as unknown as string,
|
||||
};
|
||||
@@ -272,8 +272,8 @@ describe("Suite 3: Status-Edge Consistency", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("Suite 3b: Enum-Based Multi-Exit", () => {
|
||||
test("3b.1 enum multi-exit passes with matching graph keys", () => {
|
||||
describe("Suite 3b: Enum-Based $status is Rejected", () => {
|
||||
test("3b.1 enum multi-exit is rejected (must use oneOf + const)", () => {
|
||||
const wf = makeWorkflow();
|
||||
wf.roles.reviewer = {
|
||||
...wf.roles.reviewer,
|
||||
@@ -291,52 +291,10 @@ describe("Suite 3b: Enum-Based Multi-Exit", () => {
|
||||
rejected: { role: "writer", prompt: "Fix: {{{comments}}}", location: null },
|
||||
};
|
||||
const errors = validateWorkflow(wf);
|
||||
expect(errors).toEqual([]);
|
||||
expect(errors.some((e) => e.includes("must define") && e.includes("const"))).toBe(true);
|
||||
});
|
||||
|
||||
test("3b.2 enum multi-exit with extra graph key", () => {
|
||||
const wf = makeWorkflow();
|
||||
wf.roles.reviewer = {
|
||||
...wf.roles.reviewer,
|
||||
frontmatter: {
|
||||
type: "object",
|
||||
properties: {
|
||||
$status: { enum: ["approved", "rejected"] },
|
||||
comments: { type: "string" },
|
||||
},
|
||||
required: ["$status", "comments"],
|
||||
} as unknown as string,
|
||||
};
|
||||
wf.graph.reviewer = {
|
||||
approved: { role: "$END", prompt: "Done", location: null },
|
||||
rejected: { role: "writer", prompt: "Fix", location: null },
|
||||
timeout: { role: "$END", prompt: "Timed out", location: null },
|
||||
};
|
||||
const errors = validateWorkflow(wf);
|
||||
expect(errors.some((e) => e.includes("extra status keys: timeout"))).toBe(true);
|
||||
});
|
||||
|
||||
test("3b.3 enum multi-exit with missing graph key", () => {
|
||||
const wf = makeWorkflow();
|
||||
wf.roles.reviewer = {
|
||||
...wf.roles.reviewer,
|
||||
frontmatter: {
|
||||
type: "object",
|
||||
properties: {
|
||||
$status: { enum: ["approved", "rejected"] },
|
||||
comments: { type: "string" },
|
||||
},
|
||||
required: ["$status", "comments"],
|
||||
} as unknown as string,
|
||||
};
|
||||
wf.graph.reviewer = {
|
||||
approved: { role: "$END", prompt: "Done", location: null },
|
||||
};
|
||||
const errors = validateWorkflow(wf);
|
||||
expect(errors.some((e) => e.includes("missing status keys: rejected"))).toBe(true);
|
||||
});
|
||||
|
||||
test("3b.4 enum with single explicit value passes", () => {
|
||||
test("3b.2 enum single-exit is rejected (must use const)", () => {
|
||||
const wf = makeWorkflow();
|
||||
wf.roles.writer = {
|
||||
...wf.roles.writer,
|
||||
@@ -351,28 +309,71 @@ describe("Suite 3b: Enum-Based Multi-Exit", () => {
|
||||
};
|
||||
wf.graph.writer = { ready: { role: "reviewer", prompt: "Review: {{{plan}}}", location: null } };
|
||||
const errors = validateWorkflow(wf);
|
||||
expect(errors).toEqual([]);
|
||||
expect(errors.some((e) => e.includes("must define") && e.includes("const"))).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
test("3b.5 enum multi-exit mustache var not in frontmatter", () => {
|
||||
describe("Suite 3c: Const-Based Flat Schema", () => {
|
||||
test("3c.1 flat schema with const $status passes validation", () => {
|
||||
const wf = makeWorkflow();
|
||||
wf.roles.reviewer = {
|
||||
...wf.roles.reviewer,
|
||||
wf.roles.writer = {
|
||||
...wf.roles.writer,
|
||||
frontmatter: {
|
||||
type: "object",
|
||||
properties: {
|
||||
$status: { enum: ["approved", "rejected"] },
|
||||
comments: { type: "string" },
|
||||
$status: { const: "done" },
|
||||
plan: { type: "string" },
|
||||
},
|
||||
required: ["$status", "comments"],
|
||||
required: ["$status", "plan"],
|
||||
} as unknown as string,
|
||||
};
|
||||
wf.graph.reviewer = {
|
||||
approved: { role: "$END", prompt: "Done: {{{nonexistent}}}", location: null },
|
||||
rejected: { role: "writer", prompt: "Fix: {{{comments}}}", location: null },
|
||||
const errors = validateWorkflow(wf);
|
||||
expect(errors).toEqual([]);
|
||||
});
|
||||
|
||||
test("3c.2 flat schema with const $status detects extra graph key", () => {
|
||||
const wf = makeWorkflow();
|
||||
wf.roles.writer = {
|
||||
...wf.roles.writer,
|
||||
frontmatter: {
|
||||
type: "object",
|
||||
properties: {
|
||||
$status: { const: "done" },
|
||||
plan: { type: "string" },
|
||||
},
|
||||
required: ["$status", "plan"],
|
||||
} as unknown as string,
|
||||
};
|
||||
wf.graph.writer = {
|
||||
done: { role: "reviewer", prompt: "Review.", location: null },
|
||||
extra: { role: "$END", prompt: "Nope.", location: null },
|
||||
};
|
||||
const errors = validateWorkflow(wf);
|
||||
expect(errors.some((e) => e.includes("nonexistent") && e.includes("not found"))).toBe(true);
|
||||
expect(errors.some((e) => e.includes("extra status keys") && e.includes("extra"))).toBe(true);
|
||||
});
|
||||
|
||||
test("3c.3 flat schema with const $status validates mustache vars", () => {
|
||||
const wf = makeWorkflow();
|
||||
wf.roles.writer = {
|
||||
...wf.roles.writer,
|
||||
frontmatter: {
|
||||
type: "object",
|
||||
properties: {
|
||||
$status: { const: "done" },
|
||||
plan: { type: "string" },
|
||||
},
|
||||
required: ["$status", "plan"],
|
||||
} as unknown as string,
|
||||
};
|
||||
wf.graph.writer = {
|
||||
done: { role: "reviewer", prompt: "Review: {{{nonexistent}}}", location: null },
|
||||
};
|
||||
const errors = validateWorkflow(wf);
|
||||
expect(
|
||||
errors.some(
|
||||
(e) => e.includes('prompt variable "nonexistent"') && e.includes('role "writer"'),
|
||||
),
|
||||
).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -480,7 +481,7 @@ describe("Suite 6: Multiple Errors Collection", () => {
|
||||
output: "None",
|
||||
frontmatter: {
|
||||
type: "object",
|
||||
properties: { $status: { enum: ["done"] } },
|
||||
properties: { $status: { const: "done" } },
|
||||
required: ["$status"],
|
||||
} as unknown as string,
|
||||
};
|
||||
|
||||
@@ -31,7 +31,7 @@ function makeMinimalPayload(name: string, description: string): WorkflowPayload
|
||||
frontmatter: {
|
||||
type: "object",
|
||||
properties: {
|
||||
$status: { type: "string", enum: ["done"] },
|
||||
$status: { const: "done" },
|
||||
},
|
||||
required: ["$status"],
|
||||
} as unknown as CasRef,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/usr/bin/env node
|
||||
#!/usr/bin/env -S node --disable-warning=ExperimentalWarning
|
||||
|
||||
import type { CasRef, ThreadId, ThreadStatus } from "@united-workforce/protocol";
|
||||
import { Command } from "commander";
|
||||
@@ -542,7 +542,7 @@ prompt
|
||||
|
||||
program
|
||||
.command("setup")
|
||||
.description("Configure provider, model, and agent")
|
||||
.description("Configure provider, model, and agent. Run without options for interactive wizard.")
|
||||
.option("--provider <name>", "Provider name")
|
||||
.option("--base-url <url>", "OpenAI-compatible API base URL")
|
||||
.option("--api-key <key>", "API key")
|
||||
|
||||
@@ -78,48 +78,105 @@ npm prefix -g # global prefix; bin is <prefix>/bin
|
||||
# FIX: add the missing directory to your shell's startup file
|
||||
# (e.g. ~/.bashrc, ~/.zshrc, ~/.profile, or fish config):
|
||||
# export PATH="<global-bin-dir>:$PATH"
|
||||
|
||||
# 4. (uwf-hermes only) hermes CLI
|
||||
which hermes
|
||||
# FIX: if hermes is in a virtualenv, add it to PATH:
|
||||
# export PATH="$HOME/.hermes/hermes-agent/.venv/bin:$PATH"
|
||||
# or create a symlink: ln -s ~/.hermes/hermes-agent/.venv/bin/hermes ~/.local/bin/hermes
|
||||
\`\`\`
|
||||
|
||||
**All checks must pass before continuing.** If you had to modify PATH, verify the change persists by opening a new shell or sourcing your shell config.
|
||||
|
||||
### Step 1 — Install CLI and agent adapter
|
||||
### Step 1 — Discover agents and install adapter
|
||||
|
||||
**First, detect which supported agents are already installed on the user's machine:**
|
||||
|
||||
\`\`\`bash
|
||||
pnpm add -g @united-workforce/cli # or: npm install -g @united-workforce/cli
|
||||
uwf --version # should print ${CLI_VERSION}
|
||||
# Check for Hermes Agent
|
||||
which hermes 2>/dev/null && hermes --version
|
||||
|
||||
# Check for Claude Code
|
||||
which claude 2>/dev/null && claude --version # should show "X.Y.Z (Claude Code)"
|
||||
\`\`\`
|
||||
|
||||
Install an agent adapter (at least one is required):
|
||||
**Based on the results:**
|
||||
|
||||
| Adapter | Install | When to use |
|
||||
|---------|---------|-------------|
|
||||
| uwf-hermes | \`pnpm add -g @united-workforce/agent-hermes\` | When your agent framework is Hermes Agent |
|
||||
| uwf-claude-code | \`pnpm add -g @united-workforce/agent-claude-code\` | When using Claude Code CLI directly |
|
||||
| uwf-builtin | \`pnpm add -g @united-workforce/agent-builtin\` | Lightweight built-in agent (no external dependency) |
|
||||
- **Only hermes found** → install \`uwf-hermes\` adapter
|
||||
- **Only claude found** → install \`uwf-claude-code\` adapter
|
||||
- **Both found** → ask the user which agent they want uwf to use as default
|
||||
- **Neither found** → the user must install at least one agent first:
|
||||
- Hermes Agent: https://hermes-agent.nousresearch.com/docs
|
||||
- Claude Code: \`npm install -g @anthropic-ai/claude-code\`
|
||||
|
||||
**Install the uwf CLI and the chosen adapter** using pnpm or npm:
|
||||
|
||||
**uwf-hermes** also requires the Hermes ACP plugin. After installing \`hermes-agent\`, run:
|
||||
\`\`\`bash
|
||||
pip install hermes-agent[acp] # or: pip install -e .[acp] if installed from source
|
||||
# CLI (required)
|
||||
pnpm add -g @united-workforce/cli # or: npm install -g @united-workforce/cli
|
||||
|
||||
# Adapter — install the one matching the detected agent:
|
||||
pnpm add -g @united-workforce/agent-hermes # or: npm i -g @united-workforce/agent-hermes
|
||||
pnpm add -g @united-workforce/agent-claude-code # or: npm i -g @united-workforce/agent-claude-code
|
||||
\`\`\`
|
||||
|
||||
Verify the adapter is installed: \`uwf-hermes --version\` (or whichever you chose).
|
||||
**⚠ Adapter versions are independent from CLI versions.** Do NOT try to match adapter version to CLI version. Just install \`@latest\` (the default).
|
||||
|
||||
**After installing, verify that \`uwf\` and the adapter are available in PATH:**
|
||||
|
||||
\`\`\`bash
|
||||
uwf --version # should print ${CLI_VERSION}
|
||||
uwf-hermes --version # or: uwf-claude-code --version
|
||||
\`\`\`
|
||||
|
||||
If either command is not found, the global bin directory is not in the current shell's PATH. **You must fix this before continuing:**
|
||||
|
||||
1. Find where the binary was installed:
|
||||
\`\`\`bash
|
||||
find ~/.local ~/.hermes /usr/local -name uwf -type f 2>/dev/null
|
||||
npm prefix -g # global prefix — bin is <prefix>/bin
|
||||
\`\`\`
|
||||
2. Add the directory to PATH permanently by appending to the user's shell config (e.g. \`~/.bashrc\`, \`~/.zshrc\`, \`~/.profile\`, or fish config):
|
||||
\`\`\`bash
|
||||
export PATH="<global-bin-dir>:$PATH"
|
||||
\`\`\`
|
||||
3. Source the updated config or open a new shell, then re-verify the commands work.
|
||||
|
||||
**uwf-hermes** also requires the Hermes ACP plugin. Verify with \`hermes acp --help\`. If not available, install it:
|
||||
\`\`\`bash
|
||||
# Option A: install into hermes venv (recommended)
|
||||
source ~/.hermes/hermes-agent/.venv/bin/activate && pip install hermes-agent[acp]
|
||||
|
||||
# Option B: pipx
|
||||
pipx install 'hermes-agent[acp]'
|
||||
|
||||
# Option C: if installed from source
|
||||
pip install -e '.[acp]'
|
||||
\`\`\`
|
||||
|
||||
### Step 2 — Configure provider and model
|
||||
|
||||
uwf needs an LLM provider to run agents. **Ask the user** for their provider, API key, and model, then run:
|
||||
|
||||
\`\`\`bash
|
||||
uwf setup --provider <name> --base-url <url> --api-key <key> --model <model> [--agent <adapter>]
|
||||
uwf setup --provider <name> --api-key <key> --model <model> --agent <adapter-command>
|
||||
\`\`\`
|
||||
|
||||
Preset providers (base-url is auto-filled when using a preset name):
|
||||
openai, xai, openrouter, venice, dashscope, deepseek, siliconflow, volcengine, kimi, glm, stepfun, minimax, ollama
|
||||
**Note:** \`--agent\` takes the adapter **command name** (e.g. \`uwf-hermes\`), not the npm package name.
|
||||
|
||||
**Preset providers** — when using a preset name, \`--base-url\` is auto-filled and can be omitted:
|
||||
|
||||
| Provider | Name | Default base URL |
|
||||
|----------|------|-----------------|
|
||||
| OpenAI | \`openai\` | https://api.openai.com/v1 |
|
||||
| xAI | \`xai\` | https://api.x.ai/v1 |
|
||||
| OpenRouter | \`openrouter\` | https://openrouter.ai/api/v1 |
|
||||
| Venice | \`venice\` | https://api.venice.ai/api/v1 |
|
||||
| Dashscope | \`dashscope\` | https://dashscope.aliyuncs.com/compatible-mode/v1 |
|
||||
| DeepSeek | \`deepseek\` | https://api.deepseek.com/v1 |
|
||||
| SiliconFlow | \`siliconflow\` | https://api.siliconflow.cn/v1 |
|
||||
| VolcEngine | \`volcengine\` | https://ark.cn-beijing.volces.com/api/v3 |
|
||||
| Kimi (Moonshot) | \`kimi\` | https://api.moonshot.cn/v1 |
|
||||
| GLM (Zhipu AI) | \`glm\` | https://open.bigmodel.cn/api/paas/v4 |
|
||||
| StepFun | \`stepfun\` | https://api.stepfun.com/v1 |
|
||||
| MiniMax | \`minimax\` | https://api.minimax.io/v1 |
|
||||
| Ollama (local) | \`ollama\` | http://localhost:11434/v1 |
|
||||
|
||||
For **non-preset providers**, you must specify \`--base-url\` manually.
|
||||
|
||||
Example:
|
||||
\`\`\`bash
|
||||
@@ -144,6 +201,8 @@ Each command outputs a complete SKILL.md with YAML frontmatter. Use your agent f
|
||||
|
||||
Verify skills are installed by listing them (e.g. \`skills_list()\`) and confirming all three appear.
|
||||
|
||||
**⚠ After saving all skills, start a new session** so the agent loads the updated skill content. Skills saved in the current session are not active until the next session.
|
||||
|
||||
### Step 4 — Verify end-to-end
|
||||
|
||||
Create a minimal workflow file to test your setup:
|
||||
@@ -162,7 +221,7 @@ roles:
|
||||
frontmatter:
|
||||
type: object
|
||||
properties:
|
||||
$status: { enum: [done] }
|
||||
$status: { const: done }
|
||||
message: { type: string }
|
||||
required: [$status, message]
|
||||
graph:
|
||||
@@ -189,11 +248,25 @@ If the thread reaches \`$END\` with status \`completed\`, the setup is working.
|
||||
### Step 1 — Update packages
|
||||
|
||||
\`\`\`bash
|
||||
pnpm add -g @united-workforce/cli@latest # or: npm install -g @united-workforce/cli@latest
|
||||
uwf --version # should print ${CLI_VERSION}
|
||||
# Using pnpm
|
||||
pnpm add -g @united-workforce/cli@latest
|
||||
|
||||
# Also update your adapter(s)
|
||||
# Using npm
|
||||
npm install -g @united-workforce/cli@latest
|
||||
\`\`\`
|
||||
|
||||
\`\`\`bash
|
||||
uwf --version # should print ${CLI_VERSION}
|
||||
\`\`\`
|
||||
|
||||
Also update your adapter(s):
|
||||
|
||||
\`\`\`bash
|
||||
# pnpm
|
||||
pnpm add -g @united-workforce/agent-hermes@latest
|
||||
|
||||
# npm
|
||||
npm install -g @united-workforce/agent-hermes@latest
|
||||
\`\`\`
|
||||
|
||||
### Step 2 — Regenerate skills
|
||||
@@ -206,6 +279,8 @@ uwf prompt workflow-authoring # → update skill "uwf-workflow-authoring"
|
||||
uwf prompt adapter-developing # → update skill "uwf-adapter-developing"
|
||||
\`\`\`
|
||||
|
||||
**⚠ After updating skills, start a new session** to load the new skill content.
|
||||
|
||||
### Step 3 — Migrate workflow YAML files (if needed)
|
||||
|
||||
Check the changelog for breaking changes. Known migrations:
|
||||
@@ -224,6 +299,17 @@ Check the changelog for breaking changes. Known migrations:
|
||||
|
||||
Update all \`.workflow/\` and \`.workflows/\` YAML files in your projects. \`uwf workflow add\` will reject files with the old \`_\` syntax.
|
||||
|
||||
- **v0.2.1**: \`$status: { enum: [value] }\` → \`$status: { const: "value" }\`. The validator no longer accepts \`enum\` for \`$status\`. Update all workflow YAML files:
|
||||
\`\`\`yaml
|
||||
# Before (v0.2.0)
|
||||
$status: { enum: [done] }
|
||||
$status: { type: string, enum: ["ready", "failed"] }
|
||||
|
||||
# After (v0.2.1+)
|
||||
$status: { const: "done" }
|
||||
# For multi-exit, use oneOf with const (unchanged)
|
||||
\`\`\`
|
||||
|
||||
### Step 4 — Verify
|
||||
|
||||
\`\`\`bash
|
||||
|
||||
@@ -1001,12 +1001,6 @@ function spawnAgent(
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
maxBuffer: 50 * 1024 * 1024, // 50 MB — stream-json output can be large
|
||||
cwd,
|
||||
env: {
|
||||
...process.env,
|
||||
NODE_OPTIONS: [process.env.NODE_OPTIONS, "--disable-warning=ExperimentalWarning"]
|
||||
.filter(Boolean)
|
||||
.join(" "),
|
||||
},
|
||||
});
|
||||
} catch (e) {
|
||||
const err = e as NodeJS.ErrnoException & { stderr?: Buffer | string | null };
|
||||
@@ -1254,12 +1248,6 @@ async function cmdThreadStepBackground(
|
||||
const child = spawn(scriptPath, args, {
|
||||
detached: true,
|
||||
stdio: "ignore",
|
||||
env: {
|
||||
...process.env,
|
||||
NODE_OPTIONS: [process.env.NODE_OPTIONS, "--disable-warning=ExperimentalWarning"]
|
||||
.filter(Boolean)
|
||||
.join(" "),
|
||||
},
|
||||
});
|
||||
|
||||
child.unref();
|
||||
|
||||
@@ -24,22 +24,22 @@ function isOneOfSchema(fm: unknown): fm is SchemaObj & { oneOf: SchemaObj[] } {
|
||||
return Array.isArray(obj.oneOf);
|
||||
}
|
||||
|
||||
/** Check if a frontmatter schema declares "$status" as an enum (the required form for user roles). */
|
||||
function hasStatusEnum(fm: unknown): boolean {
|
||||
/** Check if a frontmatter schema declares "$status" as const (flat schema form). */
|
||||
function hasStatusConst(fm: unknown): boolean {
|
||||
if (typeof fm !== "object" || fm === null) return false;
|
||||
const obj = fm as SchemaObj;
|
||||
const props = obj.properties as Record<string, SchemaObj> | undefined;
|
||||
if (!props?.$status) return false;
|
||||
return Array.isArray(props.$status.enum);
|
||||
return typeof props.$status.const === "string";
|
||||
}
|
||||
|
||||
/** Extract status values from an enum-based $status field. */
|
||||
function getEnumStatuses(fm: SchemaObj): string[] {
|
||||
/** Extract status values from a const-based $status field. */
|
||||
function getConstStatuses(fm: SchemaObj): string[] {
|
||||
const props = fm.properties as Record<string, SchemaObj> | undefined;
|
||||
if (!props?.$status) return [];
|
||||
const statusDef = props.$status;
|
||||
if (!Array.isArray(statusDef.enum)) return [];
|
||||
return statusDef.enum as string[];
|
||||
if (typeof statusDef.const === "string") return [statusDef.const];
|
||||
return [];
|
||||
}
|
||||
|
||||
/** Get property names from a schema object. */
|
||||
@@ -248,21 +248,21 @@ function checkRoleConsistency(payload: WorkflowPayload, errors: string[]): void
|
||||
checkOneOfDiscriminant(roleName, variants, statuses, errors);
|
||||
checkStatusEdges(roleName, graphKeys, new Set(statuses), errors);
|
||||
checkMultiExitMustache(roleName, graphEntry, variants, errors);
|
||||
} else if (hasStatusEnum(fm)) {
|
||||
const statuses = getEnumStatuses(fm as SchemaObj);
|
||||
} else if (hasStatusConst(fm)) {
|
||||
const statuses = getConstStatuses(fm as SchemaObj);
|
||||
checkStatusEdges(roleName, graphKeys, new Set(statuses), errors);
|
||||
// For enum-based schemas, mustache vars come from the flat properties
|
||||
checkEnumMustache(roleName, graphEntry, fm as SchemaObj, errors);
|
||||
// For const-based flat schemas, mustache vars come from the flat properties
|
||||
checkFlatMustache(roleName, graphEntry, fm as SchemaObj, errors);
|
||||
} else {
|
||||
errors.push(
|
||||
`role "${roleName}" must define "$status" as an enum (or oneOf const) in frontmatter`,
|
||||
`role "${roleName}" must define "$status" as const (or oneOf with const) in frontmatter`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Check mustache vars in all edge prompts against flat schema properties. */
|
||||
function checkEnumMustache(
|
||||
function checkFlatMustache(
|
||||
roleName: string,
|
||||
graphEntry: Record<string, { role: string; prompt: string }>,
|
||||
fm: SchemaObj,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@united-workforce/eval",
|
||||
"version": "0.1.3",
|
||||
"version": "0.1.4",
|
||||
"private": false,
|
||||
"files": [
|
||||
"src",
|
||||
|
||||
@@ -6,7 +6,7 @@ import { formatList, selectEntries } from "./format.js";
|
||||
import { readEvalEntries } from "./read.js";
|
||||
|
||||
const log = createLogger({ sink: { kind: "stderr" } });
|
||||
const LOG_LIST = "L5KX9R2B";
|
||||
const LOG_LIST = "H5KX9R2B";
|
||||
|
||||
type ListCliOptions = {
|
||||
task: string | undefined;
|
||||
|
||||
@@ -143,7 +143,7 @@ describe("buildOutputFormatInstruction", () => {
|
||||
{
|
||||
type: "object",
|
||||
properties: {
|
||||
$status: { type: "string", enum: ["approved"] },
|
||||
$status: { const: "approved" },
|
||||
branch: { type: "string" },
|
||||
},
|
||||
required: ["$status"],
|
||||
@@ -151,7 +151,7 @@ describe("buildOutputFormatInstruction", () => {
|
||||
{
|
||||
type: "object",
|
||||
properties: {
|
||||
$status: { type: "string", enum: ["rejected"] },
|
||||
$status: { const: "rejected" },
|
||||
comments: { type: "string" },
|
||||
},
|
||||
required: ["$status"],
|
||||
@@ -225,4 +225,34 @@ describe("buildOutputFormatInstruction", () => {
|
||||
const result = buildOutputFormatInstruction({});
|
||||
expect(result).toContain("Focus exclusively on YOUR role");
|
||||
});
|
||||
|
||||
test("renders const value as literal in flat schema example", () => {
|
||||
const schema = {
|
||||
type: "object",
|
||||
properties: {
|
||||
$status: { type: "string", const: "greeted" },
|
||||
message: { type: "string" },
|
||||
},
|
||||
required: ["$status", "message"],
|
||||
};
|
||||
const result = buildOutputFormatInstruction(schema);
|
||||
expect(result).toContain("$status: greeted");
|
||||
expect(result).toContain("fixed value");
|
||||
expect(result).not.toContain("$status: <string>");
|
||||
});
|
||||
|
||||
test("renders const value for non-string types", () => {
|
||||
const schema = {
|
||||
type: "object",
|
||||
properties: {
|
||||
count: { type: "number", const: 42 },
|
||||
done: { type: "boolean", const: true },
|
||||
},
|
||||
required: ["count", "done"],
|
||||
};
|
||||
const result = buildOutputFormatInstruction(schema);
|
||||
expect(result).toContain("count: 42");
|
||||
expect(result).toContain("done: true");
|
||||
expect(result).toContain("fixed value");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
import type { StepContext } from "@united-workforce/protocol";
|
||||
import { describe, expect, test } from "vitest";
|
||||
import { buildThreadProgress } from "../src/build-thread-progress.js";
|
||||
|
||||
function makeStep(role: string): StepContext {
|
||||
return {
|
||||
role,
|
||||
output: {},
|
||||
detail: "0000000000000" as string,
|
||||
agent: "uwf-mock",
|
||||
edgePrompt: "",
|
||||
startedAtMs: 0,
|
||||
completedAtMs: 0,
|
||||
cwd: "",
|
||||
assembledPrompt: null,
|
||||
usage: null,
|
||||
content: null,
|
||||
};
|
||||
}
|
||||
|
||||
describe("buildThreadProgress", () => {
|
||||
test("first step of thread", () => {
|
||||
const result = buildThreadProgress([], "proponent");
|
||||
expect(result).toContain("## Thread Progress");
|
||||
expect(result).toContain("first step");
|
||||
expect(result).toContain("first time");
|
||||
expect(result).toContain("proponent");
|
||||
});
|
||||
|
||||
test("second step, role not seen before", () => {
|
||||
const steps = [makeStep("opponent")];
|
||||
const result = buildThreadProgress(steps, "proponent");
|
||||
expect(result).toContain("Thread step 2");
|
||||
expect(result).toContain("spoken 0 times");
|
||||
});
|
||||
|
||||
test("role has spoken once before", () => {
|
||||
const steps = [makeStep("proponent"), makeStep("opponent")];
|
||||
const result = buildThreadProgress(steps, "proponent");
|
||||
expect(result).toContain("Thread step 3");
|
||||
expect(result).toContain("spoken 1 time before");
|
||||
// singular "time" not "times"
|
||||
expect(result).not.toContain("1 times");
|
||||
});
|
||||
|
||||
test("role has spoken multiple times", () => {
|
||||
const steps = [
|
||||
makeStep("proponent"),
|
||||
makeStep("opponent"),
|
||||
makeStep("proponent"),
|
||||
makeStep("opponent"),
|
||||
makeStep("proponent"),
|
||||
makeStep("opponent"),
|
||||
];
|
||||
const result = buildThreadProgress(steps, "proponent");
|
||||
expect(result).toContain("Thread step 7");
|
||||
expect(result).toContain("spoken 3 times");
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@united-workforce/util-agent",
|
||||
"version": "0.1.0",
|
||||
"version": "0.1.1",
|
||||
"files": [
|
||||
"src",
|
||||
"dist",
|
||||
|
||||
@@ -74,6 +74,10 @@ function collectObjectSchemas(schema: JSONSchema): JSONSchema[] {
|
||||
}
|
||||
|
||||
function resolvePropertySchema(prop: JSONSchema): JSONSchema {
|
||||
if (prop.const !== undefined) {
|
||||
return prop;
|
||||
}
|
||||
|
||||
if (Array.isArray(prop.enum) && prop.enum.length > 0) {
|
||||
return prop;
|
||||
}
|
||||
@@ -113,6 +117,11 @@ function buildPropertyExampleLine(prop: SchemaProperty): string {
|
||||
commentParts.push("required");
|
||||
}
|
||||
|
||||
if (resolved.const !== undefined) {
|
||||
commentParts.push("fixed value");
|
||||
return `${prop.name}: ${formatYamlScalar(resolved.const)}${buildPropertyComment(commentParts)}`;
|
||||
}
|
||||
|
||||
if (Array.isArray(resolved.enum) && resolved.enum.length > 0) {
|
||||
const enumValues = resolved.enum.map((v) => String(v));
|
||||
commentParts.push(...enumValues);
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
import type { StepContext } from "@united-workforce/protocol";
|
||||
|
||||
/**
|
||||
* Build a compact thread-progress summary so the agent knows where it is
|
||||
* in the conversation without making tool calls to count steps.
|
||||
*
|
||||
* Example output:
|
||||
* ## Thread Progress
|
||||
* Thread step 6. You (proponent) have spoken 2 times before this turn.
|
||||
*/
|
||||
export function buildThreadProgress(steps: StepContext[], role: string): string {
|
||||
const totalSteps = steps.length;
|
||||
const roleVisits = steps.filter((s) => s.role === role).length;
|
||||
|
||||
const parts = [`## Thread Progress`];
|
||||
if (totalSteps === 0) {
|
||||
parts.push(
|
||||
`This is the first step of the thread. You (${role}) are speaking for the first time.`,
|
||||
);
|
||||
} else {
|
||||
parts.push(
|
||||
`Thread step ${totalSteps + 1}. You (${role}) have spoken ${roleVisits} time${roleVisits === 1 ? "" : "s"} before this turn.`,
|
||||
);
|
||||
}
|
||||
|
||||
return parts.join("\n");
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
export { buildContinuationPrompt } from "./build-continuation-prompt.js";
|
||||
export { buildOutputFormatInstruction } from "./build-output-format-instruction.js";
|
||||
export { buildRolePrompt } from "./build-role-prompt.js";
|
||||
export { buildThreadProgress } from "./build-thread-progress.js";
|
||||
export type { BuildContextMeta } from "./context.js";
|
||||
export { buildContext, buildContextWithMeta } from "./context.js";
|
||||
export type { ExtractResult, ResolvedLlmProvider } from "./extract.js";
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@united-workforce/util",
|
||||
"version": "0.1.2",
|
||||
"version": "0.1.4",
|
||||
"files": [
|
||||
"src",
|
||||
"dist",
|
||||
|
||||
@@ -140,5 +140,18 @@ For specific scenarios, run the corresponding \`uwf prompt\` command:
|
||||
|----------|---------|-------------|
|
||||
| Writing workflow YAML | \`uwf prompt workflow-authoring\` | Designing roles, conditions, graphs, and edge prompts |
|
||||
| Building a new agent adapter | \`uwf prompt adapter-developing\` | Creating a new \`uwf-<name>\` CLI adapter |
|
||||
|
||||
## Upgrading
|
||||
|
||||
\`\`\`bash
|
||||
# Install the latest version
|
||||
pnpm add -g @united-workforce/cli@latest @united-workforce/agent-hermes@latest
|
||||
# or: npm install -g @united-workforce/cli@latest @united-workforce/agent-hermes@latest
|
||||
|
||||
# Verify
|
||||
uwf --version
|
||||
|
||||
# Then run uwf prompt bootstrap and follow the upgrade instructions
|
||||
\`\`\`
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -28,6 +28,7 @@ roles: # named actors
|
||||
2. Do that
|
||||
output: "..." # what the agent should produce
|
||||
frontmatter: # JSON Schema for structured output
|
||||
type: object
|
||||
oneOf:
|
||||
- properties:
|
||||
$status: { const: "ready" }
|
||||
@@ -71,10 +72,13 @@ The \`frontmatter\` field is a standard JSON Schema. It defines the structured f
|
||||
|
||||
### \`$status\` Field
|
||||
|
||||
\`$status\` is the only standard field. Its value determines which graph edge the moderator follows. Use \`const\` to constrain each variant:
|
||||
\`$status\` is the only standard field. Its value determines which graph edge the moderator follows.
|
||||
|
||||
**Multi-exit (oneOf)** — use \`const\` to constrain each variant:
|
||||
|
||||
\`\`\`yaml
|
||||
frontmatter:
|
||||
type: object
|
||||
oneOf:
|
||||
- properties:
|
||||
$status: { const: "done" }
|
||||
@@ -86,26 +90,25 @@ frontmatter:
|
||||
required: [$status, error]
|
||||
\`\`\`
|
||||
|
||||
### Custom Fields
|
||||
|
||||
Add any fields you need for data passing between roles. These are available in edge prompts via Mustache templates.
|
||||
|
||||
### Flat Schema (Single Status)
|
||||
|
||||
When a role has only one outcome, use \`enum\` with a single value:
|
||||
**Single-exit (flat schema)** — same syntax, just no \`oneOf\` wrapper:
|
||||
|
||||
\`\`\`yaml
|
||||
frontmatter:
|
||||
type: object
|
||||
properties:
|
||||
$status:
|
||||
type: string
|
||||
enum: [done]
|
||||
$status: { const: "done" }
|
||||
summary: { type: string }
|
||||
required: [$status, summary]
|
||||
\`\`\`
|
||||
|
||||
Note: \`$status: { const: "done" }\` is **not** valid in flat schemas — the validator requires \`enum\` or \`oneOf\` with \`const\`. Use \`const\` only inside \`oneOf\` variants.
|
||||
**Important rules:**
|
||||
- \`type: object\` is **required** at the top level of frontmatter (both flat and oneOf)
|
||||
- \`$status\` always uses \`const: "value"\` — simple and consistent
|
||||
- \`enum\` is **not supported** for \`$status\` — the validator will reject it
|
||||
|
||||
### Custom Fields
|
||||
|
||||
Add any fields you need for data passing between roles. These are available in edge prompts via Mustache templates.
|
||||
|
||||
## Graph Routing
|
||||
|
||||
|
||||
Reference in New Issue
Block a user