refactor: rename workflow-role-llm → workflow-agent-llm

The package only contains createLlmAdapter (OpenAI chat → AgentFn),
which is an agent adapter, not a role. Aligns with workflow-agent-cursor
and workflow-agent-hermes naming.
This commit is contained in:
2026-05-06 10:14:35 +00:00
parent 2cd2a7d713
commit 513c006ce3
12 changed files with 8 additions and 8 deletions
@@ -0,0 +1,64 @@
import { describe, expect, test } from "bun:test";
import { START, type ThreadContext } from "@uncaged/workflow";
import { createLlmAdapter } from "../src/create-llm-adapter.js";
function makeCtx(userContent: string): ThreadContext {
return {
start: {
role: START,
content: userContent,
meta: { maxRounds: 10 },
timestamp: 1,
},
steps: [],
};
}
describe("createLlmAdapter", () => {
const originalFetch = globalThis.fetch;
test("posts system + user (start.content) and returns assistant text", async () => {
globalThis.fetch = (() =>
Promise.resolve(
new Response(JSON.stringify({ choices: [{ message: { content: "model reply" } }] }), {
status: 200,
headers: { "Content-Type": "application/json" },
}),
)) as unknown as typeof fetch;
const provider = { baseUrl: "https://api.example/v1", apiKey: "k", model: "m" };
const adapter = createLlmAdapter(provider);
const out = await adapter(makeCtx("trigger text"), "system instructions");
globalThis.fetch = originalFetch;
expect(out).toBe("model reply");
});
test("throws on non-ok fetch response", async () => {
globalThis.fetch = (() =>
Promise.resolve(
new Response("Internal Server Error", {
status: 500,
headers: { "Content-Type": "text/plain" },
}),
)) as unknown as typeof fetch;
const provider = { baseUrl: "https://api.example/v1", apiKey: "k", model: "m" };
const adapter = createLlmAdapter(provider);
await expect(adapter(makeCtx("hi"), "sys")).rejects.toThrow("llm:");
globalThis.fetch = originalFetch;
});
test("throws on fetch network failure", async () => {
globalThis.fetch = (() => Promise.reject(new Error("ECONNREFUSED"))) as unknown as typeof fetch;
const provider = { baseUrl: "https://api.example/v1", apiKey: "k", model: "m" };
const adapter = createLlmAdapter(provider);
await expect(adapter(makeCtx("hi"), "sys")).rejects.toThrow();
globalThis.fetch = originalFetch;
});
});
+19
View File
@@ -0,0 +1,19 @@
{
"name": "@uncaged/workflow-agent-llm",
"version": "0.1.0",
"type": "module",
"main": "src/index.ts",
"types": "src/index.ts",
"exports": {
".": "./src/index.ts"
},
"scripts": {
"build": "echo 'TODO'",
"test": "bun test"
},
"dependencies": {
"@uncaged/workflow": "workspace:*",
"@uncaged/workflow-util-role": "workspace:*",
"zod": "^4.0.0"
}
}
@@ -0,0 +1,107 @@
import { type AgentFn, err, ok, type Result, type ThreadContext } from "@uncaged/workflow";
import type { LlmMessage, LlmProvider } from "@uncaged/workflow-util-role";
export type LlmChatError =
| { kind: "http_error"; status: number; body: string }
| { kind: "invalid_response_json"; message: string }
| { kind: "network_error"; message: string }
| { kind: "empty_choices" }
| { kind: "no_assistant_text" };
function chatUrl(baseUrl: string): string {
const trimmed = baseUrl.replace(/\/+$/, "");
return `${trimmed}/chat/completions`;
}
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null && !Array.isArray(value);
}
function formatLlmChatError(e: LlmChatError): string {
return JSON.stringify(e);
}
async function fetchChatJson(
provider: LlmProvider,
body: Record<string, unknown>,
): Promise<Result<unknown, LlmChatError>> {
let response: Response;
try {
response = await fetch(chatUrl(provider.baseUrl), {
method: "POST",
headers: {
Authorization: `Bearer ${provider.apiKey}`,
"Content-Type": "application/json",
},
body: JSON.stringify(body),
});
} catch (cause) {
const message = cause instanceof Error ? cause.message : String(cause);
return err({ kind: "network_error", message });
}
const responseText = await response.text();
if (!response.ok) {
return err({ kind: "http_error", status: response.status, body: responseText.slice(0, 4000) });
}
let parsed: unknown;
try {
parsed = JSON.parse(responseText) as unknown;
} catch (cause) {
const message = cause instanceof Error ? cause.message : String(cause);
return err({ kind: "invalid_response_json", message });
}
return ok(parsed);
}
function parseAssistantText(parsed: unknown): Result<string, LlmChatError> {
if (!isRecord(parsed)) {
return err({ kind: "invalid_response_json", message: "Not an object" });
}
const choices = parsed.choices;
if (!Array.isArray(choices) || choices.length === 0) {
return err({ kind: "empty_choices" });
}
const c0 = choices[0];
if (!isRecord(c0)) {
return err({ kind: "empty_choices" });
}
const messageObj = c0.message;
if (!isRecord(messageObj)) {
return err({ kind: "no_assistant_text" });
}
const content = messageObj.content;
if (typeof content === "string") {
return ok(content);
}
return err({ kind: "no_assistant_text" });
}
export async function chatCompletionText(options: {
provider: LlmProvider;
messages: LlmMessage[];
}): Promise<Result<string, LlmChatError>> {
const body = { model: options.provider.model, messages: options.messages };
const res = await fetchChatJson(options.provider, body);
if (!res.ok) {
return res;
}
return parseAssistantText(res.value);
}
/** Single-turn chat adapter: system comes from `createRole` prompt; user is the thread start frame. */
export function createLlmAdapter(provider: LlmProvider): AgentFn {
return async (ctx: ThreadContext, systemPrompt: string) => {
const result = await chatCompletionText({
provider,
messages: [
{ role: "system", content: systemPrompt },
{ role: "user", content: ctx.start.content },
],
});
if (!result.ok) {
throw new Error(`llm: ${formatLlmChatError(result.error)}`);
}
return result.value;
};
}
+22
View File
@@ -0,0 +1,22 @@
export {
buildDescriptorFromRoles,
type CreateRoleArgs,
createRole,
decorateRole,
extractMetaOrThrow,
type LlmError,
type LlmExtractArgs,
type LlmMessage,
type LlmProvider,
llmErrorToCause,
llmExtract,
llmExtractWithRetry,
type MetaExtractConfig,
type OnFailOptions,
onFail,
type RoleDecorator,
type RoleDescriptorInput,
type WithDryRunOptions,
withDryRun,
} from "@uncaged/workflow-util-role";
export { chatCompletionText, createLlmAdapter, type LlmChatError } from "./create-llm-adapter.js";
+10
View File
@@ -0,0 +1,10 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"rootDir": "src",
"outDir": "dist",
"composite": true
},
"include": ["src/**/*.ts"],
"references": [{ "path": "../workflow" }, { "path": "../workflow-util-role" }]
}