refactor: unify env vars + env only in CLI (#37) #38

Merged
xiaomo merged 1 commits from refactor/37-env-vars into main 2026-06-04 05:13:21 +00:00
45 changed files with 394 additions and 333 deletions
+8 -8
View File
@@ -17,7 +17,7 @@ roles:
docker run -d --name uwf-e2e-$$ \ docker run -d --name uwf-e2e-$$ \
-v "$(pwd):/workspace:ro" \ -v "$(pwd):/workspace:ro" \
-e HOME=/root \ -e HOME=/root \
-e UWF_STORAGE_ROOT=/tmp/uwf-e2e-storage \ -e UWF_HOME=/tmp/uwf-e2e-storage \
--add-host=host.docker.internal:host-gateway \ --add-host=host.docker.internal:host-gateway \
-w /workspace \ -w /workspace \
node:22-bookworm \ node:22-bookworm \
@@ -39,7 +39,7 @@ roles:
export PATH="$HOME/.bun/bin:$PATH" export PATH="$HOME/.bun/bin:$PATH"
# Isolated storage # Isolated storage
mkdir -p $UWF_STORAGE_ROOT mkdir -p $UWF_HOME
# Install workspace deps # Install workspace deps
cd /root/workflow && bun install cd /root/workflow && bun install
@@ -62,9 +62,9 @@ roles:
``` ```
docker cp ~/.uwf/config.yaml uwf-e2e-$$:/tmp/uwf-e2e-storage/config.yaml 2>/dev/null || true docker cp ~/.uwf/config.yaml uwf-e2e-$$:/tmp/uwf-e2e-storage/config.yaml 2>/dev/null || true
docker exec uwf-e2e-$$ bash -c ' docker exec uwf-e2e-$$ bash -c '
if [ -f $UWF_STORAGE_ROOT/config.yaml ]; then if [ -f $UWF_HOME/config.yaml ]; then
sed -i "s|localhost|host.docker.internal|g; s|127\.0\.0\.1|host.docker.internal|g" \ sed -i "s|localhost|host.docker.internal|g; s|127\.0\.0\.1|host.docker.internal|g" \
$UWF_STORAGE_ROOT/config.yaml $UWF_HOME/config.yaml
fi fi
' '
``` ```
@@ -95,7 +95,7 @@ roles:
All commands use `uwf` (installed via `bun link` inside the container). All commands use `uwf` (installed via `bun link` inside the container).
Remember to set env vars in each exec: Remember to set env vars in each exec:
export PATH="$HOME/.bun/bin:$PATH" export PATH="$HOME/.bun/bin:$PATH"
export UWF_STORAGE_ROOT=/tmp/uwf-e2e-storage export UWF_HOME=/tmp/uwf-e2e-storage
Config tests: Config tests:
1. `uwf config list` — verify it returns valid JSON 1. `uwf config list` — verify it returns valid JSON
@@ -133,7 +133,7 @@ roles:
procedure: | procedure: |
Use the container (containerName) and workflow (workflowName) from your prompt. Use the container (containerName) and workflow (workflowName) from your prompt.
All commands via: `docker exec <containerName> bash -c '...'` All commands via: `docker exec <containerName> bash -c '...'`
Set env: PATH="$HOME/.bun/bin:$PATH" UWF_STORAGE_ROOT=/tmp/uwf-e2e-storage Set env: PATH="$HOME/.bun/bin:$PATH" UWF_HOME=/tmp/uwf-e2e-storage
1. `uwf thread start <workflowName> -p 'E2E test: what is 2+2?'` — capture thread ID from JSON output 1. `uwf thread start <workflowName> -p 'E2E test: what is 2+2?'` — capture thread ID from JSON output
2. `uwf thread list` — verify the thread appears in the list 2. `uwf thread list` — verify the thread appears in the list
@@ -166,7 +166,7 @@ roles:
procedure: | procedure: |
Use the container (containerName) and threadId from your prompt. Use the container (containerName) and threadId from your prompt.
All commands via: `docker exec <containerName> bash -c '...'` All commands via: `docker exec <containerName> bash -c '...'`
Set env: PATH="$HOME/.bun/bin:$PATH" UWF_STORAGE_ROOT=/tmp/uwf-e2e-storage Set env: PATH="$HOME/.bun/bin:$PATH" UWF_HOME=/tmp/uwf-e2e-storage
Step inspection: Step inspection:
1. `uwf step list <threadId>` — verify steps array has length > 1 1. `uwf step list <threadId>` — verify steps array has length > 1
@@ -208,7 +208,7 @@ roles:
procedure: | procedure: |
Use containerName, threadId, lastStepHash, and workflowName from your prompt. Use containerName, threadId, lastStepHash, and workflowName from your prompt.
All commands via: `docker exec <containerName> bash -c '...'` All commands via: `docker exec <containerName> bash -c '...'`
Set env: PATH="$HOME/.bun/bin:$PATH" UWF_STORAGE_ROOT=/tmp/uwf-e2e-storage Set env: PATH="$HOME/.bun/bin:$PATH" UWF_HOME=/tmp/uwf-e2e-storage
Cancel: Cancel:
1. Start a second thread: `uwf thread start <workflowName> -p 'E2E cancel test'` 1. Start a second thread: `uwf thread start <workflowName> -p 'E2E cancel test'`
@@ -29,6 +29,8 @@ function minimalContext(overrides: Partial<AgentContext> = {}): AgentContext {
outputFormatInstruction: "---\nstatus: done\n---", outputFormatInstruction: "---\nstatus: done\n---",
edgePrompt: "Implement the fix described in the plan.", edgePrompt: "Implement the fix described in the plan.",
isFirstVisit: true, isFirstVisit: true,
storageRoot: "/tmp/uwf-test",
casDir: "/tmp/ocas-test",
...overrides, ...overrides,
}; };
} }
+4 -3
View File
@@ -6,7 +6,6 @@ import {
createAgent, createAgent,
loadWorkflowConfig, loadWorkflowConfig,
resolveModel, resolveModel,
resolveStorageRoot,
} from "@united-workforce/util-agent"; } from "@united-workforce/util-agent";
import { storeBuiltinDetail } from "./detail.js"; import { storeBuiltinDetail } from "./detail.js";
@@ -40,6 +39,7 @@ type SessionRecord = {
model: string; model: string;
startedAtMs: number; startedAtMs: number;
messages: ChatMessage[]; messages: ChatMessage[];
storageRoot: string;
}; };
const sessions = new Map<string, SessionRecord>(); const sessions = new Map<string, SessionRecord>();
@@ -103,7 +103,7 @@ async function runBuiltinWithMessages(
} }
async function runBuiltin(ctx: AgentContext): Promise<AgentRunResult> { async function runBuiltin(ctx: AgentContext): Promise<AgentRunResult> {
const storageRoot = resolveStorageRoot(); const storageRoot = ctx.storageRoot;
const config = await loadWorkflowConfig(storageRoot); const config = await loadWorkflowConfig(storageRoot);
const provider = resolveModel(config, config.defaultModel); const provider = resolveModel(config, config.defaultModel);
@@ -116,6 +116,7 @@ async function runBuiltin(ctx: AgentContext): Promise<AgentRunResult> {
model: provider.model, model: provider.model,
startedAtMs: Date.now(), startedAtMs: Date.now(),
messages, messages,
storageRoot,
}; };
sessions.set(sessionId, session); sessions.set(sessionId, session);
@@ -136,7 +137,7 @@ async function continueBuiltin(
store: Store, store: Store,
): Promise<AgentRunResult> { ): Promise<AgentRunResult> {
const session = getSession(sessionId); const session = getSession(sessionId);
const storageRoot = resolveStorageRoot(); const storageRoot = session.storageRoot;
const config = await loadWorkflowConfig(storageRoot); const config = await loadWorkflowConfig(storageRoot);
const provider = resolveModel(config, config.defaultModel); const provider = resolveModel(config, config.defaultModel);
@@ -27,6 +27,8 @@ function makeCtx(overrides: Partial<AgentContext> = {}): AgentContext {
steps: [], steps: [],
store: {} as AgentContext["store"], store: {} as AgentContext["store"],
outputFormatInstruction: "Use YAML frontmatter", outputFormatInstruction: "Use YAML frontmatter",
storageRoot: "/tmp/uwf-test",
casDir: "/tmp/ocas-test",
...overrides, ...overrides,
}; };
} }
+38 -15
View File
@@ -17,7 +17,6 @@ const log = createLogger({ sink: { kind: "stderr" } });
const CLAUDE_COMMAND = "claude"; const CLAUDE_COMMAND = "claude";
const CLAUDE_MAX_TURNS = 90; const CLAUDE_MAX_TURNS = 90;
const CLAUDE_MODEL = process.env.CLAUDE_MODEL ?? null;
/** Assemble system prompt, task, and prior step outputs for Claude Code. */ /** Assemble system prompt, task, and prior step outputs for Claude Code. */
export function buildClaudeCodePrompt(ctx: AgentContext): string { export function buildClaudeCodePrompt(ctx: AgentContext): string {
@@ -85,6 +84,7 @@ function spawnClaude(
function spawnClaudeRun( function spawnClaudeRun(
prompt: string, prompt: string,
model: string | null,
): Promise<{ stdout: string; stderr: string; exitCode: number | null }> { ): Promise<{ stdout: string; stderr: string; exitCode: number | null }> {
const args = [ const args = [
"-p", "-p",
@@ -96,8 +96,8 @@ function spawnClaudeRun(
"--max-turns", "--max-turns",
String(CLAUDE_MAX_TURNS), String(CLAUDE_MAX_TURNS),
]; ];
if (CLAUDE_MODEL !== null) { if (model !== null) {
args.push("--model", CLAUDE_MODEL); args.push("--model", model);
} }
return spawnClaude(args); return spawnClaude(args);
} }
@@ -105,6 +105,7 @@ function spawnClaudeRun(
function spawnClaudeResume( function spawnClaudeResume(
sessionId: string, sessionId: string,
message: string, message: string,
model: string | null,
): Promise<{ stdout: string; stderr: string; exitCode: number | null }> { ): Promise<{ stdout: string; stderr: string; exitCode: number | null }> {
const args = [ const args = [
"-p", "-p",
@@ -118,8 +119,8 @@ function spawnClaudeResume(
"--max-turns", "--max-turns",
String(CLAUDE_MAX_TURNS), String(CLAUDE_MAX_TURNS),
]; ];
if (CLAUDE_MODEL !== null) { if (model !== null) {
args.push("--model", CLAUDE_MODEL); args.push("--model", model);
} }
return spawnClaude(args); return spawnClaude(args);
} }
@@ -157,20 +158,35 @@ async function processClaudeOutput(
); );
} }
async function runClaudeCode(ctx: AgentContext): Promise<AgentRunResult> { async function runClaudeCode(ctx: AgentContext, model: string | null): Promise<AgentRunResult> {
const fullPrompt = buildClaudeCodePrompt(ctx); const fullPrompt = buildClaudeCodePrompt(ctx);
log("K7R2M4N8", `prompt for role=${ctx.role} (length=${fullPrompt.length}):\n${fullPrompt}`); log("K7R2M4N8", `prompt for role=${ctx.role} (length=${fullPrompt.length}):\n${fullPrompt}`);
// Try resuming a cached session for re-entry scenarios (e.g. reviewer reject → developer re-entry). // Try resuming a cached session for re-entry scenarios (e.g. reviewer reject → developer re-entry).
if (!ctx.isFirstVisit) { if (!ctx.isFirstVisit) {
const cachedSessionId = await getCachedSessionId("claude-code", ctx.threadId, ctx.role); const cachedSessionId = await getCachedSessionId(
"claude-code",
ctx.threadId,
ctx.role,
ctx.storageRoot,
);
if (cachedSessionId !== null) { if (cachedSessionId !== null) {
try { try {
const { stdout, stderr, exitCode } = await spawnClaudeResume(cachedSessionId, fullPrompt); const { stdout, stderr, exitCode } = await spawnClaudeResume(
cachedSessionId,
fullPrompt,
model,
);
const result = await processClaudeOutput(stdout, stderr, exitCode, ctx.store, fullPrompt); const result = await processClaudeOutput(stdout, stderr, exitCode, ctx.store, fullPrompt);
if (result.sessionId !== undefined && result.sessionId !== "") { if (result.sessionId !== undefined && result.sessionId !== "") {
await setCachedSessionId("claude-code", ctx.threadId, ctx.role, result.sessionId); await setCachedSessionId(
"claude-code",
ctx.threadId,
ctx.role,
result.sessionId,
ctx.storageRoot,
);
} }
return result; return result;
} catch (err) { } catch (err) {
@@ -182,10 +198,16 @@ async function runClaudeCode(ctx: AgentContext): Promise<AgentRunResult> {
} }
} }
const { stdout, stderr, exitCode } = await spawnClaudeRun(fullPrompt); const { stdout, stderr, exitCode } = await spawnClaudeRun(fullPrompt, model);
const result = await processClaudeOutput(stdout, stderr, exitCode, ctx.store, fullPrompt); const result = await processClaudeOutput(stdout, stderr, exitCode, ctx.store, fullPrompt);
if (result.sessionId !== undefined && result.sessionId !== "") { if (result.sessionId !== undefined && result.sessionId !== "") {
await setCachedSessionId("claude-code", ctx.threadId, ctx.role, result.sessionId); await setCachedSessionId(
"claude-code",
ctx.threadId,
ctx.role,
result.sessionId,
ctx.storageRoot,
);
} }
return result; return result;
} }
@@ -194,16 +216,17 @@ async function continueClaudeCode(
sessionId: string, sessionId: string,
message: string, message: string,
store: Store, store: Store,
model: string | null,
): Promise<AgentRunResult> { ): Promise<AgentRunResult> {
const { stdout, stderr, exitCode } = await spawnClaudeResume(sessionId, message); const { stdout, stderr, exitCode } = await spawnClaudeResume(sessionId, message, model);
return processClaudeOutput(stdout, stderr, exitCode, store, ""); return processClaudeOutput(stdout, stderr, exitCode, store, "");
} }
/** Agent CLI factory: parses argv, runs Claude Code, extracts output, writes StepNode. */ /** Agent CLI factory: parses argv, runs Claude Code, extracts output, writes StepNode. */
export function createClaudeCodeAgent(): () => Promise<void> { export function createClaudeCodeAgent(model: string | null): () => Promise<void> {
return createAgent({ return createAgent({
name: "claude-code", name: "claude-code",
run: runClaudeCode, run: (ctx) => runClaudeCode(ctx, model),
continue: continueClaudeCode, continue: (sessionId, message, store) => continueClaudeCode(sessionId, message, store, model),
}); });
} }
+2 -1
View File
@@ -2,5 +2,6 @@
import { createClaudeCodeAgent } from "./claude-code.js"; import { createClaudeCodeAgent } from "./claude-code.js";
const main = createClaudeCodeAgent(); const model = process.env.CLAUDE_MODEL ?? null;
const main = createClaudeCodeAgent(model);
void main(); void main();
@@ -27,6 +27,8 @@ function makeCtx(overrides: Partial<AgentContext> = {}): AgentContext {
steps: [], steps: [],
store: {} as AgentContext["store"], store: {} as AgentContext["store"],
outputFormatInstruction: "Use YAML frontmatter", outputFormatInstruction: "Use YAML frontmatter",
storageRoot: "/tmp/uwf-test",
casDir: "/tmp/ocas-test",
...overrides, ...overrides,
}; };
} }
+3 -1
View File
@@ -1,6 +1,8 @@
#!/usr/bin/env node #!/usr/bin/env node
import { createHermesAgent } from "./hermes.js"; import { createHermesAgent } from "./hermes.js";
import { isResumeDisabled } from "./session-cache.js";
const main = createHermesAgent(); const resumeDisabled = isResumeDisabled(process.env.UWF_HERMES_RESUME ?? null);
const main = createHermesAgent(resumeDisabled);
void main(); void main();
+8 -7
View File
@@ -9,7 +9,7 @@ import {
} from "@united-workforce/util-agent"; } from "@united-workforce/util-agent";
import { HermesAcpClient } from "./acp-client.js"; import { HermesAcpClient } from "./acp-client.js";
import { getCachedSessionId, isResumeDisabled, setCachedSessionId } from "./session-cache.js"; import { getCachedSessionId, setCachedSessionId } from "./session-cache.js";
import { loadHermesSession, storeHermesSessionDetail } from "./session-detail.js"; import { loadHermesSession, storeHermesSessionDetail } from "./session-detail.js";
const log = createLogger({ sink: { kind: "stderr" } }); const log = createLogger({ sink: { kind: "stderr" } });
@@ -66,13 +66,14 @@ async function prepareSession(
client: HermesAcpClient, client: HermesAcpClient,
ctx: AgentContext, ctx: AgentContext,
cwd: string, cwd: string,
resumeDisabled: boolean,
): Promise<PromptAttempt> { ): Promise<PromptAttempt> {
if (ctx.isFirstVisit || isResumeDisabled()) { if (ctx.isFirstVisit || resumeDisabled) {
await client.connect(cwd); await client.connect(cwd);
return { useContinuation: false, resumed: false }; return { useContinuation: false, resumed: false };
} }
const cachedSessionId = await getCachedSessionId(ctx.threadId, ctx.role); const cachedSessionId = await getCachedSessionId(ctx.threadId, ctx.role, ctx.storageRoot);
if (cachedSessionId === null) { if (cachedSessionId === null) {
log("6RWK3N8Q", `no cached session for ${ctx.threadId}:${ctx.role}, starting new session`); log("6RWK3N8Q", `no cached session for ${ctx.threadId}:${ctx.role}, starting new session`);
await client.connect(cwd); await client.connect(cwd);
@@ -99,7 +100,7 @@ async function prepareSession(
* frontmatter retry loops keep the same Hermes session context. The client * frontmatter retry loops keep the same Hermes session context. The client
* is closed once the agent process exits (via process.on("exit")). * is closed once the agent process exits (via process.on("exit")).
*/ */
export function createHermesAgent(): () => Promise<void> { export function createHermesAgent(resumeDisabled: boolean): () => Promise<void> {
const client = new HermesAcpClient(); const client = new HermesAcpClient();
// Ensure cleanup regardless of how the process exits. // Ensure cleanup regardless of how the process exits.
@@ -113,8 +114,8 @@ export function createHermesAgent(): () => Promise<void> {
const { text, sessionId } = await client.prompt(fullPrompt); const { text, sessionId } = await client.prompt(fullPrompt);
const { detailHash } = await storePromptResult(ctx.store, sessionId); const { detailHash } = await storePromptResult(ctx.store, sessionId);
if (!isResumeDisabled()) { if (!resumeDisabled) {
await setCachedSessionId(ctx.threadId, ctx.role, sessionId); await setCachedSessionId(ctx.threadId, ctx.role, sessionId, ctx.storageRoot);
} }
return { output: text, detailHash, sessionId, assembledPrompt: fullPrompt }; return { output: text, detailHash, sessionId, assembledPrompt: fullPrompt };
@@ -122,7 +123,7 @@ export function createHermesAgent(): () => Promise<void> {
async function runHermes(ctx: AgentContext): Promise<AgentRunResult> { async function runHermes(ctx: AgentContext): Promise<AgentRunResult> {
const cwd = process.cwd(); const cwd = process.cwd();
const attempt = await prepareSession(client, ctx, cwd); const attempt = await prepareSession(client, ctx, cwd, resumeDisabled);
try { try {
return await runPrompt(ctx, attempt.useContinuation); return await runPrompt(ctx, attempt.useContinuation);
+23 -13
View File
@@ -6,28 +6,38 @@ import {
setCachedSessionId as setCachedSessionIdBase, setCachedSessionId as setCachedSessionIdBase,
} from "@united-workforce/util-agent"; } from "@united-workforce/util-agent";
export async function getCachedSessionId(threadId: ThreadId, role: string): Promise<string | null> { export async function getCachedSessionId(
return getCachedSessionIdBase("hermes", threadId, role); threadId: ThreadId,
role: string,
storageRoot: string,
): Promise<string | null> {
return getCachedSessionIdBase("hermes", threadId, role, storageRoot);
} }
export async function setCachedSessionId( export async function setCachedSessionId(
threadId: ThreadId, threadId: ThreadId,
role: string, role: string,
sessionId: string, sessionId: string,
storageRoot: string,
): Promise<void> { ): Promise<void> {
return setCachedSessionIdBase("hermes", threadId, role, sessionId); return setCachedSessionIdBase("hermes", threadId, role, sessionId, storageRoot);
} }
export function isResumeDisabled(): boolean { /**
// Hermes ACP session/resume is broken: _restore fails for custom providers * Decide whether Hermes session resume is disabled, given the raw
// because resolve_runtime_provider("custom") throws and base_url/api_mode * `UWF_HERMES_RESUME` flag (read by the CLI entry point — library code must
// are lost in the fallback path. Resume silently creates a new session * not read `process.env`).
// (different sessionId, no history), causing empty-text responses. *
// See: https://github.com/NousResearch/hermes-agent/issues/13489 * Hermes ACP session/resume is broken: _restore fails for custom providers
// Disable by default until upstream fixes the bug. Set UWF_HERMES_RESUME=1 * because resolve_runtime_provider("custom") throws and base_url/api_mode
// to opt back in. * are lost in the fallback path. Resume silently creates a new session
const enableFlag = process.env.UWF_HERMES_RESUME; * (different sessionId, no history), causing empty-text responses.
if (enableFlag === "1" || enableFlag === "true") { * See: https://github.com/NousResearch/hermes-agent/issues/13489
* Disable by default until upstream fixes the bug. Set UWF_HERMES_RESUME=1
* to opt back in.
*/
export function isResumeDisabled(resumeFlag: string | null): boolean {
if (resumeFlag === "1" || resumeFlag === "true") {
return false; return false;
} }
return true; return true;
+2 -3
View File
@@ -216,7 +216,6 @@ src/
| Variable | Purpose | Default | | Variable | Purpose | Default |
|----------|---------|---------| |----------|---------|---------|
| `OCAS_DIR` | Override the global CAS directory location | `~/.ocas` | | `OCAS_HOME` | Override the global CAS directory location | `~/.ocas` |
| `UWF_STORAGE_ROOT` | Internal override for workflow metadata storage | `~/.uwf` | | `UWF_HOME` | Override the workflow metadata storage root | `~/.uwf` |
| `WORKFLOW_STORAGE_ROOT` | User override for workflow metadata storage | `~/.uwf` |
@@ -68,7 +68,7 @@ describe("C1: adapter JSON round-trip integration", () => {
prompt: "Test round-trip task", prompt: "Test round-trip task",
}); });
process.env.OCAS_DIR = casDir; process.env.OCAS_HOME = casDir;
const threadId = "01ROUNDTRIPTEST0000000000" as ThreadId; const threadId = "01ROUNDTRIPTEST0000000000" as ThreadId;
await seedThreads(tmpDir, { [threadId]: startHash }); await seedThreads(tmpDir, { [threadId]: startHash });
@@ -134,8 +134,8 @@ describe("C1: adapter JSON round-trip integration", () => {
stdio: ["ignore", "pipe", "pipe"], stdio: ["ignore", "pipe", "pipe"],
env: { env: {
...process.env, ...process.env,
WORKFLOW_STORAGE_ROOT: tmpDir, UWF_HOME: tmpDir,
OCAS_DIR: casDir, OCAS_HOME: casDir,
}, },
cwd: tmpDir, cwd: tmpDir,
timeout: 30000, timeout: 30000,
@@ -225,9 +225,9 @@ describe("currentRole field", () => {
await mkdir(storageRoot, { recursive: true }); await mkdir(storageRoot, { recursive: true });
await mkdir(casDir, { recursive: true }); await mkdir(casDir, { recursive: true });
// Set OCAS_DIR for this test // Set OCAS_HOME for this test
originalEnv = process.env.OCAS_DIR; originalEnv = process.env.OCAS_HOME;
process.env.OCAS_DIR = casDir; process.env.OCAS_HOME = casDir;
} }
async function teardown() { async function teardown() {
@@ -236,9 +236,9 @@ describe("currentRole field", () => {
} }
// Restore original environment // Restore original environment
if (originalEnv === undefined) { if (originalEnv === undefined) {
delete process.env.OCAS_DIR; delete process.env.OCAS_HOME;
} else { } else {
process.env.OCAS_DIR = originalEnv; process.env.OCAS_HOME = originalEnv;
} }
} }
@@ -12,7 +12,7 @@ beforeEach(async () => {
tmpDir = await mkdtemp(join(tmpdir(), "cli-uwf-resolve-head-")); tmpDir = await mkdtemp(join(tmpdir(), "cli-uwf-resolve-head-"));
const casDir = join(tmpDir, "cas"); const casDir = join(tmpDir, "cas");
await mkdir(casDir, { recursive: true }); await mkdir(casDir, { recursive: true });
process.env.OCAS_DIR = casDir; process.env.OCAS_HOME = casDir;
}); });
afterEach(async () => { afterEach(async () => {
+18 -18
View File
@@ -70,16 +70,16 @@ let originalEnv: string | undefined;
beforeEach(async () => { beforeEach(async () => {
tmpDir = await mkdtemp(join(tmpdir(), "cli-uwf-step-read-test-")); tmpDir = await mkdtemp(join(tmpdir(), "cli-uwf-step-read-test-"));
originalEnv = process.env.OCAS_DIR; originalEnv = process.env.OCAS_HOME;
}); });
afterEach(async () => { afterEach(async () => {
await rm(tmpDir, { recursive: true, force: true }); await rm(tmpDir, { recursive: true, force: true });
// Restore original environment // Restore original environment
if (originalEnv === undefined) { if (originalEnv === undefined) {
delete process.env.OCAS_DIR; delete process.env.OCAS_HOME;
} else { } else {
process.env.OCAS_DIR = originalEnv; process.env.OCAS_HOME = originalEnv;
} }
}); });
@@ -88,10 +88,10 @@ afterEach(async () => {
describe("step read", () => { describe("step read", () => {
test("test 1: basic single-step read with 3 turns", async () => { test("test 1: basic single-step read with 3 turns", async () => {
const casDir = join(tmpDir, "cas"); const casDir = join(tmpDir, "cas");
process.env.OCAS_DIR = casDir; process.env.OCAS_HOME = casDir;
await mkdir(casDir, { recursive: true }); await mkdir(casDir, { recursive: true });
process.env.OCAS_DIR = casDir; process.env.OCAS_HOME = casDir;
process.env.OCAS_DIR = casDir; process.env.OCAS_HOME = casDir;
const store = await openStore(casDir); const store = await openStore(casDir);
const schemas = await registerUwfSchemas(store); const schemas = await registerUwfSchemas(store);
const detailSchemas = await registerDetailSchemas(store); const detailSchemas = await registerDetailSchemas(store);
@@ -177,9 +177,9 @@ describe("step read", () => {
test("test 2: quota enforcement - multiple turns", async () => { test("test 2: quota enforcement - multiple turns", async () => {
const casDir = join(tmpDir, "cas"); const casDir = join(tmpDir, "cas");
process.env.OCAS_DIR = casDir; process.env.OCAS_HOME = casDir;
await mkdir(casDir, { recursive: true }); await mkdir(casDir, { recursive: true });
process.env.OCAS_DIR = casDir; process.env.OCAS_HOME = casDir;
const store = await openStore(casDir); const store = await openStore(casDir);
const schemas = await registerUwfSchemas(store); const schemas = await registerUwfSchemas(store);
const detailSchemas = await registerDetailSchemas(store); const detailSchemas = await registerDetailSchemas(store);
@@ -263,9 +263,9 @@ describe("step read", () => {
test("test 3: minimal quota edge case - always show at least one turn", async () => { test("test 3: minimal quota edge case - always show at least one turn", async () => {
const casDir = join(tmpDir, "cas"); const casDir = join(tmpDir, "cas");
process.env.OCAS_DIR = casDir; process.env.OCAS_HOME = casDir;
await mkdir(casDir, { recursive: true }); await mkdir(casDir, { recursive: true });
process.env.OCAS_DIR = casDir; process.env.OCAS_HOME = casDir;
const store = await openStore(casDir); const store = await openStore(casDir);
const schemas = await registerUwfSchemas(store); const schemas = await registerUwfSchemas(store);
const detailSchemas = await registerDetailSchemas(store); const detailSchemas = await registerDetailSchemas(store);
@@ -340,9 +340,9 @@ describe("step read", () => {
test("test 4: step with no detail field", async () => { test("test 4: step with no detail field", async () => {
const casDir = join(tmpDir, "cas"); const casDir = join(tmpDir, "cas");
process.env.OCAS_DIR = casDir; process.env.OCAS_HOME = casDir;
await mkdir(casDir, { recursive: true }); await mkdir(casDir, { recursive: true });
process.env.OCAS_DIR = casDir; process.env.OCAS_HOME = casDir;
const store = await openStore(casDir); const store = await openStore(casDir);
const schemas = await registerUwfSchemas(store); const schemas = await registerUwfSchemas(store);
@@ -401,9 +401,9 @@ describe("step read", () => {
test("test 5: step with detail but no turns array", async () => { test("test 5: step with detail but no turns array", async () => {
const casDir = join(tmpDir, "cas"); const casDir = join(tmpDir, "cas");
process.env.OCAS_DIR = casDir; process.env.OCAS_HOME = casDir;
await mkdir(casDir, { recursive: true }); await mkdir(casDir, { recursive: true });
process.env.OCAS_DIR = casDir; process.env.OCAS_HOME = casDir;
const store = await openStore(casDir); const store = await openStore(casDir);
const schemas = await registerUwfSchemas(store); const schemas = await registerUwfSchemas(store);
await registerDetailSchemas(store); await registerDetailSchemas(store);
@@ -479,9 +479,9 @@ describe("step read", () => {
test("test 6: displays role and tool calls in turn body", async () => { test("test 6: displays role and tool calls in turn body", async () => {
const casDir = join(tmpDir, "cas"); const casDir = join(tmpDir, "cas");
process.env.OCAS_DIR = casDir; process.env.OCAS_HOME = casDir;
await mkdir(casDir, { recursive: true }); await mkdir(casDir, { recursive: true });
process.env.OCAS_DIR = casDir; process.env.OCAS_HOME = casDir;
const store = await openStore(casDir); const store = await openStore(casDir);
const schemas = await registerUwfSchemas(store); const schemas = await registerUwfSchemas(store);
const detailSchemas = await registerDetailSchemas(store); const detailSchemas = await registerDetailSchemas(store);
@@ -553,9 +553,9 @@ describe("step read", () => {
test("test 7: turn content with special characters", async () => { test("test 7: turn content with special characters", async () => {
const casDir = join(tmpDir, "cas"); const casDir = join(tmpDir, "cas");
process.env.OCAS_DIR = casDir; process.env.OCAS_HOME = casDir;
await mkdir(casDir, { recursive: true }); await mkdir(casDir, { recursive: true });
process.env.OCAS_DIR = casDir; process.env.OCAS_HOME = casDir;
const store = await openStore(casDir); const store = await openStore(casDir);
const schemas = await registerUwfSchemas(store); const schemas = await registerUwfSchemas(store);
const detailSchemas = await registerDetailSchemas(store); const detailSchemas = await registerDetailSchemas(store);
@@ -131,16 +131,16 @@ describe("cmdStepShow JSON serialization", () => {
testDir = await mkdtemp(join(tmpdir(), "uwf-test-")); testDir = await mkdtemp(join(tmpdir(), "uwf-test-"));
casDir = join(testDir, "cas"); casDir = join(testDir, "cas");
await mkdir(casDir, { recursive: true }); await mkdir(casDir, { recursive: true });
originalEnv = process.env.OCAS_DIR; originalEnv = process.env.OCAS_HOME;
process.env.OCAS_DIR = casDir; process.env.OCAS_HOME = casDir;
}); });
afterEach(async () => { afterEach(async () => {
await rm(testDir, { recursive: true, force: true }); await rm(testDir, { recursive: true, force: true });
if (originalEnv === undefined) { if (originalEnv === undefined) {
delete process.env.OCAS_DIR; delete process.env.OCAS_HOME;
} else { } else {
process.env.OCAS_DIR = originalEnv; process.env.OCAS_HOME = originalEnv;
} }
}); });
@@ -67,17 +67,17 @@ let originalEnv: string | undefined;
beforeEach(async () => { beforeEach(async () => {
tmpDir = await mkdtemp(join(tmpdir(), "cli-uwf-step-timing-test-")); tmpDir = await mkdtemp(join(tmpdir(), "cli-uwf-step-timing-test-"));
originalEnv = process.env.OCAS_DIR; originalEnv = process.env.OCAS_HOME;
process.env.OCAS_DIR = join(tmpDir, "cas"); process.env.OCAS_HOME = join(tmpDir, "cas");
await mkdir(process.env.OCAS_DIR, { recursive: true }); await mkdir(process.env.OCAS_HOME, { recursive: true });
}); });
afterEach(async () => { afterEach(async () => {
await rm(tmpDir, { recursive: true, force: true }); await rm(tmpDir, { recursive: true, force: true });
if (originalEnv === undefined) { if (originalEnv === undefined) {
delete process.env.OCAS_DIR; delete process.env.OCAS_HOME;
} else { } else {
process.env.OCAS_DIR = originalEnv; process.env.OCAS_HOME = originalEnv;
} }
}); });
@@ -20,7 +20,7 @@ describe("Global CAS directory", () => {
beforeEach(async () => { beforeEach(async () => {
tmpDir = join(tmpdir(), `uwf-test-global-cas-${Date.now()}`); tmpDir = join(tmpdir(), `uwf-test-global-cas-${Date.now()}`);
await mkdir(tmpDir, { recursive: true }); await mkdir(tmpDir, { recursive: true });
originalOcasDir = process.env.OCAS_DIR; originalOcasDir = process.env.OCAS_HOME;
}); });
afterEach(async () => { afterEach(async () => {
@@ -28,27 +28,27 @@ describe("Global CAS directory", () => {
await rm(tmpDir, { recursive: true, force: true }); await rm(tmpDir, { recursive: true, force: true });
} }
if (originalOcasDir === undefined) { if (originalOcasDir === undefined) {
delete process.env.OCAS_DIR; delete process.env.OCAS_HOME;
} else { } else {
process.env.OCAS_DIR = originalOcasDir; process.env.OCAS_HOME = originalOcasDir;
} }
}); });
test("getGlobalCasDir returns default path when no env var set", () => { test("getGlobalCasDir returns default path when no env var set", () => {
delete process.env.OCAS_DIR; delete process.env.OCAS_HOME;
const casDir = getGlobalCasDir(); const casDir = getGlobalCasDir();
expect(casDir).toContain(".ocas"); expect(casDir).toContain(".ocas");
}); });
test("getGlobalCasDir respects OCAS_DIR environment variable", () => { test("getGlobalCasDir respects OCAS_HOME environment variable", () => {
const customPath = join(tmpDir, "custom-cas"); const customPath = join(tmpDir, "custom-cas");
process.env.OCAS_DIR = customPath; process.env.OCAS_HOME = customPath;
const casDir = getGlobalCasDir(); const casDir = getGlobalCasDir();
expect(casDir).toBe(customPath); expect(casDir).toBe(customPath);
}); });
test("getGlobalCasDir ignores empty OCAS_DIR", () => { test("getGlobalCasDir ignores empty OCAS_HOME", () => {
process.env.OCAS_DIR = ""; process.env.OCAS_HOME = "";
const casDir = getGlobalCasDir(); const casDir = getGlobalCasDir();
expect(casDir).toContain(".ocas"); expect(casDir).toContain(".ocas");
}); });
@@ -61,7 +61,7 @@ describe("Global CAS directory", () => {
test("createUwfStore uses global CAS directory", async () => { test("createUwfStore uses global CAS directory", async () => {
const globalCasDir = join(tmpDir, "global-cas"); const globalCasDir = join(tmpDir, "global-cas");
process.env.OCAS_DIR = globalCasDir; process.env.OCAS_HOME = globalCasDir;
const storageRoot = join(tmpDir, "storage"); const storageRoot = join(tmpDir, "storage");
await mkdir(storageRoot, { recursive: true }); await mkdir(storageRoot, { recursive: true });
@@ -82,7 +82,7 @@ describe("Global CAS directory", () => {
test("createUwfStore creates global CAS directory if it does not exist", async () => { test("createUwfStore creates global CAS directory if it does not exist", async () => {
const globalCasDir = join(tmpDir, "new-global-cas"); const globalCasDir = join(tmpDir, "new-global-cas");
process.env.OCAS_DIR = globalCasDir; process.env.OCAS_HOME = globalCasDir;
const storageRoot = join(tmpDir, "storage"); const storageRoot = join(tmpDir, "storage");
await mkdir(storageRoot, { recursive: true }); await mkdir(storageRoot, { recursive: true });
@@ -97,7 +97,7 @@ describe("Global CAS directory", () => {
test("multiple uwfStore instances share the same global CAS filesystem", async () => { test("multiple uwfStore instances share the same global CAS filesystem", async () => {
const globalCasDir = join(tmpDir, "shared-cas"); const globalCasDir = join(tmpDir, "shared-cas");
process.env.OCAS_DIR = globalCasDir; process.env.OCAS_HOME = globalCasDir;
const storageRoot1 = join(tmpDir, "storage1"); const storageRoot1 = join(tmpDir, "storage1");
const storageRoot2 = join(tmpDir, "storage2"); const storageRoot2 = join(tmpDir, "storage2");
@@ -127,7 +127,7 @@ describe("Global CAS directory", () => {
test("workflow registry is stored in global CAS variable store", async () => { test("workflow registry is stored in global CAS variable store", async () => {
const globalCasDir = join(tmpDir, "global-cas"); const globalCasDir = join(tmpDir, "global-cas");
process.env.OCAS_DIR = globalCasDir; process.env.OCAS_HOME = globalCasDir;
const storageRoot = join(tmpDir, "storage"); const storageRoot = join(tmpDir, "storage");
await mkdir(storageRoot, { recursive: true }); await mkdir(storageRoot, { recursive: true });
@@ -148,7 +148,7 @@ describe("Global CAS directory", () => {
test("migrates workflows.yaml to variable store and renames file", async () => { test("migrates workflows.yaml to variable store and renames file", async () => {
const globalCasDir = join(tmpDir, "global-cas"); const globalCasDir = join(tmpDir, "global-cas");
process.env.OCAS_DIR = globalCasDir; process.env.OCAS_HOME = globalCasDir;
const storageRoot = join(tmpDir, "storage-migrate"); const storageRoot = join(tmpDir, "storage-migrate");
await mkdir(storageRoot, { recursive: true }); await mkdir(storageRoot, { recursive: true });
@@ -173,7 +173,7 @@ describe("Global CAS directory", () => {
test("migrates threads.yaml to variable store and renames file", async () => { test("migrates threads.yaml to variable store and renames file", async () => {
const globalCasDir = join(tmpDir, "global-cas-threads"); const globalCasDir = join(tmpDir, "global-cas-threads");
process.env.OCAS_DIR = globalCasDir; process.env.OCAS_HOME = globalCasDir;
const storageRoot = join(tmpDir, "storage-threads-migrate"); const storageRoot = join(tmpDir, "storage-threads-migrate");
await mkdir(storageRoot, { recursive: true }); await mkdir(storageRoot, { recursive: true });
@@ -197,7 +197,7 @@ describe("Global CAS directory", () => {
test("thread metadata stored in ocas variable store", async () => { test("thread metadata stored in ocas variable store", async () => {
const globalCasDir = join(tmpDir, "global-cas"); const globalCasDir = join(tmpDir, "global-cas");
process.env.OCAS_DIR = globalCasDir; process.env.OCAS_HOME = globalCasDir;
const storageRoot = join(tmpDir, "storage"); const storageRoot = join(tmpDir, "storage");
await mkdir(storageRoot, { recursive: true }); await mkdir(storageRoot, { recursive: true });
@@ -218,7 +218,7 @@ describe("Global CAS directory", () => {
test("history is stored in global CAS variable store", async () => { test("history is stored in global CAS variable store", async () => {
const globalCasDir = join(tmpDir, "global-cas"); const globalCasDir = join(tmpDir, "global-cas");
process.env.OCAS_DIR = globalCasDir; process.env.OCAS_HOME = globalCasDir;
const storageRoot = join(tmpDir, "storage"); const storageRoot = join(tmpDir, "storage");
await mkdir(storageRoot, { recursive: true }); await mkdir(storageRoot, { recursive: true });
@@ -249,7 +249,7 @@ describe("Global CAS directory", () => {
test("migrates history.jsonl to variable store and renames file", async () => { test("migrates history.jsonl to variable store and renames file", async () => {
const globalCasDir = join(tmpDir, "global-cas-history"); const globalCasDir = join(tmpDir, "global-cas-history");
process.env.OCAS_DIR = globalCasDir; process.env.OCAS_HOME = globalCasDir;
const storageRoot = join(tmpDir, "storage-history-migrate"); const storageRoot = join(tmpDir, "storage-history-migrate");
await mkdir(storageRoot, { recursive: true }); await mkdir(storageRoot, { recursive: true });
@@ -292,7 +292,7 @@ describe("Global CAS directory", () => {
test("CAS nodes are stored in global directory", async () => { test("CAS nodes are stored in global directory", async () => {
const globalCasDir = join(tmpDir, "global-cas"); const globalCasDir = join(tmpDir, "global-cas");
process.env.OCAS_DIR = globalCasDir; process.env.OCAS_HOME = globalCasDir;
const storageRoot = join(tmpDir, "storage"); const storageRoot = join(tmpDir, "storage");
await mkdir(storageRoot, { recursive: true }); await mkdir(storageRoot, { recursive: true });
@@ -4,7 +4,7 @@ import { afterEach, beforeEach, describe, expect, test } from "vitest";
import { getDefaultStorageRoot, getGlobalCasDir, resolveStorageRoot } from "../store.js"; import { getDefaultStorageRoot, getGlobalCasDir, resolveStorageRoot } from "../store.js";
describe("Storage root resolution", () => { describe("Storage root resolution", () => {
const envKeys = ["UWF_STORAGE_ROOT", "WORKFLOW_STORAGE_ROOT", "OCAS_DIR"] as const; const envKeys = ["UWF_HOME", "OCAS_HOME"] as const;
const savedEnv: Partial<Record<(typeof envKeys)[number], string | undefined>> = {}; const savedEnv: Partial<Record<(typeof envKeys)[number], string | undefined>> = {};
beforeEach(() => { beforeEach(() => {
@@ -28,15 +28,13 @@ describe("Storage root resolution", () => {
expect(getDefaultStorageRoot()).toBe(join(homedir(), ".uwf")); expect(getDefaultStorageRoot()).toBe(join(homedir(), ".uwf"));
}); });
test("resolveStorageRoot prefers UWF_STORAGE_ROOT", () => { test("resolveStorageRoot uses UWF_HOME", () => {
process.env.UWF_STORAGE_ROOT = "/tmp/uwf-primary"; process.env.UWF_HOME = "/tmp/uwf-primary";
process.env.WORKFLOW_STORAGE_ROOT = "/tmp/uwf-fallback";
expect(resolveStorageRoot()).toBe("/tmp/uwf-primary"); expect(resolveStorageRoot()).toBe("/tmp/uwf-primary");
}); });
test("resolveStorageRoot falls back to WORKFLOW_STORAGE_ROOT", () => { test("resolveStorageRoot falls back to default when UWF_HOME unset", () => {
process.env.WORKFLOW_STORAGE_ROOT = "/tmp/uwf-fallback"; expect(resolveStorageRoot()).toBe(getDefaultStorageRoot());
expect(resolveStorageRoot()).toBe("/tmp/uwf-fallback");
}); });
test("getGlobalCasDir returns ~/.ocas by default", () => { test("getGlobalCasDir returns ~/.ocas by default", () => {
@@ -44,8 +42,8 @@ describe("Storage root resolution", () => {
expect(casDir).toBe(join(homedir(), ".ocas")); expect(casDir).toBe(join(homedir(), ".ocas"));
}); });
test("getGlobalCasDir respects OCAS_DIR", () => { test("getGlobalCasDir respects OCAS_HOME", () => {
process.env.OCAS_DIR = "/tmp/ocas-primary"; process.env.OCAS_HOME = "/tmp/ocas-primary";
expect(getGlobalCasDir()).toBe("/tmp/ocas-primary"); expect(getGlobalCasDir()).toBe("/tmp/ocas-primary");
}); });
}); });
@@ -8,7 +8,7 @@ import { addHistoryEntry, createUwfStore, loadAllHistory } from "../store.js";
async function makeUwfStore(storageRoot: string) { async function makeUwfStore(storageRoot: string) {
const casDir = join(storageRoot, "cas"); const casDir = join(storageRoot, "cas");
await mkdir(casDir, { recursive: true }); await mkdir(casDir, { recursive: true });
process.env.OCAS_DIR = casDir; process.env.OCAS_HOME = casDir;
return createUwfStore(storageRoot); return createUwfStore(storageRoot);
} }
@@ -22,8 +22,8 @@ import {
async function makeUwfStore(storageRoot: string): Promise<UwfStore> { async function makeUwfStore(storageRoot: string): Promise<UwfStore> {
const casDir = join(storageRoot, "cas"); const casDir = join(storageRoot, "cas");
await mkdir(casDir, { recursive: true }); await mkdir(casDir, { recursive: true });
// Set OCAS_DIR to use the test's CAS directory // Set OCAS_HOME to use the test's CAS directory
process.env.OCAS_DIR = casDir; process.env.OCAS_HOME = casDir;
return createUwfStore(storageRoot); return createUwfStore(storageRoot);
} }
@@ -19,9 +19,9 @@ describe("Thread and edge location integration", () => {
await mkdir(storageRoot, { recursive: true }); await mkdir(storageRoot, { recursive: true });
await mkdir(casDir, { recursive: true }); await mkdir(casDir, { recursive: true });
// Set OCAS_DIR for this test // Set OCAS_HOME for this test
originalEnv = process.env.OCAS_DIR; originalEnv = process.env.OCAS_HOME;
process.env.OCAS_DIR = casDir; process.env.OCAS_HOME = casDir;
} }
async function teardown() { async function teardown() {
@@ -30,9 +30,9 @@ describe("Thread and edge location integration", () => {
} }
// Restore original environment // Restore original environment
if (originalEnv === undefined) { if (originalEnv === undefined) {
delete process.env.OCAS_DIR; delete process.env.OCAS_HOME;
} else { } else {
process.env.OCAS_DIR = originalEnv; process.env.OCAS_HOME = originalEnv;
} }
} }
@@ -71,17 +71,17 @@ let originalEnv: string | undefined;
beforeEach(async () => { beforeEach(async () => {
tmpDir = await mkdtemp(join(tmpdir(), "cli-uwf-quota-test-")); tmpDir = await mkdtemp(join(tmpdir(), "cli-uwf-quota-test-"));
originalEnv = process.env.OCAS_DIR; originalEnv = process.env.OCAS_HOME;
process.env.OCAS_DIR = join(tmpDir, "cas"); process.env.OCAS_HOME = join(tmpDir, "cas");
await mkdir(process.env.OCAS_DIR, { recursive: true }); await mkdir(process.env.OCAS_HOME, { recursive: true });
}); });
afterEach(async () => { afterEach(async () => {
await rm(tmpDir, { recursive: true, force: true }); await rm(tmpDir, { recursive: true, force: true });
if (originalEnv === undefined) { if (originalEnv === undefined) {
delete process.env.OCAS_DIR; delete process.env.OCAS_HOME;
} else { } else {
process.env.OCAS_DIR = originalEnv; process.env.OCAS_HOME = originalEnv;
} }
}); });
@@ -52,7 +52,7 @@ const DETAIL_SCHEMA = {
async function makeUwfStore(storageRoot: string): Promise<UwfStore> { async function makeUwfStore(storageRoot: string): Promise<UwfStore> {
const casDir = join(storageRoot, "cas"); const casDir = join(storageRoot, "cas");
await mkdir(casDir, { recursive: true }); await mkdir(casDir, { recursive: true });
process.env.OCAS_DIR = casDir; process.env.OCAS_HOME = casDir;
return createUwfStore(storageRoot); return createUwfStore(storageRoot);
} }
@@ -89,7 +89,7 @@ async function setupSuspendedThread(mode: MockAgentMode): Promise<{
cwd: tmpDir, cwd: tmpDir,
}); });
process.env.OCAS_DIR = casDir; process.env.OCAS_HOME = casDir;
await seedThreads(tmpDir, { [THREAD_ID]: startHash }); await seedThreads(tmpDir, { [THREAD_ID]: startHash });
const outputHash = await store.cas.put(outputSchemaHash, { const outputHash = await store.cas.put(outputSchemaHash, {
@@ -189,8 +189,8 @@ function runUwf(
stdio: ["ignore", "pipe", "pipe"], stdio: ["ignore", "pipe", "pipe"],
env: { env: {
...process.env, ...process.env,
WORKFLOW_STORAGE_ROOT: tmpDir, UWF_HOME: tmpDir,
OCAS_DIR: casDir, OCAS_HOME: casDir,
}, },
cwd: tmpDir, cwd: tmpDir,
timeout: 30000, timeout: 30000,
@@ -242,7 +242,7 @@ describe("uwf thread resume", () => {
cwd: tmpDir, cwd: tmpDir,
}); });
process.env.OCAS_DIR = casDir; process.env.OCAS_HOME = casDir;
await seedThreads(tmpDir, { [THREAD_ID]: startHash }); await seedThreads(tmpDir, { [THREAD_ID]: startHash });
const result = runUwf(["thread", "resume", THREAD_ID], casDir); const result = runUwf(["thread", "resume", THREAD_ID], casDir);
@@ -251,9 +251,9 @@ describe("uwf thread resume", () => {
}); });
test("resume suspended thread executes step and becomes idle", async () => { test("resume suspended thread executes step and becomes idle", async () => {
const originalCasDir = process.env.OCAS_DIR; const originalCasDir = process.env.OCAS_HOME;
const { casDir, mockAgentPath } = await setupSuspendedThread("ok"); const { casDir, mockAgentPath } = await setupSuspendedThread("ok");
process.env.OCAS_DIR = casDir; process.env.OCAS_HOME = casDir;
try { try {
const result = runUwf(["thread", "resume", THREAD_ID, "--agent", mockAgentPath], casDir); const result = runUwf(["thread", "resume", THREAD_ID, "--agent", mockAgentPath], casDir);
@@ -279,17 +279,17 @@ describe("uwf thread resume", () => {
expect(showResult.suspendMessage).toBeNull(); expect(showResult.suspendMessage).toBeNull();
} finally { } finally {
if (originalCasDir === undefined) { if (originalCasDir === undefined) {
delete process.env.OCAS_DIR; delete process.env.OCAS_HOME;
} else { } else {
process.env.OCAS_DIR = originalCasDir; process.env.OCAS_HOME = originalCasDir;
} }
} }
}); });
test("resume without -p uses suspend message as agent prompt", async () => { test("resume without -p uses suspend message as agent prompt", async () => {
const originalCasDir = process.env.OCAS_DIR; const originalCasDir = process.env.OCAS_HOME;
const { casDir, mockAgentPath, promptCapturePath } = await setupSuspendedThread("ok"); const { casDir, mockAgentPath, promptCapturePath } = await setupSuspendedThread("ok");
process.env.OCAS_DIR = casDir; process.env.OCAS_HOME = casDir;
try { try {
const result = runUwf(["thread", "resume", THREAD_ID, "--agent", mockAgentPath], casDir); const result = runUwf(["thread", "resume", THREAD_ID, "--agent", mockAgentPath], casDir);
@@ -299,17 +299,17 @@ describe("uwf thread resume", () => {
expect(capturedPrompt).toBe(SUSPEND_MESSAGE); expect(capturedPrompt).toBe(SUSPEND_MESSAGE);
} finally { } finally {
if (originalCasDir === undefined) { if (originalCasDir === undefined) {
delete process.env.OCAS_DIR; delete process.env.OCAS_HOME;
} else { } else {
process.env.OCAS_DIR = originalCasDir; process.env.OCAS_HOME = originalCasDir;
} }
} }
}); });
test("resume with -p appends supplementary info to agent prompt", async () => { test("resume with -p appends supplementary info to agent prompt", async () => {
const originalCasDir = process.env.OCAS_DIR; const originalCasDir = process.env.OCAS_HOME;
const { casDir, mockAgentPath, promptCapturePath } = await setupSuspendedThread("ok"); const { casDir, mockAgentPath, promptCapturePath } = await setupSuspendedThread("ok");
process.env.OCAS_DIR = casDir; process.env.OCAS_HOME = casDir;
try { try {
const supplement = "Use the REST API."; const supplement = "Use the REST API.";
@@ -323,17 +323,17 @@ describe("uwf thread resume", () => {
expect(capturedPrompt).toBe(`${SUSPEND_MESSAGE}\n\n${supplement}`); expect(capturedPrompt).toBe(`${SUSPEND_MESSAGE}\n\n${supplement}`);
} finally { } finally {
if (originalCasDir === undefined) { if (originalCasDir === undefined) {
delete process.env.OCAS_DIR; delete process.env.OCAS_HOME;
} else { } else {
process.env.OCAS_DIR = originalCasDir; process.env.OCAS_HOME = originalCasDir;
} }
} }
}); });
test("multiple suspend/resume cycles", async () => { test("multiple suspend/resume cycles", async () => {
const originalCasDir = process.env.OCAS_DIR; const originalCasDir = process.env.OCAS_HOME;
const { casDir, mockAgentPath, promptCapturePath } = await setupSuspendedThread("suspend"); const { casDir, mockAgentPath, promptCapturePath } = await setupSuspendedThread("suspend");
process.env.OCAS_DIR = casDir; process.env.OCAS_HOME = casDir;
try { try {
const firstResult = runUwf(["thread", "resume", THREAD_ID, "--agent", mockAgentPath], casDir); const firstResult = runUwf(["thread", "resume", THREAD_ID, "--agent", mockAgentPath], casDir);
@@ -371,9 +371,9 @@ describe("uwf thread resume", () => {
expect(capturedPrompt).toBe(SUSPEND_MESSAGE); expect(capturedPrompt).toBe(SUSPEND_MESSAGE);
} finally { } finally {
if (originalCasDir === undefined) { if (originalCasDir === undefined) {
delete process.env.OCAS_DIR; delete process.env.OCAS_HOME;
} else { } else {
process.env.OCAS_DIR = originalCasDir; process.env.OCAS_HOME = originalCasDir;
} }
} }
}); });
@@ -305,8 +305,8 @@ describe("thread show status field", () => {
await setupTestEnv(); await setupTestEnv();
const casDir = join(tmpDir, "cas"); const casDir = join(tmpDir, "cas");
await mkdir(casDir, { recursive: true }); await mkdir(casDir, { recursive: true });
const originalCasDir = process.env.OCAS_DIR; const originalCasDir = process.env.OCAS_HOME;
process.env.OCAS_DIR = casDir; process.env.OCAS_HOME = casDir;
try { try {
const workflowPath = join(tmpDir, "test-suspend-status.yaml"); const workflowPath = join(tmpDir, "test-suspend-status.yaml");
@@ -331,9 +331,9 @@ describe("thread show status field", () => {
expect(result.thread).toBe(threadId); expect(result.thread).toBe(threadId);
} finally { } finally {
if (originalCasDir === undefined) { if (originalCasDir === undefined) {
delete process.env.OCAS_DIR; delete process.env.OCAS_HOME;
} else { } else {
process.env.OCAS_DIR = originalCasDir; process.env.OCAS_HOME = originalCasDir;
} }
await teardown(); await teardown();
} }
@@ -21,9 +21,9 @@ describe("thread start --cwd CLI option", () => {
await mkdir(storageRoot, { recursive: true }); await mkdir(storageRoot, { recursive: true });
await mkdir(casDir, { recursive: true }); await mkdir(casDir, { recursive: true });
// Set OCAS_DIR for this test // Set OCAS_HOME for this test
originalEnv = process.env.OCAS_DIR; originalEnv = process.env.OCAS_HOME;
process.env.OCAS_DIR = casDir; process.env.OCAS_HOME = casDir;
} }
async function teardown() { async function teardown() {
@@ -32,9 +32,9 @@ describe("thread start --cwd CLI option", () => {
} }
// Restore original environment // Restore original environment
if (originalEnv === undefined) { if (originalEnv === undefined) {
delete process.env.OCAS_DIR; delete process.env.OCAS_HOME;
} else { } else {
process.env.OCAS_DIR = originalEnv; process.env.OCAS_HOME = originalEnv;
} }
} }
@@ -139,7 +139,7 @@ graph:
// Register the workflow // Register the workflow
execFileSync(process.execPath, [uwfBin, "workflow", "add", workflowPath], { execFileSync(process.execPath, [uwfBin, "workflow", "add", workflowPath], {
env: { ...process.env, UWF_STORAGE_ROOT: storageRoot, OCAS_DIR: casDir }, env: { ...process.env, UWF_HOME: storageRoot, OCAS_HOME: casDir },
encoding: "utf8", encoding: "utf8",
}); });
@@ -148,7 +148,7 @@ graph:
process.execPath, process.execPath,
[uwfBin, "thread", "start", "test-cwd-cli", "-p", "test prompt", "--cwd", testCwd], [uwfBin, "thread", "start", "test-cwd-cli", "-p", "test prompt", "--cwd", testCwd],
{ {
env: { ...process.env, UWF_STORAGE_ROOT: storageRoot, OCAS_DIR: casDir }, env: { ...process.env, UWF_HOME: storageRoot, OCAS_HOME: casDir },
encoding: "utf8", encoding: "utf8",
}, },
); );
@@ -9,7 +9,7 @@ function runCli(args: string[]): { stdout: string; stderr: string; exitCode: num
try { try {
const stdout = execFileSync("npx", ["tsx", CLI_PATH, ...args], { const stdout = execFileSync("npx", ["tsx", CLI_PATH, ...args], {
encoding: "utf8", encoding: "utf8",
env: { ...process.env, WORKFLOW_STORAGE_ROOT: "/tmp/uwf-test-nonexistent" }, env: { ...process.env, UWF_HOME: "/tmp/uwf-test-nonexistent" },
stdio: ["ignore", "pipe", "pipe"], stdio: ["ignore", "pipe", "pipe"],
}); });
return { stdout, stderr: "", exitCode: 0 }; return { stdout, stderr: "", exitCode: 0 };
@@ -35,8 +35,8 @@ describe("suspend step CAS chain and threads.yaml metadata", () => {
test("thread exec records suspend step in CAS and suspend metadata in threads.yaml", async () => { test("thread exec records suspend step in CAS and suspend metadata in threads.yaml", async () => {
const casDir = join(tmpDir, "cas"); const casDir = join(tmpDir, "cas");
await mkdir(casDir, { recursive: true }); await mkdir(casDir, { recursive: true });
const originalCasDir = process.env.OCAS_DIR; const originalCasDir = process.env.OCAS_HOME;
process.env.OCAS_DIR = casDir; process.env.OCAS_HOME = casDir;
try { try {
const store = await openStore(casDir); const store = await openStore(casDir);
@@ -128,8 +128,8 @@ describe("suspend step CAS chain and threads.yaml metadata", () => {
stdio: ["ignore", "pipe", "pipe"], stdio: ["ignore", "pipe", "pipe"],
env: { env: {
...process.env, ...process.env,
WORKFLOW_STORAGE_ROOT: tmpDir, UWF_HOME: tmpDir,
OCAS_DIR: casDir, OCAS_HOME: casDir,
}, },
cwd: tmpDir, cwd: tmpDir,
timeout: 30000, timeout: 30000,
@@ -170,9 +170,9 @@ describe("suspend step CAS chain and threads.yaml metadata", () => {
expect(showResult.suspendedRole).toBe("worker"); expect(showResult.suspendedRole).toBe("worker");
} finally { } finally {
if (originalCasDir === undefined) { if (originalCasDir === undefined) {
delete process.env.OCAS_DIR; delete process.env.OCAS_HOME;
} else { } else {
process.env.OCAS_DIR = originalCasDir; process.env.OCAS_HOME = originalCasDir;
} }
} }
}); });
@@ -33,8 +33,8 @@ describe("suspended thread display", () => {
test("thread list shows [suspended] marker for suspended threads", async () => { test("thread list shows [suspended] marker for suspended threads", async () => {
const casDir = join(tmpDir, "cas"); const casDir = join(tmpDir, "cas");
await mkdir(casDir, { recursive: true }); await mkdir(casDir, { recursive: true });
const originalCasDir = process.env.OCAS_DIR; const originalCasDir = process.env.OCAS_HOME;
process.env.OCAS_DIR = casDir; process.env.OCAS_HOME = casDir;
try { try {
const uwf = await createUwfStore(tmpDir); const uwf = await createUwfStore(tmpDir);
@@ -131,9 +131,9 @@ describe("suspended thread display", () => {
expect(idleItem!.statusDisplay).toBe("idle"); expect(idleItem!.statusDisplay).toBe("idle");
} finally { } finally {
if (originalCasDir === undefined) { if (originalCasDir === undefined) {
delete process.env.OCAS_DIR; delete process.env.OCAS_HOME;
} else { } else {
process.env.OCAS_DIR = originalCasDir; process.env.OCAS_HOME = originalCasDir;
} }
} }
}); });
@@ -141,8 +141,8 @@ describe("suspended thread display", () => {
test("thread show displays suspend info and resume hint", async () => { test("thread show displays suspend info and resume hint", async () => {
const casDir = join(tmpDir, "cas"); const casDir = join(tmpDir, "cas");
await mkdir(casDir, { recursive: true }); await mkdir(casDir, { recursive: true });
const originalCasDir = process.env.OCAS_DIR; const originalCasDir = process.env.OCAS_HOME;
process.env.OCAS_DIR = casDir; process.env.OCAS_HOME = casDir;
try { try {
const uwf = await createUwfStore(tmpDir); const uwf = await createUwfStore(tmpDir);
@@ -219,9 +219,9 @@ describe("suspended thread display", () => {
); );
} finally { } finally {
if (originalCasDir === undefined) { if (originalCasDir === undefined) {
delete process.env.OCAS_DIR; delete process.env.OCAS_HOME;
} else { } else {
process.env.OCAS_DIR = originalCasDir; process.env.OCAS_HOME = originalCasDir;
} }
} }
}); });
@@ -229,8 +229,8 @@ describe("suspended thread display", () => {
test("non-suspended threads do not show suspend markers or hints", async () => { test("non-suspended threads do not show suspend markers or hints", async () => {
const casDir = join(tmpDir, "cas"); const casDir = join(tmpDir, "cas");
await mkdir(casDir, { recursive: true }); await mkdir(casDir, { recursive: true });
const originalCasDir = process.env.OCAS_DIR; const originalCasDir = process.env.OCAS_HOME;
process.env.OCAS_DIR = casDir; process.env.OCAS_HOME = casDir;
try { try {
const uwf = await createUwfStore(tmpDir); const uwf = await createUwfStore(tmpDir);
@@ -278,9 +278,9 @@ describe("suspended thread display", () => {
expect(threadItem!.statusDisplay).toBe("idle"); expect(threadItem!.statusDisplay).toBe("idle");
} finally { } finally {
if (originalCasDir === undefined) { if (originalCasDir === undefined) {
delete process.env.OCAS_DIR; delete process.env.OCAS_HOME;
} else { } else {
process.env.OCAS_DIR = originalCasDir; process.env.OCAS_HOME = originalCasDir;
} }
} }
}); });
+1 -1
View File
@@ -57,7 +57,7 @@ const DETAIL_SCHEMA = {
async function makeUwfStore(storageRoot: string): Promise<UwfStore> { async function makeUwfStore(storageRoot: string): Promise<UwfStore> {
const casDir = join(storageRoot, "cas"); const casDir = join(storageRoot, "cas");
await mkdir(casDir, { recursive: true }); await mkdir(casDir, { recursive: true });
process.env.OCAS_DIR = casDir; process.env.OCAS_HOME = casDir;
return createUwfStore(storageRoot); return createUwfStore(storageRoot);
} }
@@ -13,7 +13,7 @@ import { createUwfStore, saveWorkflowRegistry } from "../store.js";
async function makeUwfStore(storageRoot: string): Promise<UwfStore> { async function makeUwfStore(storageRoot: string): Promise<UwfStore> {
const casDir = join(storageRoot, "cas"); const casDir = join(storageRoot, "cas");
await mkdir(casDir, { recursive: true }); await mkdir(casDir, { recursive: true });
process.env.OCAS_DIR = casDir; process.env.OCAS_HOME = casDir;
return createUwfStore(storageRoot); return createUwfStore(storageRoot);
} }
+4 -8
View File
@@ -118,17 +118,13 @@ export function getDefaultStorageRoot(): string {
/** /**
* Resolve storage root. * Resolve storage root.
* Priority: `UWF_STORAGE_ROOT` → `WORKFLOW_STORAGE_ROOT` → default. * Priority: `UWF_HOME` → default.
*/ */
export function resolveStorageRoot(): string { export function resolveStorageRoot(): string {
const primary = process.env.UWF_STORAGE_ROOT; const primary = process.env.UWF_HOME;
if (primary !== undefined && primary !== "") { if (primary !== undefined && primary !== "") {
return primary; return primary;
} }
const userOverride = process.env.WORKFLOW_STORAGE_ROOT;
if (userOverride !== undefined && userOverride !== "") {
return userOverride;
}
return getDefaultStorageRoot(); return getDefaultStorageRoot();
} }
@@ -142,10 +138,10 @@ export function getCasDir(storageRoot: string): string {
/** /**
* Returns the global CAS directory shared by all uwf and ocas tools. * Returns the global CAS directory shared by all uwf and ocas tools.
* Priority: `OCAS_DIR` → default ~/.ocas * Priority: `OCAS_HOME` → default ~/.ocas
*/ */
export function getGlobalCasDir(): string { export function getGlobalCasDir(): string {
const primary = process.env.OCAS_DIR; const primary = process.env.OCAS_HOME;
if (primary !== undefined && primary !== "") { if (primary !== undefined && primary !== "") {
return primary; return primary;
} }
+23 -3
View File
@@ -50,10 +50,19 @@ Agent CLIs call `createAgent(...)` and invoke the returned function as `main()`.
### Context ### Context
```typescript ```typescript
function buildContext(threadId: ThreadId, role: string): Promise<AgentContext> function buildContext(
threadId: ThreadId,
role: string,
edgePrompt: string,
storageRoot: string,
casDir: string,
): Promise<AgentContext>
function buildContextWithMeta( function buildContextWithMeta(
threadId: ThreadId, threadId: ThreadId,
role: string, role: string,
edgePrompt: string,
storageRoot: string,
casDir: string,
): Promise<AgentContext & { meta: BuildContextMeta }> ): Promise<AgentContext & { meta: BuildContextMeta }>
type AgentContext = ModeratorContext & { type AgentContext = ModeratorContext & {
@@ -64,6 +73,8 @@ type AgentContext = ModeratorContext & {
outputFormatInstruction: string; outputFormatInstruction: string;
edgePrompt: string; edgePrompt: string;
isFirstVisit: boolean; isFirstVisit: boolean;
storageRoot: string;
casDir: string;
}; };
type BuildContextMeta = { type BuildContextMeta = {
@@ -99,6 +110,8 @@ function extract(
rawOutput: string, rawOutput: string,
outputSchema: CasRef, outputSchema: CasRef,
config: WorkflowConfig, config: WorkflowConfig,
storageRoot: string,
casDir: string,
): Promise<ExtractResult> ): Promise<ExtractResult>
type ResolvedLlmProvider = { baseUrl: string; apiKey: string; model: string }; type ResolvedLlmProvider = { baseUrl: string; apiKey: string; model: string };
@@ -120,11 +133,18 @@ type FrontmatterFastPathResult = { body: string; outputHash: CasRef };
### Session cache ### Session cache
```typescript ```typescript
function getCachedSessionId(threadId: ThreadId, role: string): Promise<string | null> function getCachedSessionId(
agentName: string,
threadId: ThreadId,
role: string,
storageRoot: string,
): Promise<string | null>
function setCachedSessionId( function setCachedSessionId(
agentName: string,
threadId: ThreadId, threadId: ThreadId,
role: string, role: string,
sessionId: string, sessionId: string,
storageRoot: string,
): Promise<void> ): Promise<void>
``` ```
@@ -133,7 +153,7 @@ function setCachedSessionId(
```typescript ```typescript
function getConfigPath(storageRoot: string): string function getConfigPath(storageRoot: string): string
function getEnvPath(storageRoot: string): string function getEnvPath(storageRoot: string): string
function resolveStorageRoot(): string function resolveStorageRoot(override: string | null): string
function loadWorkflowConfig(storageRoot: string): Promise<WorkflowConfig> function loadWorkflowConfig(storageRoot: string): Promise<WorkflowConfig>
``` ```
@@ -4,37 +4,31 @@ import type { ThreadId } from "@united-workforce/protocol";
import { afterEach, beforeEach, describe, expect, test } from "vitest"; import { afterEach, beforeEach, describe, expect, test } from "vitest";
import { getCachedSessionId, getCachePath, setCachedSessionId } from "../src/session-cache.js"; import { getCachedSessionId, getCachePath, setCachedSessionId } from "../src/session-cache.js";
import { resolveStorageRoot } from "../src/storage.js"; import { getDefaultStorageRoot } from "../src/storage.js";
describe("session-cache", () => { describe("session-cache", () => {
let originalStorageRoot: string;
let testStorageRoot: string; let testStorageRoot: string;
beforeEach(async () => { beforeEach(async () => {
// Create a temporary test storage root // Create a temporary test storage root
originalStorageRoot = resolveStorageRoot(); testStorageRoot = join(getDefaultStorageRoot(), "test-cache", `test-${Date.now()}`);
testStorageRoot = join(originalStorageRoot, "test-cache", `test-${Date.now()}`);
await mkdir(testStorageRoot, { recursive: true }); await mkdir(testStorageRoot, { recursive: true });
// Override the storage root for testing
process.env.WORKFLOW_STORAGE_ROOT = testStorageRoot;
}); });
afterEach(async () => { afterEach(async () => {
// Clean up test storage root // Clean up test storage root
await rm(testStorageRoot, { recursive: true, force: true }); await rm(testStorageRoot, { recursive: true, force: true });
delete process.env.WORKFLOW_STORAGE_ROOT;
}); });
describe("getCachePath", () => { describe("getCachePath", () => {
test("returns agent-specific file path", () => { test("returns agent-specific file path", () => {
const path = getCachePath("claude-code"); const path = getCachePath("claude-code", testStorageRoot);
expect(path).toMatch(/\/cache\/claude-code-sessions\.json$/); expect(path).toMatch(/\/cache\/claude-code-sessions\.json$/);
}); });
test("returns different paths for different agents", () => { test("returns different paths for different agents", () => {
const pathClaudeCode = getCachePath("claude-code"); const pathClaudeCode = getCachePath("claude-code", testStorageRoot);
const pathHermes = getCachePath("hermes"); const pathHermes = getCachePath("hermes", testStorageRoot);
expect(pathClaudeCode).not.toBe(pathHermes); expect(pathClaudeCode).not.toBe(pathHermes);
expect(pathClaudeCode).toMatch(/claude-code-sessions\.json$/); expect(pathClaudeCode).toMatch(/claude-code-sessions\.json$/);
@@ -42,8 +36,8 @@ describe("session-cache", () => {
}); });
test("handles agent names with special characters", () => { test("handles agent names with special characters", () => {
const path1 = getCachePath("my-agent"); const path1 = getCachePath("my-agent", testStorageRoot);
const path2 = getCachePath("my_agent"); const path2 = getCachePath("my_agent", testStorageRoot);
expect(path1).toMatch(/my-agent-sessions\.json$/); expect(path1).toMatch(/my-agent-sessions\.json$/);
expect(path2).toMatch(/my_agent-sessions\.json$/); expect(path2).toMatch(/my_agent-sessions\.json$/);
@@ -56,12 +50,12 @@ describe("session-cache", () => {
test("sessions are isolated per agent", async () => { test("sessions are isolated per agent", async () => {
// Cache different session IDs for each agent // Cache different session IDs for each agent
await setCachedSessionId("claude-code", threadId, role, "session-cc-001"); await setCachedSessionId("claude-code", threadId, role, "session-cc-001", testStorageRoot);
await setCachedSessionId("hermes", threadId, role, "session-hermes-001"); await setCachedSessionId("hermes", threadId, role, "session-hermes-001", testStorageRoot);
// Each agent should retrieve its own session ID // Each agent should retrieve its own session ID
const sessionCC = await getCachedSessionId("claude-code", threadId, role); const sessionCC = await getCachedSessionId("claude-code", threadId, role, testStorageRoot);
const sessionHermes = await getCachedSessionId("hermes", threadId, role); const sessionHermes = await getCachedSessionId("hermes", threadId, role, testStorageRoot);
expect(sessionCC).toBe("session-cc-001"); expect(sessionCC).toBe("session-cc-001");
expect(sessionHermes).toBe("session-hermes-001"); expect(sessionHermes).toBe("session-hermes-001");
@@ -69,30 +63,30 @@ describe("session-cache", () => {
test("updating one agent's cache does not affect another", async () => { test("updating one agent's cache does not affect another", async () => {
// Set initial sessions for both agents // Set initial sessions for both agents
await setCachedSessionId("claude-code", threadId, role, "session-cc-001"); await setCachedSessionId("claude-code", threadId, role, "session-cc-001", testStorageRoot);
await setCachedSessionId("hermes", threadId, role, "session-hermes-001"); await setCachedSessionId("hermes", threadId, role, "session-hermes-001", testStorageRoot);
// Update claude-code's session // Update claude-code's session
await setCachedSessionId("claude-code", threadId, role, "session-cc-002"); await setCachedSessionId("claude-code", threadId, role, "session-cc-002", testStorageRoot);
// Hermes's session should remain unchanged // Hermes's session should remain unchanged
const sessionHermes = await getCachedSessionId("hermes", threadId, role); const sessionHermes = await getCachedSessionId("hermes", threadId, role, testStorageRoot);
expect(sessionHermes).toBe("session-hermes-001"); expect(sessionHermes).toBe("session-hermes-001");
// Claude-code should have the new session // Claude-code should have the new session
const sessionCC = await getCachedSessionId("claude-code", threadId, role); const sessionCC = await getCachedSessionId("claude-code", threadId, role, testStorageRoot);
expect(sessionCC).toBe("session-cc-002"); expect(sessionCC).toBe("session-cc-002");
}); });
test("missing session returns null for specific agent", async () => { test("missing session returns null for specific agent", async () => {
const session = await getCachedSessionId("claude-code", threadId, role); const session = await getCachedSessionId("claude-code", threadId, role, testStorageRoot);
expect(session).toBeNull(); expect(session).toBeNull();
}); });
test("empty session ID is treated as missing", async () => { test("empty session ID is treated as missing", async () => {
await setCachedSessionId("claude-code", threadId, role, ""); await setCachedSessionId("claude-code", threadId, role, "", testStorageRoot);
const session = await getCachedSessionId("claude-code", threadId, role); const session = await getCachedSessionId("claude-code", threadId, role, testStorageRoot);
expect(session).toBeNull(); expect(session).toBeNull();
}); });
}); });
@@ -102,14 +96,14 @@ describe("session-cache", () => {
const role = "developer"; const role = "developer";
test("cache directory is created if missing", async () => { test("cache directory is created if missing", async () => {
const cachePath = getCachePath("claude-code"); const cachePath = getCachePath("claude-code", testStorageRoot);
const cacheDir = dirname(cachePath); const cacheDir = dirname(cachePath);
// Ensure cache dir doesn't exist // Ensure cache dir doesn't exist
await rm(cacheDir, { recursive: true, force: true }); await rm(cacheDir, { recursive: true, force: true });
// Write a session // Write a session
await setCachedSessionId("claude-code", threadId, role, "session-001"); await setCachedSessionId("claude-code", threadId, role, "session-001", testStorageRoot);
// Cache directory should be created // Cache directory should be created
const stats = await stat(cacheDir); const stats = await stat(cacheDir);
@@ -118,12 +112,12 @@ describe("session-cache", () => {
test("multiple agents create separate cache files", async () => { test("multiple agents create separate cache files", async () => {
// Cache sessions for multiple agents // Cache sessions for multiple agents
await setCachedSessionId("claude-code", threadId, role, "session-cc-001"); await setCachedSessionId("claude-code", threadId, role, "session-cc-001", testStorageRoot);
await setCachedSessionId("hermes", threadId, role, "session-hermes-001"); await setCachedSessionId("hermes", threadId, role, "session-hermes-001", testStorageRoot);
// Separate cache files should exist // Separate cache files should exist
const pathCC = getCachePath("claude-code"); const pathCC = getCachePath("claude-code", testStorageRoot);
const pathHermes = getCachePath("hermes"); const pathHermes = getCachePath("hermes", testStorageRoot);
const contentCC = JSON.parse(await readFile(pathCC, "utf8")) as Record<string, string>; const contentCC = JSON.parse(await readFile(pathCC, "utf8")) as Record<string, string>;
const contentHermes = JSON.parse(await readFile(pathHermes, "utf8")) as Record< const contentHermes = JSON.parse(await readFile(pathHermes, "utf8")) as Record<
@@ -137,10 +131,10 @@ describe("session-cache", () => {
test("atomic writes prevent partial reads", async () => { test("atomic writes prevent partial reads", async () => {
// Write a session // Write a session
await setCachedSessionId("claude-code", threadId, role, "session-001"); await setCachedSessionId("claude-code", threadId, role, "session-001", testStorageRoot);
// The final file should exist (no .tmp files left behind) // The final file should exist (no .tmp files left behind)
const cachePath = getCachePath("claude-code"); const cachePath = getCachePath("claude-code", testStorageRoot);
const dir = dirname(cachePath); const dir = dirname(cachePath);
const files = await readdir(dir); const files = await readdir(dir);
@@ -155,7 +149,7 @@ describe("session-cache", () => {
test("old agent-sessions.json is ignored", async () => { test("old agent-sessions.json is ignored", async () => {
// Create old agent-sessions.json file // Create old agent-sessions.json file
const oldCachePath = join(resolveStorageRoot(), "cache", "agent-sessions.json"); const oldCachePath = join(testStorageRoot, "cache", "agent-sessions.json");
await mkdir(dirname(oldCachePath), { recursive: true }); await mkdir(dirname(oldCachePath), { recursive: true });
await writeFile( await writeFile(
oldCachePath, oldCachePath,
@@ -166,7 +160,7 @@ describe("session-cache", () => {
); );
// Query with the new per-agent cache // Query with the new per-agent cache
const session = await getCachedSessionId("claude-code", threadId, role); const session = await getCachedSessionId("claude-code", threadId, role, testStorageRoot);
// Should return null (old cache is ignored) // Should return null (old cache is ignored)
expect(session).toBeNull(); expect(session).toBeNull();
@@ -174,7 +168,7 @@ describe("session-cache", () => {
test("new per-agent cache takes precedence", async () => { test("new per-agent cache takes precedence", async () => {
// Create both old and new cache files // Create both old and new cache files
const oldPath = join(resolveStorageRoot(), "cache", "agent-sessions.json"); const oldPath = join(testStorageRoot, "cache", "agent-sessions.json");
await mkdir(dirname(oldPath), { recursive: true }); await mkdir(dirname(oldPath), { recursive: true });
await writeFile( await writeFile(
oldPath, oldPath,
@@ -184,10 +178,10 @@ describe("session-cache", () => {
"utf8", "utf8",
); );
await setCachedSessionId("claude-code", threadId, role, "new-session"); await setCachedSessionId("claude-code", threadId, role, "new-session", testStorageRoot);
// The new per-agent cache value should be returned // The new per-agent cache value should be returned
const session = await getCachedSessionId("claude-code", threadId, role); const session = await getCachedSessionId("claude-code", threadId, role, testStorageRoot);
expect(session).toBe("new-session"); expect(session).toBe("new-session");
}); });
}); });
@@ -198,29 +192,29 @@ describe("session-cache", () => {
test("invalid JSON in cache file returns empty cache", async () => { test("invalid JSON in cache file returns empty cache", async () => {
// Create a corrupted cache file // Create a corrupted cache file
const cachePath = getCachePath("claude-code"); const cachePath = getCachePath("claude-code", testStorageRoot);
await mkdir(dirname(cachePath), { recursive: true }); await mkdir(dirname(cachePath), { recursive: true });
await writeFile(cachePath, "{ invalid json }", "utf8"); await writeFile(cachePath, "{ invalid json }", "utf8");
// Should return null (treating corrupted cache as empty) // Should return null (treating corrupted cache as empty)
const session = await getCachedSessionId("claude-code", threadId, role); const session = await getCachedSessionId("claude-code", threadId, role, testStorageRoot);
expect(session).toBeNull(); expect(session).toBeNull();
}); });
test("non-object JSON in cache file returns empty cache", async () => { test("non-object JSON in cache file returns empty cache", async () => {
// Create a cache file with non-object JSON // Create a cache file with non-object JSON
const cachePath = getCachePath("claude-code"); const cachePath = getCachePath("claude-code", testStorageRoot);
await mkdir(dirname(cachePath), { recursive: true }); await mkdir(dirname(cachePath), { recursive: true });
await writeFile(cachePath, JSON.stringify(["not", "an", "object"]), "utf8"); await writeFile(cachePath, JSON.stringify(["not", "an", "object"]), "utf8");
// Should return null // Should return null
const session = await getCachedSessionId("claude-code", threadId, role); const session = await getCachedSessionId("claude-code", threadId, role, testStorageRoot);
expect(session).toBeNull(); expect(session).toBeNull();
}); });
test("cache entries with non-string values are ignored", async () => { test("cache entries with non-string values are ignored", async () => {
// Create a cache file with mixed types // Create a cache file with mixed types
const cachePath = getCachePath("claude-code"); const cachePath = getCachePath("claude-code", testStorageRoot);
const cacheData = { const cacheData = {
"thread1:role1": "valid-session", "thread1:role1": "valid-session",
"thread2:role2": 12345, // number "thread2:role2": 12345, // number
@@ -231,13 +225,33 @@ describe("session-cache", () => {
await writeFile(cachePath, JSON.stringify(cacheData), "utf8"); await writeFile(cachePath, JSON.stringify(cacheData), "utf8");
// Valid string entries should be returned // Valid string entries should be returned
const session1 = await getCachedSessionId("claude-code", "thread1" as ThreadId, "role1"); const session1 = await getCachedSessionId(
"claude-code",
"thread1" as ThreadId,
"role1",
testStorageRoot,
);
expect(session1).toBe("valid-session"); expect(session1).toBe("valid-session");
// Invalid entries should return null // Invalid entries should return null
const session2 = await getCachedSessionId("claude-code", "thread2" as ThreadId, "role2"); const session2 = await getCachedSessionId(
const session3 = await getCachedSessionId("claude-code", "thread3" as ThreadId, "role3"); "claude-code",
const session4 = await getCachedSessionId("claude-code", "thread4" as ThreadId, "role4"); "thread2" as ThreadId,
"role2",
testStorageRoot,
);
const session3 = await getCachedSessionId(
"claude-code",
"thread3" as ThreadId,
"role3",
testStorageRoot,
);
const session4 = await getCachedSessionId(
"claude-code",
"thread4" as ThreadId,
"role4",
testStorageRoot,
);
expect(session2).toBeNull(); expect(session2).toBeNull();
expect(session3).toBeNull(); expect(session3).toBeNull();
+13 -44
View File
@@ -1,6 +1,6 @@
import { homedir } from "node:os"; import { homedir } from "node:os";
import { join } from "node:path"; import { join } from "node:path";
import { describe, it, expect, beforeEach, afterEach } from "vitest"; import { describe, it, expect } from "vitest";
import { import {
resolveStorageRoot, resolveStorageRoot,
getDefaultStorageRoot, getDefaultStorageRoot,
@@ -26,42 +26,16 @@ describe("getDefaultStorageRoot", () => {
}); });
describe("resolveStorageRoot", () => { describe("resolveStorageRoot", () => {
const saved: Record<string, string | undefined> = {}; it("uses the override when provided", () => {
expect(resolveStorageRoot("/tmp/uwf1")).toBe("/tmp/uwf1");
beforeEach(() => {
saved.UWF_STORAGE_ROOT = process.env.UWF_STORAGE_ROOT;
saved.WORKFLOW_STORAGE_ROOT = process.env.WORKFLOW_STORAGE_ROOT;
}); });
afterEach(() => { it("falls back to default when override is null", () => {
for (const k of ["UWF_STORAGE_ROOT", "WORKFLOW_STORAGE_ROOT"] as const) { expect(resolveStorageRoot(null)).toBe(getDefaultStorageRoot());
if (saved[k] === undefined) delete process.env[k];
else process.env[k] = saved[k];
}
}); });
it("uses UWF_STORAGE_ROOT first", () => { it("ignores empty override", () => {
process.env.UWF_STORAGE_ROOT = "/tmp/uwf1"; expect(resolveStorageRoot("")).toBe(getDefaultStorageRoot());
process.env.WORKFLOW_STORAGE_ROOT = "/tmp/uwf2";
expect(resolveStorageRoot()).toBe("/tmp/uwf1");
});
it("falls back to WORKFLOW_STORAGE_ROOT", () => {
delete process.env.UWF_STORAGE_ROOT;
process.env.WORKFLOW_STORAGE_ROOT = "/tmp/uwf2";
expect(resolveStorageRoot()).toBe("/tmp/uwf2");
});
it("falls back to default when both unset", () => {
delete process.env.UWF_STORAGE_ROOT;
delete process.env.WORKFLOW_STORAGE_ROOT;
expect(resolveStorageRoot()).toBe(getDefaultStorageRoot());
});
it("ignores empty UWF_STORAGE_ROOT", () => {
process.env.UWF_STORAGE_ROOT = "";
process.env.WORKFLOW_STORAGE_ROOT = "/tmp/uwf2";
expect(resolveStorageRoot()).toBe("/tmp/uwf2");
}); });
}); });
@@ -72,21 +46,16 @@ describe("path helpers", () => {
}); });
describe("getGlobalCasDir", () => { describe("getGlobalCasDir", () => {
const saved = { OCAS_DIR: process.env.OCAS_DIR }; it("uses the override when provided", () => {
expect(getGlobalCasDir("/tmp/ocas")).toBe("/tmp/ocas");
afterEach(() => {
if (saved.OCAS_DIR === undefined) delete process.env.OCAS_DIR;
else process.env.OCAS_DIR = saved.OCAS_DIR;
}); });
it("uses OCAS_DIR when set", () => { it("defaults to ~/.ocas when override is null", () => {
process.env.OCAS_DIR = "/tmp/ocas"; expect(getGlobalCasDir(null)).toBe(join(homedir(), ".ocas"));
expect(getGlobalCasDir()).toBe("/tmp/ocas");
}); });
it("defaults to ~/.ocas", () => { it("ignores empty override", () => {
delete process.env.OCAS_DIR; expect(getGlobalCasDir("")).toBe(join(homedir(), ".ocas"));
expect(getGlobalCasDir()).toBe(join(homedir(), ".ocas"));
}); });
}); });
+13 -7
View File
@@ -7,7 +7,7 @@ import type {
ThreadId, ThreadId,
} from "@united-workforce/protocol"; } from "@united-workforce/protocol";
import type { AgentStore } from "./storage.js"; import type { AgentStore } from "./storage.js";
import { createAgentStore, getActiveThreadEntry, resolveStorageRoot } from "./storage.js"; import { createAgentStore, getActiveThreadEntry } from "./storage.js";
import type { AgentContext } from "./types.js"; import type { AgentContext } from "./types.js";
type ChainState = { type ChainState = {
@@ -157,12 +157,13 @@ export async function buildContext(
threadId: ThreadId, threadId: ThreadId,
role: string, role: string,
edgePrompt: string, edgePrompt: string,
storageRoot: string,
casDir: string,
): Promise<AgentContext> { ): Promise<AgentContext> {
const storageRoot = resolveStorageRoot(); const agentStore = await createAgentStore(storageRoot, casDir);
const agentStore = await createAgentStore(storageRoot);
const { store, schemas } = agentStore; const { store, schemas } = agentStore;
const entry = await getActiveThreadEntry(storageRoot, threadId); const entry = await getActiveThreadEntry(casDir, threadId);
if (entry === null) { if (entry === null) {
fail(`thread not found in active thread index: ${threadId}`); fail(`thread not found in active thread index: ${threadId}`);
} }
@@ -187,6 +188,8 @@ export async function buildContext(
outputFormatInstruction: "", outputFormatInstruction: "",
edgePrompt, edgePrompt,
isFirstVisit, isFirstVisit,
storageRoot,
casDir,
}; };
} }
@@ -205,12 +208,13 @@ export async function buildContextWithMeta(
threadId: ThreadId, threadId: ThreadId,
role: string, role: string,
edgePrompt: string, edgePrompt: string,
storageRoot: string,
casDir: string,
): Promise<AgentContext & { meta: BuildContextMeta }> { ): Promise<AgentContext & { meta: BuildContextMeta }> {
const storageRoot = resolveStorageRoot(); const agentStore = await createAgentStore(storageRoot, casDir);
const agentStore = await createAgentStore(storageRoot);
const { store, schemas } = agentStore; const { store, schemas } = agentStore;
const entry = await getActiveThreadEntry(storageRoot, threadId); const entry = await getActiveThreadEntry(casDir, threadId);
if (entry === null) { if (entry === null) {
fail(`thread not found in active thread index: ${threadId}`); fail(`thread not found in active thread index: ${threadId}`);
} }
@@ -235,6 +239,8 @@ export async function buildContextWithMeta(
outputFormatInstruction: "", outputFormatInstruction: "",
edgePrompt, edgePrompt,
isFirstVisit, isFirstVisit,
storageRoot,
casDir,
meta: { storageRoot, store, schemas, headHash: entry.head, chain }, meta: { storageRoot, store, schemas, headHash: entry.head, chain },
}; };
} }
+4 -4
View File
@@ -1,7 +1,7 @@
import { getSchema, validate } from "@ocas/core"; import { getSchema, validate } from "@ocas/core";
import type { CasRef, ModelAlias, WorkflowConfig } from "@united-workforce/protocol"; import type { CasRef, ModelAlias, WorkflowConfig } from "@united-workforce/protocol";
import { createAgentStore, resolveStorageRoot } from "./storage.js"; import { createAgentStore } from "./storage.js";
export type ResolvedLlmProvider = { export type ResolvedLlmProvider = {
baseUrl: string; baseUrl: string;
@@ -135,10 +135,10 @@ export async function extract(
rawOutput: string, rawOutput: string,
outputSchema: CasRef, outputSchema: CasRef,
config: WorkflowConfig, config: WorkflowConfig,
storageRoot: string,
casDir: string,
): Promise<ExtractResult> { ): Promise<ExtractResult> {
const storageRoot = resolveStorageRoot(); const { store } = await createAgentStore(storageRoot, casDir);
const { store } = await createAgentStore(storageRoot);
const schema = getSchema(store, outputSchema); const schema = getSchema(store, outputSchema);
if (schema === null) { if (schema === null) {
throw new Error(`output schema not found in CAS: ${outputSchema}`); throw new Error(`output schema not found in CAS: ${outputSchema}`);
+17 -3
View File
@@ -5,7 +5,7 @@ import { buildOutputFormatInstruction } from "./build-output-format-instruction.
import { buildContextWithMeta } from "./context.js"; import { buildContextWithMeta } from "./context.js";
import { tryFrontmatterFastPath } from "./frontmatter.js"; import { tryFrontmatterFastPath } from "./frontmatter.js";
import type { AgentStore } from "./storage.js"; import type { AgentStore } from "./storage.js";
import { getEnvPath, resolveStorageRoot } from "./storage.js"; import { getEnvPath, getGlobalCasDir, resolveStorageRoot } from "./storage.js";
import type { AdapterOutput, AgentOptions } from "./types.js"; import type { AdapterOutput, AgentOptions } from "./types.js";
const MAX_FRONTMATTER_RETRIES = 2; const MAX_FRONTMATTER_RETRIES = 2;
@@ -135,13 +135,27 @@ async function persistStep(options: {
}); });
} }
/**
* Resolve uwf storage root + global CAS directory from the process env.
* This is the agent CLI entry point — the only place in this package allowed
* to read `process.env` for these settings.
*/
function resolveAgentDirs(): { storageRoot: string; casDir: string } {
return {
storageRoot: resolveStorageRoot(process.env.UWF_HOME ?? null),
casDir: getGlobalCasDir(process.env.OCAS_HOME ?? null),
};
}
export function createAgent(options: AgentOptions): () => Promise<void> { export function createAgent(options: AgentOptions): () => Promise<void> {
return async function main(): Promise<void> { return async function main(): Promise<void> {
const { threadId, role, prompt } = parseArgv(process.argv); const { threadId, role, prompt } = parseArgv(process.argv);
const storageRoot = resolveStorageRoot(); const { storageRoot, casDir } = resolveAgentDirs();
loadDotenv({ path: getEnvPath(storageRoot) }); loadDotenv({ path: getEnvPath(storageRoot) });
const ctx = await runWithMessage("context", () => buildContextWithMeta(threadId, role, prompt)); const ctx = await runWithMessage("context", () =>
buildContextWithMeta(threadId, role, prompt, storageRoot, casDir),
);
const roleDef = ctx.workflow.roles[role]; const roleDef = ctx.workflow.roles[role];
if (roleDef === undefined) { if (roleDef === undefined) {
+15 -11
View File
@@ -4,12 +4,10 @@ import { dirname, join } from "node:path";
import type { ThreadId } from "@united-workforce/protocol"; import type { ThreadId } from "@united-workforce/protocol";
import { resolveStorageRoot } from "./storage.js";
type SessionCache = Record<string, string>; type SessionCache = Record<string, string>;
export function getCachePath(agentName: string): string { export function getCachePath(agentName: string, storageRoot: string): string {
return join(resolveStorageRoot(), "cache", `${agentName}-sessions.json`); return join(storageRoot, "cache", `${agentName}-sessions.json`);
} }
function cacheKey(threadId: ThreadId, role: string): string { function cacheKey(threadId: ThreadId, role: string): string {
@@ -20,8 +18,8 @@ function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null && !Array.isArray(value); return typeof value === "object" && value !== null && !Array.isArray(value);
} }
async function readCache(agentName: string): Promise<SessionCache> { async function readCache(agentName: string, storageRoot: string): Promise<SessionCache> {
const path = getCachePath(agentName); const path = getCachePath(agentName, storageRoot);
try { try {
const text = await readFile(path, "utf8"); const text = await readFile(path, "utf8");
const raw = JSON.parse(text) as unknown; const raw = JSON.parse(text) as unknown;
@@ -48,8 +46,12 @@ async function readCache(agentName: string): Promise<SessionCache> {
} }
} }
async function writeCache(agentName: string, cache: SessionCache): Promise<void> { async function writeCache(
const path = getCachePath(agentName); agentName: string,
storageRoot: string,
cache: SessionCache,
): Promise<void> {
const path = getCachePath(agentName, storageRoot);
const dir = dirname(path); const dir = dirname(path);
await mkdir(dir, { recursive: true }); await mkdir(dir, { recursive: true });
// Atomic write: write to temp file then rename to avoid partial reads on concurrent access. // Atomic write: write to temp file then rename to avoid partial reads on concurrent access.
@@ -65,8 +67,9 @@ export async function getCachedSessionId(
agentName: string, agentName: string,
threadId: ThreadId, threadId: ThreadId,
role: string, role: string,
storageRoot: string,
): Promise<string | null> { ): Promise<string | null> {
const cache = await readCache(agentName); const cache = await readCache(agentName, storageRoot);
const sessionId = cache[cacheKey(threadId, role)]; const sessionId = cache[cacheKey(threadId, role)];
return sessionId ?? null; return sessionId ?? null;
} }
@@ -77,8 +80,9 @@ export async function setCachedSessionId(
threadId: ThreadId, threadId: ThreadId,
role: string, role: string,
sessionId: string, sessionId: string,
storageRoot: string,
): Promise<void> { ): Promise<void> {
const cache = await readCache(agentName); const cache = await readCache(agentName, storageRoot);
cache[cacheKey(threadId, role)] = sessionId; cache[cacheKey(threadId, role)] = sessionId;
await writeCache(agentName, cache); await writeCache(agentName, storageRoot, cache);
} }
+13 -20
View File
@@ -28,17 +28,12 @@ export function getDefaultStorageRoot(): string {
} }
/** /**
* Resolve storage root. * Resolve storage root from an explicit override (e.g. the `UWF_HOME` value
* Priority: `UWF_STORAGE_ROOT` → `WORKFLOW_STORAGE_ROOT` → default. * read by the CLI entry point). Library code must not read `process.env`.
*/ */
export function resolveStorageRoot(): string { export function resolveStorageRoot(override: string | null): string {
const primary = process.env.UWF_STORAGE_ROOT; if (override !== null && override !== "") {
if (primary !== undefined && primary !== "") { return override;
return primary;
}
const userOverride = process.env.WORKFLOW_STORAGE_ROOT;
if (userOverride !== undefined && userOverride !== "") {
return userOverride;
} }
return getDefaultStorageRoot(); return getDefaultStorageRoot();
} }
@@ -58,13 +53,13 @@ export function getEnvPath(storageRoot: string): string {
const THREAD_VAR_PREFIX = "@uwf/thread/"; const THREAD_VAR_PREFIX = "@uwf/thread/";
/** /**
* Global CAS directory (same as uwf CLI). * Resolve the global CAS directory from an explicit override (e.g. the
* Priority: `OCAS_DIR` → default ~/.ocas * `OCAS_HOME` value read by the CLI entry point). Library code must not read
* `process.env`. Defaults to `~/.ocas`.
*/ */
export function getGlobalCasDir(): string { export function getGlobalCasDir(override: string | null): string {
const primary = process.env.OCAS_DIR; if (override !== null && override !== "") {
if (primary !== undefined && primary !== "") { return override;
return primary;
} }
return join(homedir(), ".ocas"); return join(homedir(), ".ocas");
} }
@@ -75,10 +70,9 @@ function threadVarName(threadId: ThreadId): string {
/** Read active thread head + suspend metadata from ocas variable store. */ /** Read active thread head + suspend metadata from ocas variable store. */
export async function getActiveThreadEntry( export async function getActiveThreadEntry(
_storageRoot: string, casDir: string,
threadId: ThreadId, threadId: ThreadId,
): Promise<ThreadIndexEntry | null> { ): Promise<ThreadIndexEntry | null> {
const casDir = getGlobalCasDir();
const cas = createFsStore(casDir); const cas = createFsStore(casDir);
const { var: varStore } = createSqliteVarStore(join(casDir, "vars"), cas); const { var: varStore } = createSqliteVarStore(join(casDir, "vars"), cas);
const vars = varStore.list({ exactName: threadVarName(threadId) }); const vars = varStore.list({ exactName: threadVarName(threadId) });
@@ -99,8 +93,7 @@ export type AgentStore = {
schemas: Awaited<ReturnType<typeof registerAgentSchemas>>; schemas: Awaited<ReturnType<typeof registerAgentSchemas>>;
}; };
export async function createAgentStore(storageRoot: string): Promise<AgentStore> { export async function createAgentStore(storageRoot: string, casDir: string): Promise<AgentStore> {
const casDir = getGlobalCasDir();
const cas = createFsStore(casDir); const cas = createFsStore(casDir);
const { var: varSub, tag } = createSqliteVarStore(join(casDir, "vars"), cas); const { var: varSub, tag } = createSqliteVarStore(join(casDir, "vars"), cas);
const store: Store = { cas, var: varSub, tag }; const store: Store = { cas, var: varSub, tag };
+4
View File
@@ -21,6 +21,10 @@ export type AgentContext = ModeratorContext & {
* True when the current role has not appeared in steps history before this invocation. * True when the current role has not appeared in steps history before this invocation.
*/ */
isFirstVisit: boolean; isFirstVisit: boolean;
/** Resolved uwf storage root (from `UWF_HOME`), threaded from the CLI entry point. */
storageRoot: string;
/** Resolved global CAS directory (from `OCAS_HOME`), threaded from the CLI entry point. */
casDir: string;
}; };
export type AgentRunResult = { export type AgentRunResult = {
+1 -1
View File
@@ -37,7 +37,7 @@ uwf setup --provider <name> --base-url <url> \\
[--agent <name>] # optional default agent [--agent <name>] # optional default agent
\`\`\` \`\`\`
Config is stored at \`~/.uwf/config.yaml\`. Override storage root with \`UWF_STORAGE_ROOT\` (or \`WORKFLOW_STORAGE_ROOT\`). Config is stored at \`~/.uwf/config.yaml\`. Override storage root with \`UWF_HOME\`.
## Workflow Commands ## Workflow Commands
+1 -1
View File
@@ -89,7 +89,7 @@ echo ""
echo "=== Config ===" echo "=== Config ==="
# Check workflow config exists # Check workflow config exists
CONFIG_DIR="${UWF_STORAGE_ROOT:-$HOME/.shazhou/united-workforce}" CONFIG_DIR="${UWF_HOME:-$HOME/.shazhou/united-workforce}"
check "config.yaml exists" \ check "config.yaml exists" \
"[ -f '$CONFIG_DIR/config.yaml' ]" \ "[ -f '$CONFIG_DIR/config.yaml' ]" \
"Run: uwf setup" "Run: uwf setup"
+4 -4
View File
@@ -1,6 +1,6 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# E2E walkthrough for shazhou/united-workforce. # E2E walkthrough for shazhou/united-workforce.
# Runs inside Docker with isolated UWF_STORAGE_ROOT. # Runs inside Docker with isolated UWF_HOME.
# Exercises: setup → workflow add → thread start/exec → cancel/fork → read/inspect. # Exercises: setup → workflow add → thread start/exec → cancel/fork → read/inspect.
# #
# Usage: # Usage:
@@ -70,8 +70,8 @@ cat > "$E2E_DIR/run.sh" << 'INNER_SCRIPT'
set -euo pipefail set -euo pipefail
# Isolated storage — never touches host's ~/.uwf # Isolated storage — never touches host's ~/.uwf
export UWF_STORAGE_ROOT="/tmp/uwf-e2e-storage" export UWF_HOME="/tmp/uwf-e2e-storage"
mkdir -p "$UWF_STORAGE_ROOT" mkdir -p "$UWF_HOME"
REPO_DIR="$1" REPO_DIR="$1"
AGENT="$2" AGENT="$2"
@@ -157,7 +157,7 @@ if [ -n "$PROVIDER" ] && [ -n "$MODEL" ] && [ -n "$API_KEY" ]; then
else else
# Copy host config if available # Copy host config if available
if [ -f "$HOME/.shazhou/united-workforce/config.yaml" ]; then if [ -f "$HOME/.shazhou/united-workforce/config.yaml" ]; then
cp "$HOME/.shazhou/united-workforce/config.yaml" "$UWF_STORAGE_ROOT/config.yaml" cp "$HOME/.shazhou/united-workforce/config.yaml" "$UWF_HOME/config.yaml"
echo " Copied host config.yaml" >&2 echo " Copied host config.yaml" >&2
fi fi
fi fi