diff --git a/packages/workflow-template-develop/bundle-entry.ts b/packages/workflow-template-develop/bundle-entry.ts index d178f2d..f330b20 100644 --- a/packages/workflow-template-develop/bundle-entry.ts +++ b/packages/workflow-template-develop/bundle-entry.ts @@ -5,34 +5,21 @@ */ import { createCursorAgent } from "@uncaged/workflow-agent-cursor"; import { createWorkflow } from "@uncaged/workflow-runtime"; +import { optionalEnv, requireEnv } from "@uncaged/workflow-util"; import { wrapAgentAsAdapter } from "@uncaged/workflow-util-agent"; import { buildDevelopDescriptor, developWorkflowDefinition } from "./src/index.js"; -function requireEnv(name: string): string { - const value = process.env[name]; - if (value === undefined || value === "") { - throw new Error(`missing required env var: ${name}`); - } - return value; -} - -function optionalEnv(name: string): string | null { - const value = process.env[name]; - if (value === undefined || value === "") { - return null; - } - return value; -} - const llmProvider = { - baseUrl: - optionalEnv("WORKFLOW_LLM_BASE_URL") ?? "https://dashscope.aliyuncs.com/compatible-mode/v1", - apiKey: requireEnv("WORKFLOW_LLM_API_KEY"), - model: optionalEnv("WORKFLOW_LLM_MODEL") ?? "qwen-plus", + baseUrl: optionalEnv( + "WORKFLOW_LLM_BASE_URL", + "https://dashscope.aliyuncs.com/compatible-mode/v1", + ), + apiKey: requireEnv("WORKFLOW_LLM_API_KEY", "set WORKFLOW_LLM_API_KEY for meta extraction"), + model: optionalEnv("WORKFLOW_LLM_MODEL", "qwen-plus"), }; const agent = createCursorAgent({ - command: requireEnv("WORKFLOW_CURSOR_COMMAND"), + command: requireEnv("WORKFLOW_CURSOR_COMMAND", "set WORKFLOW_CURSOR_COMMAND (e.g. cursor-agent)"), model: optionalEnv("WORKFLOW_CURSOR_MODEL"), timeout: optionalEnv("WORKFLOW_CURSOR_TIMEOUT") ? Number(optionalEnv("WORKFLOW_CURSOR_TIMEOUT")) diff --git a/packages/workflow-template-solve-issue/bundle-entry.ts b/packages/workflow-template-solve-issue/bundle-entry.ts index deb775d..8dabff0 100644 --- a/packages/workflow-template-solve-issue/bundle-entry.ts +++ b/packages/workflow-template-solve-issue/bundle-entry.ts @@ -7,17 +7,10 @@ import { createHermesAgent } from "@uncaged/workflow-agent-hermes"; import { workflowAsAgent } from "@uncaged/workflow-execute"; import { createWorkflow } from "@uncaged/workflow-runtime"; +import { optionalEnv } from "@uncaged/workflow-util"; import { wrapAgentAsAdapter } from "@uncaged/workflow-util-agent"; import { buildSolveIssueDescriptor, solveIssueWorkflowDefinition } from "./src/index.js"; -function optionalEnv(name: string): string | null { - const value = process.env[name]; - if (value === undefined || value === "") { - return null; - } - return value; -} - const hermesAgent = createHermesAgent({ model: optionalEnv("WORKFLOW_HERMES_MODEL"), timeout: optionalEnv("WORKFLOW_HERMES_TIMEOUT") diff --git a/packages/workflow-util-agent/package.json b/packages/workflow-util-agent/package.json index eca63cd..2b0d962 100644 --- a/packages/workflow-util-agent/package.json +++ b/packages/workflow-util-agent/package.json @@ -15,6 +15,7 @@ }, "dependencies": { "@uncaged/workflow-runtime": "workspace:*", - "@uncaged/workflow-cas": "workspace:*" + "@uncaged/workflow-cas": "workspace:*", + "zod": "^4.0.0" } } diff --git a/packages/workflow-util-agent/src/wrap-agent-as-adapter.ts b/packages/workflow-util-agent/src/wrap-agent-as-adapter.ts index 1af09cb..1830922 100644 --- a/packages/workflow-util-agent/src/wrap-agent-as-adapter.ts +++ b/packages/workflow-util-agent/src/wrap-agent-as-adapter.ts @@ -19,12 +19,18 @@ export function wrapAgentAsAdapter( ): AdapterFn { return (prompt: string, schema: z.ZodType) => { return async (ctx: ThreadContext, runtime: WorkflowRuntime): Promise> => { - const agentCtx: AgentContext = { ...ctx, currentRole: { name: "agent", systemPrompt: prompt } }; + const agentCtx: AgentContext = { + ...ctx, + currentRole: { name: "agent", systemPrompt: prompt }, + }; const result = await agentFn(agentCtx); const output = typeof result === "string" ? result : result.output; const childThread = typeof result === "string" ? null : result.childThread; const contentHash = await putContentNodeWithRefs(runtime.cas, output, []); - const extracted = await runtime.extract(schema as z.ZodType>, contentHash); + const extracted = await runtime.extract( + schema as z.ZodType>, + contentHash, + ); return { meta: extracted.meta as T, childThread }; }; }; diff --git a/packages/workflow-util/__tests__/env.test.ts b/packages/workflow-util/__tests__/env.test.ts new file mode 100644 index 0000000..0d72fb8 --- /dev/null +++ b/packages/workflow-util/__tests__/env.test.ts @@ -0,0 +1,44 @@ +import { describe, expect, test } from "bun:test"; +import { optionalEnv, requireEnv } from "../src/env.js"; + +describe("requireEnv", () => { + test("returns value when set", () => { + process.env.TEST_REQ = "hello"; + expect(requireEnv("TEST_REQ", "missing")).toBe("hello"); + delete process.env.TEST_REQ; + }); + + test("throws with message when missing", () => { + expect(() => requireEnv("TEST_MISSING_XYZ", "need this")).toThrow("need this"); + }); + + test("throws when empty string", () => { + process.env.TEST_EMPTY = ""; + expect(() => requireEnv("TEST_EMPTY", "cannot be empty")).toThrow("cannot be empty"); + delete process.env.TEST_EMPTY; + }); +}); + +describe("optionalEnv", () => { + test("returns value when set", () => { + process.env.TEST_OPT = "world"; + expect(optionalEnv("TEST_OPT")).toBe("world"); + expect(optionalEnv("TEST_OPT", "default")).toBe("world"); + delete process.env.TEST_OPT; + }); + + test("returns null when missing and no fallback", () => { + expect(optionalEnv("TEST_MISSING_ABC")).toBeNull(); + }); + + test("returns fallback when missing", () => { + expect(optionalEnv("TEST_MISSING_ABC", "fallback")).toBe("fallback"); + }); + + test("returns fallback when empty string", () => { + process.env.TEST_EMPTY2 = ""; + expect(optionalEnv("TEST_EMPTY2", "fb")).toBe("fb"); + expect(optionalEnv("TEST_EMPTY2")).toBeNull(); + delete process.env.TEST_EMPTY2; + }); +}); diff --git a/packages/workflow-util/src/env.ts b/packages/workflow-util/src/env.ts new file mode 100644 index 0000000..22ff527 --- /dev/null +++ b/packages/workflow-util/src/env.ts @@ -0,0 +1,23 @@ +/** + * Read a required environment variable. Throws with `message` if missing or empty. + */ +export function requireEnv(name: string, message: string): string { + const value = process.env[name]; + if (value === undefined || value === "") { + throw new Error(message); + } + return value; +} + +/** + * Read an optional environment variable. Returns `fallback` if missing or empty. + */ +export function optionalEnv(name: string, fallback: string): string; +export function optionalEnv(name: string): string | null; +export function optionalEnv(name: string, fallback?: string): string | null { + const value = process.env[name]; + if (value === undefined || value === "") { + return fallback ?? null; + } + return value; +} diff --git a/packages/workflow-util/src/index.ts b/packages/workflow-util/src/index.ts index 9d5e2f4..4fdb736 100644 --- a/packages/workflow-util/src/index.ts +++ b/packages/workflow-util/src/index.ts @@ -6,6 +6,7 @@ export { encodeCrockfordBase32Bits, encodeUint64AsCrockford, } from "./base32.js"; +export { optionalEnv, requireEnv } from "./env.js"; export { createLogger } from "./logger.js"; export { mergeRefsWithContentHash, normalizeRefsField } from "./refs-field.js"; export { getDefaultWorkflowStorageRoot, getGlobalCasDir } from "./storage-root.js";