diff --git a/workflows/knowledge-extraction/index.ts b/workflows/extract-knowledge/index.ts similarity index 100% rename from workflows/knowledge-extraction/index.ts rename to workflows/extract-knowledge/index.ts diff --git a/workflows/knowledge-extraction/lib/knowledge-queue.ts b/workflows/extract-knowledge/lib/knowledge-queue.ts similarity index 100% rename from workflows/knowledge-extraction/lib/knowledge-queue.ts rename to workflows/extract-knowledge/lib/knowledge-queue.ts diff --git a/workflows/knowledge-extraction/lib/workdir.ts b/workflows/extract-knowledge/lib/workdir.ts similarity index 100% rename from workflows/knowledge-extraction/lib/workdir.ts rename to workflows/extract-knowledge/lib/workdir.ts diff --git a/workflows/knowledge-extraction/moderator.ts b/workflows/extract-knowledge/moderator.ts similarity index 100% rename from workflows/knowledge-extraction/moderator.ts rename to workflows/extract-knowledge/moderator.ts diff --git a/workflows/knowledge-extraction/roles/answerer.ts b/workflows/extract-knowledge/roles/answerer.ts similarity index 100% rename from workflows/knowledge-extraction/roles/answerer.ts rename to workflows/extract-knowledge/roles/answerer.ts diff --git a/workflows/knowledge-extraction/build.ts b/workflows/knowledge-extraction/build.ts deleted file mode 100644 index 05dd5b6..0000000 --- a/workflows/knowledge-extraction/build.ts +++ /dev/null @@ -1,33 +0,0 @@ -import type { AgentFn, WorkflowDefinition } from "@uncaged/nerve-core"; -import type { LlmExtractorConfig } from "@uncaged/nerve-workflow-utils"; -import { createLlmAdapter } from "@uncaged/nerve-workflow-utils"; - -import { moderator } from "./moderator.js"; -import type { WorkflowMeta } from "./moderator.js"; -import { createAnswererRole } from "./roles/answerer.js"; -import { createExplorerRole } from "./roles/explorer.js"; -import { createQuestionerRole } from "./roles/questioner.js"; - -export type CreateKnowledgeExtractionDeps = { - defaultAdapter: AgentFn; - adapters?: Partial>; - extract: LlmExtractorConfig; -}; - -export function createKnowledgeExtractionWorkflow({ - defaultAdapter, - adapters, - extract, -}: CreateKnowledgeExtractionDeps): WorkflowDefinition { - const a = (role: keyof WorkflowMeta) => adapters?.[role] ?? defaultAdapter; - const llmAdapter = createLlmAdapter(extract.provider); - return { - name: "knowledge-extraction", - roles: { - questioner: createQuestionerRole(adapters?.questioner ?? llmAdapter, { extract }), - answerer: createAnswererRole(adapters?.answerer ?? llmAdapter, { extract }), - explorer: createExplorerRole(a("explorer"), { extract }), - }, - moderator, - }; -} diff --git a/workflows/knowledge-extraction/roles/explorer.ts b/workflows/knowledge-extraction/roles/explorer.ts deleted file mode 100644 index 31700d3..0000000 --- a/workflows/knowledge-extraction/roles/explorer.ts +++ /dev/null @@ -1,93 +0,0 @@ -import type { AgentFn, Role, ThreadContext, WorkflowMessage } from "@uncaged/nerve-core"; -import type { LlmExtractorConfig } from "@uncaged/nerve-workflow-utils"; -import { createRole } from "@uncaged/nerve-workflow-utils"; -import { z } from "zod"; - -import { resolveWorkdir } from "../lib/workdir.js"; - -import type { AnswererMeta } from "./answerer.js"; -import type { QuestionerMeta } from "./questioner.js"; - -export const explorerMetaSchema = z.object({ - patches: z.array( - z.object({ - card: z.string(), - section: z.string(), - }), - ), - new_cards: z.array(z.string()), -}); - -export type ExplorerMeta = z.infer; - -export type CreateExplorerRoleDeps = { - extract: LlmExtractorConfig; -}; - -function lastMeta(messages: WorkflowMessage[], role: string): M | undefined { - for (let i = messages.length - 1; i >= 0; i--) { - if (messages[i].role === role) { - return messages[i].meta as M; - } - } - return undefined; -} - -export function explorerPrompt(ctx: ThreadContext): string { - const messages = ctx.steps as unknown as WorkflowMessage[]; - const threadId = ctx.start.meta.threadId; - const qm = lastMeta(messages, "questioner"); - const am = lastMeta(messages, "answerer"); - const cwd = resolveWorkdir(ctx.start); - - const unanswered = - am?.results.filter((r) => !r.found).map((r) => r.id) ?? []; - - return `You are the **explorer** in a knowledge-extraction workflow. - -## Context - -- Thread: \`nerve thread ${threadId}\` -- Working directory (repo root for paths): ${cwd} -- Current knowledge card (questioner): ${qm?.card ?? "(unknown)"} - -## Unanswered question ids - -${JSON.stringify(unanswered)} - -Use the prior answerer results in the thread to map ids to full question text when you read messages above. - -## Task - -For each unanswered question, **read the codebase** as needed, then either: - -- Add a new markdown file under \`.knowledge/\`, or -- Patch an existing card (prefer updating the card listed above when appropriate). - -After any write or patch to \`.knowledge\`, run: - -\`\`\`bash -nerve knowledge sync -\`\`\` - -from this repo root (${cwd}), and fix failures until sync succeeds. - -## Output meta - -Report \`patches\` as { card, section } entries for cards you edited (section is a short heading or path hint). -Report \`new_cards\` as repo-relative paths for brand-new files you created (e.g. \`.knowledge/new-topic.md\`). - -Do not claim work you did not perform.`; -} - -export function createExplorerRole( - adapter: AgentFn, - { extract }: CreateExplorerRoleDeps, -): Role { - return createRole( - adapter, - async (ctx: ThreadContext) => explorerPrompt(ctx), - explorerMetaSchema, - extract, - ); -} diff --git a/workflows/knowledge-extraction/roles/questioner.ts b/workflows/knowledge-extraction/roles/questioner.ts deleted file mode 100644 index 9fda7b9..0000000 --- a/workflows/knowledge-extraction/roles/questioner.ts +++ /dev/null @@ -1,108 +0,0 @@ -import { readFile } from "node:fs/promises"; -import { join } from "node:path"; - -import type { AgentFn, Role, ThreadContext, WorkflowMessage } from "@uncaged/nerve-core"; -import type { LlmExtractorConfig } from "@uncaged/nerve-workflow-utils"; -import { createRole } from "@uncaged/nerve-workflow-utils"; -import { z } from "zod"; - -import { resolveQueueForQuestioner } from "../lib/knowledge-queue.js"; -import { resolveWorkdir } from "../lib/workdir.js"; - -const questionerExtractSchema = z.object({ - questions: z - .array( - z.object({ - id: z.string(), - question: z.string(), - domain: z.string(), - }), - ) - .length(5), -}); - -export type QuestionerMeta = { - /** Empty when no .knowledge cards and no work to do. */ - card: string; - questions: { id: string; question: string; domain: string }[]; - remaining_queue: string[]; -}; - -export type CreateQuestionerRoleDeps = { - extract: LlmExtractorConfig; -}; - -function questionerSystem(): string { - return `You are the **questioner** in a knowledge-extraction workflow. - -Read the given markdown knowledge card. Propose exactly **five** technical questions that are **not** already answered or covered by that card. - -Rules: -- Questions must be concrete and technical. -- Each question needs a stable string id (e.g. q1, q2, q3, q4, q5), a short domain label (e.g. routing, storage), and the question text. -- Do not assume access to other files or tools — reason only from the card content shown.`; -} - -function questionerUser(card: string, cardBody: string, remainingHint: string[]): string { - return `Current card path: ${card} - -Remaining queue after this card (paths, may be empty): ${JSON.stringify(remainingHint)} - ---- Card content --- - -${cardBody}`; -} - -export async function questionerPrompt(ctx: ThreadContext): Promise { - const messages = ctx.steps as unknown as WorkflowMessage[]; - const cwd = resolveWorkdir(ctx.start); - const queue = await resolveQueueForQuestioner(ctx.start, messages, cwd); - if (queue.length === 0) { - throw new Error( - "questioner: prompt invoked with empty queue — wrapped role should short-circuit before LLM", - ); - } - const card = queue[0]!; - const remaining_queue = queue.slice(1); - let cardBody: string; - try { - cardBody = await readFile(join(cwd, card), "utf8"); - } catch (e) { - const msg = e instanceof Error ? e.message : String(e); - throw new Error(`questioner: failed to read ${card}: ${msg}`); - } - return `${questionerSystem()}\n\n${questionerUser(card, cardBody, remaining_queue)}`; -} - -export function createQuestionerRole(adapter: AgentFn, { extract }: CreateQuestionerRoleDeps): Role { - const inner = createRole(adapter, questionerPrompt, questionerExtractSchema, extract); - - return async (ctx: ThreadContext) => { - const messages = ctx.steps as unknown as WorkflowMessage[]; - const cwd = resolveWorkdir(ctx.start); - const queue = await resolveQueueForQuestioner(ctx.start, messages, cwd); - if (queue.length === 0) { - return { - content: - "questioner: no `.knowledge` markdown files found and no seed path in the trigger prompt; queue is empty.", - meta: { - card: "", - questions: [], - remaining_queue: [], - }, - }; - } - - const card = queue[0]!; - const remaining_queue = queue.slice(1); - const r = await inner(ctx); - return { - content: r.content, - meta: { - card, - questions: r.meta.questions, - remaining_queue, - }, - }; - }; -}