feat(workflow-utils): add createLlmAdapter AgentFn factory

Single-turn chat via chatCompletionText: system from createRole prompt, user from ctx.start.content.

Fixes #277

Made-with: Cursor
This commit is contained in:
2026-04-30 12:38:00 +00:00
parent ba286a2f27
commit fbe1cc8eba
3 changed files with 77 additions and 0 deletions
@@ -0,0 +1,54 @@
import { afterEach, describe, expect, it, vi } from "vitest";
import { START, type ThreadContext } from "@uncaged/nerve-core";
import { createLlmAdapter } from "../create-llm-adapter.js";
function makeCtx(threadId: string, userContent: string): ThreadContext {
return {
threadId,
start: {
role: START,
content: userContent,
meta: { maxRounds: 10, threadId },
timestamp: 1,
},
steps: [],
};
}
describe("createLlmAdapter", () => {
afterEach(() => {
vi.unstubAllGlobals();
vi.restoreAllMocks();
});
it("posts system + user (start.content) and returns assistant text", async () => {
const fetchMock = vi.fn().mockResolvedValue({
ok: true,
status: 200,
text: async () =>
JSON.stringify({
choices: [{ message: { content: "model reply" } }],
}),
});
vi.stubGlobal("fetch", fetchMock);
const provider = { baseUrl: "https://api.example/v1", apiKey: "k", model: "m" };
const adapter = createLlmAdapter(provider);
const out = await adapter(makeCtx("t1", "trigger text"), "system instructions");
expect(out).toBe("model reply");
expect(fetchMock).toHaveBeenCalledTimes(1);
const [, init] = fetchMock.mock.calls[0] as [string, RequestInit];
const body = JSON.parse(init.body as string) as {
model: string;
messages: Array<{ role: string; content: string }>;
};
expect(body.model).toBe("m");
expect(body.messages).toEqual([
{ role: "system", content: "system instructions" },
{ role: "user", content: "trigger text" },
]);
});
});
@@ -0,0 +1,22 @@
import type { AgentFn, ThreadContext } from "@uncaged/nerve-core";
import { formatLlmError } from "./shared/format-error.js";
import { chatCompletionText } from "./shared/llm-chat.js";
import type { LlmProvider } from "./shared/llm-extract.js";
/** 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: ${formatLlmError(result.error)}`);
}
return result.value;
};
}
+1
View File
@@ -1,4 +1,5 @@
// Primary API — role factory templates
export { createLlmAdapter } from "./create-llm-adapter.js";
export { createRole, type LlmExtractorConfig } from "./create-role.js";
export { createCursorRole } from "./role-cursor.js";
export { createHermesRole } from "./role-hermes.js";