feat(util): extract requireEnv/optionalEnv to workflow-util
- requireEnv(name, message) — throws with custom error message
- optionalEnv(name, fallback?) — returns fallback or null
- Update develop and solve-issue bundle entries to use shared helpers
- Remove inline requireEnv/optionalEnv and wrapAgentAsAdapter usage
- Add tests for both functions
小橘 🍊
This commit is contained in:
@@ -5,34 +5,21 @@
|
|||||||
*/
|
*/
|
||||||
import { createCursorAgent } from "@uncaged/workflow-agent-cursor";
|
import { createCursorAgent } from "@uncaged/workflow-agent-cursor";
|
||||||
import { createWorkflow } from "@uncaged/workflow-runtime";
|
import { createWorkflow } from "@uncaged/workflow-runtime";
|
||||||
|
import { optionalEnv, requireEnv } from "@uncaged/workflow-util";
|
||||||
import { wrapAgentAsAdapter } from "@uncaged/workflow-util-agent";
|
import { wrapAgentAsAdapter } from "@uncaged/workflow-util-agent";
|
||||||
import { buildDevelopDescriptor, developWorkflowDefinition } from "./src/index.js";
|
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 = {
|
const llmProvider = {
|
||||||
baseUrl:
|
baseUrl: optionalEnv(
|
||||||
optionalEnv("WORKFLOW_LLM_BASE_URL") ?? "https://dashscope.aliyuncs.com/compatible-mode/v1",
|
"WORKFLOW_LLM_BASE_URL",
|
||||||
apiKey: requireEnv("WORKFLOW_LLM_API_KEY"),
|
"https://dashscope.aliyuncs.com/compatible-mode/v1",
|
||||||
model: optionalEnv("WORKFLOW_LLM_MODEL") ?? "qwen-plus",
|
),
|
||||||
|
apiKey: requireEnv("WORKFLOW_LLM_API_KEY", "set WORKFLOW_LLM_API_KEY for meta extraction"),
|
||||||
|
model: optionalEnv("WORKFLOW_LLM_MODEL", "qwen-plus"),
|
||||||
};
|
};
|
||||||
|
|
||||||
const agent = createCursorAgent({
|
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"),
|
model: optionalEnv("WORKFLOW_CURSOR_MODEL"),
|
||||||
timeout: optionalEnv("WORKFLOW_CURSOR_TIMEOUT")
|
timeout: optionalEnv("WORKFLOW_CURSOR_TIMEOUT")
|
||||||
? Number(optionalEnv("WORKFLOW_CURSOR_TIMEOUT"))
|
? Number(optionalEnv("WORKFLOW_CURSOR_TIMEOUT"))
|
||||||
|
|||||||
@@ -7,17 +7,10 @@
|
|||||||
import { createHermesAgent } from "@uncaged/workflow-agent-hermes";
|
import { createHermesAgent } from "@uncaged/workflow-agent-hermes";
|
||||||
import { workflowAsAgent } from "@uncaged/workflow-execute";
|
import { workflowAsAgent } from "@uncaged/workflow-execute";
|
||||||
import { createWorkflow } from "@uncaged/workflow-runtime";
|
import { createWorkflow } from "@uncaged/workflow-runtime";
|
||||||
|
import { optionalEnv } from "@uncaged/workflow-util";
|
||||||
import { wrapAgentAsAdapter } from "@uncaged/workflow-util-agent";
|
import { wrapAgentAsAdapter } from "@uncaged/workflow-util-agent";
|
||||||
import { buildSolveIssueDescriptor, solveIssueWorkflowDefinition } from "./src/index.js";
|
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({
|
const hermesAgent = createHermesAgent({
|
||||||
model: optionalEnv("WORKFLOW_HERMES_MODEL"),
|
model: optionalEnv("WORKFLOW_HERMES_MODEL"),
|
||||||
timeout: optionalEnv("WORKFLOW_HERMES_TIMEOUT")
|
timeout: optionalEnv("WORKFLOW_HERMES_TIMEOUT")
|
||||||
|
|||||||
@@ -15,6 +15,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@uncaged/workflow-runtime": "workspace:*",
|
"@uncaged/workflow-runtime": "workspace:*",
|
||||||
"@uncaged/workflow-cas": "workspace:*"
|
"@uncaged/workflow-cas": "workspace:*",
|
||||||
|
"zod": "^4.0.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,12 +19,18 @@ export function wrapAgentAsAdapter(
|
|||||||
): AdapterFn {
|
): AdapterFn {
|
||||||
return <T>(prompt: string, schema: z.ZodType<T>) => {
|
return <T>(prompt: string, schema: z.ZodType<T>) => {
|
||||||
return async (ctx: ThreadContext, runtime: WorkflowRuntime): Promise<RoleResult<T>> => {
|
return async (ctx: ThreadContext, runtime: WorkflowRuntime): Promise<RoleResult<T>> => {
|
||||||
const agentCtx: AgentContext = { ...ctx, currentRole: { name: "agent", systemPrompt: prompt } };
|
const agentCtx: AgentContext = {
|
||||||
|
...ctx,
|
||||||
|
currentRole: { name: "agent", systemPrompt: prompt },
|
||||||
|
};
|
||||||
const result = await agentFn(agentCtx);
|
const result = await agentFn(agentCtx);
|
||||||
const output = typeof result === "string" ? result : result.output;
|
const output = typeof result === "string" ? result : result.output;
|
||||||
const childThread = typeof result === "string" ? null : result.childThread;
|
const childThread = typeof result === "string" ? null : result.childThread;
|
||||||
const contentHash = await putContentNodeWithRefs(runtime.cas, output, []);
|
const contentHash = await putContentNodeWithRefs(runtime.cas, output, []);
|
||||||
const extracted = await runtime.extract(schema as z.ZodType<Record<string, unknown>>, contentHash);
|
const extracted = await runtime.extract(
|
||||||
|
schema as z.ZodType<Record<string, unknown>>,
|
||||||
|
contentHash,
|
||||||
|
);
|
||||||
return { meta: extracted.meta as T, childThread };
|
return { meta: extracted.meta as T, childThread };
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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;
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -6,6 +6,7 @@ export {
|
|||||||
encodeCrockfordBase32Bits,
|
encodeCrockfordBase32Bits,
|
||||||
encodeUint64AsCrockford,
|
encodeUint64AsCrockford,
|
||||||
} from "./base32.js";
|
} from "./base32.js";
|
||||||
|
export { optionalEnv, requireEnv } from "./env.js";
|
||||||
export { createLogger } from "./logger.js";
|
export { createLogger } from "./logger.js";
|
||||||
export { mergeRefsWithContentHash, normalizeRefsField } from "./refs-field.js";
|
export { mergeRefsWithContentHash, normalizeRefsField } from "./refs-field.js";
|
||||||
export { getDefaultWorkflowStorageRoot, getGlobalCasDir } from "./storage-root.js";
|
export { getDefaultWorkflowStorageRoot, getGlobalCasDir } from "./storage-root.js";
|
||||||
|
|||||||
Reference in New Issue
Block a user