refactor: unify env vars (UWF_HOME, OCAS_HOME) + env only in CLI (#37)
CI / check (pull_request) Failing after 3m6s
CI / check (pull_request) Failing after 3m6s
Breaking changes: - UWF_STORAGE_ROOT → UWF_HOME - WORKFLOW_STORAGE_ROOT removed (no fallback) - OCAS_DIR → OCAS_HOME (aligned with ocas CLI) Library functions no longer read process.env: - util-agent/storage.ts: resolveStorageRoot(override), getGlobalCasDir(override) - agent-hermes: isResumeDisabled(flag) pure function, CLI reads env - agent-claude-code: CLI reads CLAUDE_MODEL and passes to agent Fixes #37
This commit is contained in:
@@ -50,10 +50,19 @@ Agent CLIs call `createAgent(...)` and invoke the returned function as `main()`.
|
||||
### Context
|
||||
|
||||
```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(
|
||||
threadId: ThreadId,
|
||||
role: string,
|
||||
edgePrompt: string,
|
||||
storageRoot: string,
|
||||
casDir: string,
|
||||
): Promise<AgentContext & { meta: BuildContextMeta }>
|
||||
|
||||
type AgentContext = ModeratorContext & {
|
||||
@@ -64,6 +73,8 @@ type AgentContext = ModeratorContext & {
|
||||
outputFormatInstruction: string;
|
||||
edgePrompt: string;
|
||||
isFirstVisit: boolean;
|
||||
storageRoot: string;
|
||||
casDir: string;
|
||||
};
|
||||
|
||||
type BuildContextMeta = {
|
||||
@@ -99,6 +110,8 @@ function extract(
|
||||
rawOutput: string,
|
||||
outputSchema: CasRef,
|
||||
config: WorkflowConfig,
|
||||
storageRoot: string,
|
||||
casDir: string,
|
||||
): Promise<ExtractResult>
|
||||
|
||||
type ResolvedLlmProvider = { baseUrl: string; apiKey: string; model: string };
|
||||
@@ -120,11 +133,18 @@ type FrontmatterFastPathResult = { body: string; outputHash: CasRef };
|
||||
### Session cache
|
||||
|
||||
```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(
|
||||
agentName: string,
|
||||
threadId: ThreadId,
|
||||
role: string,
|
||||
sessionId: string,
|
||||
storageRoot: string,
|
||||
): Promise<void>
|
||||
```
|
||||
|
||||
@@ -133,7 +153,7 @@ function setCachedSessionId(
|
||||
```typescript
|
||||
function getConfigPath(storageRoot: string): string
|
||||
function getEnvPath(storageRoot: string): string
|
||||
function resolveStorageRoot(): string
|
||||
function resolveStorageRoot(override: string | null): string
|
||||
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 { getCachedSessionId, getCachePath, setCachedSessionId } from "../src/session-cache.js";
|
||||
import { resolveStorageRoot } from "../src/storage.js";
|
||||
import { getDefaultStorageRoot } from "../src/storage.js";
|
||||
|
||||
describe("session-cache", () => {
|
||||
let originalStorageRoot: string;
|
||||
let testStorageRoot: string;
|
||||
|
||||
beforeEach(async () => {
|
||||
// Create a temporary test storage root
|
||||
originalStorageRoot = resolveStorageRoot();
|
||||
testStorageRoot = join(originalStorageRoot, "test-cache", `test-${Date.now()}`);
|
||||
testStorageRoot = join(getDefaultStorageRoot(), "test-cache", `test-${Date.now()}`);
|
||||
await mkdir(testStorageRoot, { recursive: true });
|
||||
|
||||
// Override the storage root for testing
|
||||
process.env.WORKFLOW_STORAGE_ROOT = testStorageRoot;
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
// Clean up test storage root
|
||||
await rm(testStorageRoot, { recursive: true, force: true });
|
||||
delete process.env.WORKFLOW_STORAGE_ROOT;
|
||||
});
|
||||
|
||||
describe("getCachePath", () => {
|
||||
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$/);
|
||||
});
|
||||
|
||||
test("returns different paths for different agents", () => {
|
||||
const pathClaudeCode = getCachePath("claude-code");
|
||||
const pathHermes = getCachePath("hermes");
|
||||
const pathClaudeCode = getCachePath("claude-code", testStorageRoot);
|
||||
const pathHermes = getCachePath("hermes", testStorageRoot);
|
||||
|
||||
expect(pathClaudeCode).not.toBe(pathHermes);
|
||||
expect(pathClaudeCode).toMatch(/claude-code-sessions\.json$/);
|
||||
@@ -42,8 +36,8 @@ describe("session-cache", () => {
|
||||
});
|
||||
|
||||
test("handles agent names with special characters", () => {
|
||||
const path1 = getCachePath("my-agent");
|
||||
const path2 = getCachePath("my_agent");
|
||||
const path1 = getCachePath("my-agent", testStorageRoot);
|
||||
const path2 = getCachePath("my_agent", testStorageRoot);
|
||||
|
||||
expect(path1).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 () => {
|
||||
// Cache different session IDs for each agent
|
||||
await setCachedSessionId("claude-code", threadId, role, "session-cc-001");
|
||||
await setCachedSessionId("hermes", threadId, role, "session-hermes-001");
|
||||
await setCachedSessionId("claude-code", threadId, role, "session-cc-001", testStorageRoot);
|
||||
await setCachedSessionId("hermes", threadId, role, "session-hermes-001", testStorageRoot);
|
||||
|
||||
// Each agent should retrieve its own session ID
|
||||
const sessionCC = await getCachedSessionId("claude-code", threadId, role);
|
||||
const sessionHermes = await getCachedSessionId("hermes", threadId, role);
|
||||
const sessionCC = await getCachedSessionId("claude-code", threadId, role, testStorageRoot);
|
||||
const sessionHermes = await getCachedSessionId("hermes", threadId, role, testStorageRoot);
|
||||
|
||||
expect(sessionCC).toBe("session-cc-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 () => {
|
||||
// Set initial sessions for both agents
|
||||
await setCachedSessionId("claude-code", threadId, role, "session-cc-001");
|
||||
await setCachedSessionId("hermes", threadId, role, "session-hermes-001");
|
||||
await setCachedSessionId("claude-code", threadId, role, "session-cc-001", testStorageRoot);
|
||||
await setCachedSessionId("hermes", threadId, role, "session-hermes-001", testStorageRoot);
|
||||
|
||||
// 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
|
||||
const sessionHermes = await getCachedSessionId("hermes", threadId, role);
|
||||
const sessionHermes = await getCachedSessionId("hermes", threadId, role, testStorageRoot);
|
||||
expect(sessionHermes).toBe("session-hermes-001");
|
||||
|
||||
// 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");
|
||||
});
|
||||
|
||||
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();
|
||||
});
|
||||
|
||||
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();
|
||||
});
|
||||
});
|
||||
@@ -102,14 +96,14 @@ describe("session-cache", () => {
|
||||
const role = "developer";
|
||||
|
||||
test("cache directory is created if missing", async () => {
|
||||
const cachePath = getCachePath("claude-code");
|
||||
const cachePath = getCachePath("claude-code", testStorageRoot);
|
||||
const cacheDir = dirname(cachePath);
|
||||
|
||||
// Ensure cache dir doesn't exist
|
||||
await rm(cacheDir, { recursive: true, force: true });
|
||||
|
||||
// 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
|
||||
const stats = await stat(cacheDir);
|
||||
@@ -118,12 +112,12 @@ describe("session-cache", () => {
|
||||
|
||||
test("multiple agents create separate cache files", async () => {
|
||||
// Cache sessions for multiple agents
|
||||
await setCachedSessionId("claude-code", threadId, role, "session-cc-001");
|
||||
await setCachedSessionId("hermes", threadId, role, "session-hermes-001");
|
||||
await setCachedSessionId("claude-code", threadId, role, "session-cc-001", testStorageRoot);
|
||||
await setCachedSessionId("hermes", threadId, role, "session-hermes-001", testStorageRoot);
|
||||
|
||||
// Separate cache files should exist
|
||||
const pathCC = getCachePath("claude-code");
|
||||
const pathHermes = getCachePath("hermes");
|
||||
const pathCC = getCachePath("claude-code", testStorageRoot);
|
||||
const pathHermes = getCachePath("hermes", testStorageRoot);
|
||||
|
||||
const contentCC = JSON.parse(await readFile(pathCC, "utf8")) as Record<string, string>;
|
||||
const contentHermes = JSON.parse(await readFile(pathHermes, "utf8")) as Record<
|
||||
@@ -137,10 +131,10 @@ describe("session-cache", () => {
|
||||
|
||||
test("atomic writes prevent partial reads", async () => {
|
||||
// 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)
|
||||
const cachePath = getCachePath("claude-code");
|
||||
const cachePath = getCachePath("claude-code", testStorageRoot);
|
||||
const dir = dirname(cachePath);
|
||||
const files = await readdir(dir);
|
||||
|
||||
@@ -155,7 +149,7 @@ describe("session-cache", () => {
|
||||
|
||||
test("old agent-sessions.json is ignored", async () => {
|
||||
// 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 writeFile(
|
||||
oldCachePath,
|
||||
@@ -166,7 +160,7 @@ describe("session-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)
|
||||
expect(session).toBeNull();
|
||||
@@ -174,7 +168,7 @@ describe("session-cache", () => {
|
||||
|
||||
test("new per-agent cache takes precedence", async () => {
|
||||
// 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 writeFile(
|
||||
oldPath,
|
||||
@@ -184,10 +178,10 @@ describe("session-cache", () => {
|
||||
"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
|
||||
const session = await getCachedSessionId("claude-code", threadId, role);
|
||||
const session = await getCachedSessionId("claude-code", threadId, role, testStorageRoot);
|
||||
expect(session).toBe("new-session");
|
||||
});
|
||||
});
|
||||
@@ -198,29 +192,29 @@ describe("session-cache", () => {
|
||||
|
||||
test("invalid JSON in cache file returns empty cache", async () => {
|
||||
// Create a corrupted cache file
|
||||
const cachePath = getCachePath("claude-code");
|
||||
const cachePath = getCachePath("claude-code", testStorageRoot);
|
||||
await mkdir(dirname(cachePath), { recursive: true });
|
||||
await writeFile(cachePath, "{ invalid json }", "utf8");
|
||||
|
||||
// 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();
|
||||
});
|
||||
|
||||
test("non-object JSON in cache file returns empty cache", async () => {
|
||||
// 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 writeFile(cachePath, JSON.stringify(["not", "an", "object"]), "utf8");
|
||||
|
||||
// Should return null
|
||||
const session = await getCachedSessionId("claude-code", threadId, role);
|
||||
const session = await getCachedSessionId("claude-code", threadId, role, testStorageRoot);
|
||||
expect(session).toBeNull();
|
||||
});
|
||||
|
||||
test("cache entries with non-string values are ignored", async () => {
|
||||
// Create a cache file with mixed types
|
||||
const cachePath = getCachePath("claude-code");
|
||||
const cachePath = getCachePath("claude-code", testStorageRoot);
|
||||
const cacheData = {
|
||||
"thread1:role1": "valid-session",
|
||||
"thread2:role2": 12345, // number
|
||||
@@ -231,13 +225,33 @@ describe("session-cache", () => {
|
||||
await writeFile(cachePath, JSON.stringify(cacheData), "utf8");
|
||||
|
||||
// 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");
|
||||
|
||||
// Invalid entries should return null
|
||||
const session2 = await getCachedSessionId("claude-code", "thread2" as ThreadId, "role2");
|
||||
const session3 = await getCachedSessionId("claude-code", "thread3" as ThreadId, "role3");
|
||||
const session4 = await getCachedSessionId("claude-code", "thread4" as ThreadId, "role4");
|
||||
const session2 = await getCachedSessionId(
|
||||
"claude-code",
|
||||
"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(session3).toBeNull();
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { homedir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
||||
import { describe, it, expect } from "vitest";
|
||||
import {
|
||||
resolveStorageRoot,
|
||||
getDefaultStorageRoot,
|
||||
@@ -26,42 +26,16 @@ describe("getDefaultStorageRoot", () => {
|
||||
});
|
||||
|
||||
describe("resolveStorageRoot", () => {
|
||||
const saved: Record<string, string | undefined> = {};
|
||||
|
||||
beforeEach(() => {
|
||||
saved.UWF_STORAGE_ROOT = process.env.UWF_STORAGE_ROOT;
|
||||
saved.WORKFLOW_STORAGE_ROOT = process.env.WORKFLOW_STORAGE_ROOT;
|
||||
it("uses the override when provided", () => {
|
||||
expect(resolveStorageRoot("/tmp/uwf1")).toBe("/tmp/uwf1");
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
for (const k of ["UWF_STORAGE_ROOT", "WORKFLOW_STORAGE_ROOT"] as const) {
|
||||
if (saved[k] === undefined) delete process.env[k];
|
||||
else process.env[k] = saved[k];
|
||||
}
|
||||
it("falls back to default when override is null", () => {
|
||||
expect(resolveStorageRoot(null)).toBe(getDefaultStorageRoot());
|
||||
});
|
||||
|
||||
it("uses UWF_STORAGE_ROOT first", () => {
|
||||
process.env.UWF_STORAGE_ROOT = "/tmp/uwf1";
|
||||
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");
|
||||
it("ignores empty override", () => {
|
||||
expect(resolveStorageRoot("")).toBe(getDefaultStorageRoot());
|
||||
});
|
||||
});
|
||||
|
||||
@@ -72,21 +46,16 @@ describe("path helpers", () => {
|
||||
});
|
||||
|
||||
describe("getGlobalCasDir", () => {
|
||||
const saved = { OCAS_DIR: process.env.OCAS_DIR };
|
||||
|
||||
afterEach(() => {
|
||||
if (saved.OCAS_DIR === undefined) delete process.env.OCAS_DIR;
|
||||
else process.env.OCAS_DIR = saved.OCAS_DIR;
|
||||
it("uses the override when provided", () => {
|
||||
expect(getGlobalCasDir("/tmp/ocas")).toBe("/tmp/ocas");
|
||||
});
|
||||
|
||||
it("uses OCAS_DIR when set", () => {
|
||||
process.env.OCAS_DIR = "/tmp/ocas";
|
||||
expect(getGlobalCasDir()).toBe("/tmp/ocas");
|
||||
it("defaults to ~/.ocas when override is null", () => {
|
||||
expect(getGlobalCasDir(null)).toBe(join(homedir(), ".ocas"));
|
||||
});
|
||||
|
||||
it("defaults to ~/.ocas", () => {
|
||||
delete process.env.OCAS_DIR;
|
||||
expect(getGlobalCasDir()).toBe(join(homedir(), ".ocas"));
|
||||
it("ignores empty override", () => {
|
||||
expect(getGlobalCasDir("")).toBe(join(homedir(), ".ocas"));
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ import type {
|
||||
ThreadId,
|
||||
} from "@united-workforce/protocol";
|
||||
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";
|
||||
|
||||
type ChainState = {
|
||||
@@ -157,12 +157,13 @@ export async function buildContext(
|
||||
threadId: ThreadId,
|
||||
role: string,
|
||||
edgePrompt: string,
|
||||
storageRoot: string,
|
||||
casDir: string,
|
||||
): Promise<AgentContext> {
|
||||
const storageRoot = resolveStorageRoot();
|
||||
const agentStore = await createAgentStore(storageRoot);
|
||||
const agentStore = await createAgentStore(storageRoot, casDir);
|
||||
const { store, schemas } = agentStore;
|
||||
|
||||
const entry = await getActiveThreadEntry(storageRoot, threadId);
|
||||
const entry = await getActiveThreadEntry(casDir, threadId);
|
||||
if (entry === null) {
|
||||
fail(`thread not found in active thread index: ${threadId}`);
|
||||
}
|
||||
@@ -187,6 +188,8 @@ export async function buildContext(
|
||||
outputFormatInstruction: "",
|
||||
edgePrompt,
|
||||
isFirstVisit,
|
||||
storageRoot,
|
||||
casDir,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -205,12 +208,13 @@ export async function buildContextWithMeta(
|
||||
threadId: ThreadId,
|
||||
role: string,
|
||||
edgePrompt: string,
|
||||
storageRoot: string,
|
||||
casDir: string,
|
||||
): Promise<AgentContext & { meta: BuildContextMeta }> {
|
||||
const storageRoot = resolveStorageRoot();
|
||||
const agentStore = await createAgentStore(storageRoot);
|
||||
const agentStore = await createAgentStore(storageRoot, casDir);
|
||||
const { store, schemas } = agentStore;
|
||||
|
||||
const entry = await getActiveThreadEntry(storageRoot, threadId);
|
||||
const entry = await getActiveThreadEntry(casDir, threadId);
|
||||
if (entry === null) {
|
||||
fail(`thread not found in active thread index: ${threadId}`);
|
||||
}
|
||||
@@ -235,6 +239,8 @@ export async function buildContextWithMeta(
|
||||
outputFormatInstruction: "",
|
||||
edgePrompt,
|
||||
isFirstVisit,
|
||||
storageRoot,
|
||||
casDir,
|
||||
meta: { storageRoot, store, schemas, headHash: entry.head, chain },
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { getSchema, validate } from "@ocas/core";
|
||||
|
||||
import type { CasRef, ModelAlias, WorkflowConfig } from "@united-workforce/protocol";
|
||||
import { createAgentStore, resolveStorageRoot } from "./storage.js";
|
||||
import { createAgentStore } from "./storage.js";
|
||||
|
||||
export type ResolvedLlmProvider = {
|
||||
baseUrl: string;
|
||||
@@ -135,10 +135,10 @@ export async function extract(
|
||||
rawOutput: string,
|
||||
outputSchema: CasRef,
|
||||
config: WorkflowConfig,
|
||||
storageRoot: string,
|
||||
casDir: string,
|
||||
): Promise<ExtractResult> {
|
||||
const storageRoot = resolveStorageRoot();
|
||||
|
||||
const { store } = await createAgentStore(storageRoot);
|
||||
const { store } = await createAgentStore(storageRoot, casDir);
|
||||
const schema = getSchema(store, outputSchema);
|
||||
if (schema === null) {
|
||||
throw new Error(`output schema not found in CAS: ${outputSchema}`);
|
||||
|
||||
@@ -5,7 +5,7 @@ import { buildOutputFormatInstruction } from "./build-output-format-instruction.
|
||||
import { buildContextWithMeta } from "./context.js";
|
||||
import { tryFrontmatterFastPath } from "./frontmatter.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";
|
||||
|
||||
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> {
|
||||
return async function main(): Promise<void> {
|
||||
const { threadId, role, prompt } = parseArgv(process.argv);
|
||||
const storageRoot = resolveStorageRoot();
|
||||
const { storageRoot, casDir } = resolveAgentDirs();
|
||||
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];
|
||||
if (roleDef === undefined) {
|
||||
|
||||
@@ -4,12 +4,10 @@ import { dirname, join } from "node:path";
|
||||
|
||||
import type { ThreadId } from "@united-workforce/protocol";
|
||||
|
||||
import { resolveStorageRoot } from "./storage.js";
|
||||
|
||||
type SessionCache = Record<string, string>;
|
||||
|
||||
export function getCachePath(agentName: string): string {
|
||||
return join(resolveStorageRoot(), "cache", `${agentName}-sessions.json`);
|
||||
export function getCachePath(agentName: string, storageRoot: string): string {
|
||||
return join(storageRoot, "cache", `${agentName}-sessions.json`);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
async function readCache(agentName: string): Promise<SessionCache> {
|
||||
const path = getCachePath(agentName);
|
||||
async function readCache(agentName: string, storageRoot: string): Promise<SessionCache> {
|
||||
const path = getCachePath(agentName, storageRoot);
|
||||
try {
|
||||
const text = await readFile(path, "utf8");
|
||||
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> {
|
||||
const path = getCachePath(agentName);
|
||||
async function writeCache(
|
||||
agentName: string,
|
||||
storageRoot: string,
|
||||
cache: SessionCache,
|
||||
): Promise<void> {
|
||||
const path = getCachePath(agentName, storageRoot);
|
||||
const dir = dirname(path);
|
||||
await mkdir(dir, { recursive: true });
|
||||
// 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,
|
||||
threadId: ThreadId,
|
||||
role: string,
|
||||
storageRoot: string,
|
||||
): Promise<string | null> {
|
||||
const cache = await readCache(agentName);
|
||||
const cache = await readCache(agentName, storageRoot);
|
||||
const sessionId = cache[cacheKey(threadId, role)];
|
||||
return sessionId ?? null;
|
||||
}
|
||||
@@ -77,8 +80,9 @@ export async function setCachedSessionId(
|
||||
threadId: ThreadId,
|
||||
role: string,
|
||||
sessionId: string,
|
||||
storageRoot: string,
|
||||
): Promise<void> {
|
||||
const cache = await readCache(agentName);
|
||||
const cache = await readCache(agentName, storageRoot);
|
||||
cache[cacheKey(threadId, role)] = sessionId;
|
||||
await writeCache(agentName, cache);
|
||||
await writeCache(agentName, storageRoot, cache);
|
||||
}
|
||||
|
||||
@@ -28,17 +28,12 @@ export function getDefaultStorageRoot(): string {
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve storage root.
|
||||
* Priority: `UWF_STORAGE_ROOT` → `WORKFLOW_STORAGE_ROOT` → default.
|
||||
* Resolve storage root from an explicit override (e.g. the `UWF_HOME` value
|
||||
* read by the CLI entry point). Library code must not read `process.env`.
|
||||
*/
|
||||
export function resolveStorageRoot(): string {
|
||||
const primary = process.env.UWF_STORAGE_ROOT;
|
||||
if (primary !== undefined && primary !== "") {
|
||||
return primary;
|
||||
}
|
||||
const userOverride = process.env.WORKFLOW_STORAGE_ROOT;
|
||||
if (userOverride !== undefined && userOverride !== "") {
|
||||
return userOverride;
|
||||
export function resolveStorageRoot(override: string | null): string {
|
||||
if (override !== null && override !== "") {
|
||||
return override;
|
||||
}
|
||||
return getDefaultStorageRoot();
|
||||
}
|
||||
@@ -58,13 +53,13 @@ export function getEnvPath(storageRoot: string): string {
|
||||
const THREAD_VAR_PREFIX = "@uwf/thread/";
|
||||
|
||||
/**
|
||||
* Global CAS directory (same as uwf CLI).
|
||||
* Priority: `OCAS_DIR` → default ~/.ocas
|
||||
* Resolve the global CAS directory from an explicit override (e.g. the
|
||||
* `OCAS_HOME` value read by the CLI entry point). Library code must not read
|
||||
* `process.env`. Defaults to `~/.ocas`.
|
||||
*/
|
||||
export function getGlobalCasDir(): string {
|
||||
const primary = process.env.OCAS_DIR;
|
||||
if (primary !== undefined && primary !== "") {
|
||||
return primary;
|
||||
export function getGlobalCasDir(override: string | null): string {
|
||||
if (override !== null && override !== "") {
|
||||
return override;
|
||||
}
|
||||
return join(homedir(), ".ocas");
|
||||
}
|
||||
@@ -75,10 +70,9 @@ function threadVarName(threadId: ThreadId): string {
|
||||
|
||||
/** Read active thread head + suspend metadata from ocas variable store. */
|
||||
export async function getActiveThreadEntry(
|
||||
_storageRoot: string,
|
||||
casDir: string,
|
||||
threadId: ThreadId,
|
||||
): Promise<ThreadIndexEntry | null> {
|
||||
const casDir = getGlobalCasDir();
|
||||
const cas = createFsStore(casDir);
|
||||
const { var: varStore } = createSqliteVarStore(join(casDir, "vars"), cas);
|
||||
const vars = varStore.list({ exactName: threadVarName(threadId) });
|
||||
@@ -99,8 +93,7 @@ export type AgentStore = {
|
||||
schemas: Awaited<ReturnType<typeof registerAgentSchemas>>;
|
||||
};
|
||||
|
||||
export async function createAgentStore(storageRoot: string): Promise<AgentStore> {
|
||||
const casDir = getGlobalCasDir();
|
||||
export async function createAgentStore(storageRoot: string, casDir: string): Promise<AgentStore> {
|
||||
const cas = createFsStore(casDir);
|
||||
const { var: varSub, tag } = createSqliteVarStore(join(casDir, "vars"), cas);
|
||||
const store: Store = { cas, var: varSub, tag };
|
||||
|
||||
@@ -21,6 +21,10 @@ export type AgentContext = ModeratorContext & {
|
||||
* True when the current role has not appeared in steps history before this invocation.
|
||||
*/
|
||||
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 = {
|
||||
|
||||
Reference in New Issue
Block a user