Three-role workflow (questioner → answerer → explorer) that iterates over .knowledge/ cards to discover and fill knowledge gaps via BFS. - questioner: createLlmRole, reads card, asks 3 technical questions - answerer: spawnSafe nerve knowledge query, judges answers - explorer: reads code, writes/patches .knowledge cards, runs sync - moderator: BFS queue from message history, stagnation rule Closes #266
93 lines
2.6 KiB
TypeScript
93 lines
2.6 KiB
TypeScript
import type { AgentFn, Role, StartStep, 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<typeof explorerMetaSchema>;
|
|
|
|
export type CreateExplorerRoleDeps = {
|
|
extract: LlmExtractorConfig;
|
|
};
|
|
|
|
function lastMeta<M>(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(start: StartStep, messages: WorkflowMessage[]): string {
|
|
const threadId = start.meta.threadId;
|
|
const qm = lastMeta<QuestionerMeta>(messages, "questioner");
|
|
const am = lastMeta<AnswererMeta>(messages, "answerer");
|
|
const cwd = resolveWorkdir(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<ExplorerMeta> {
|
|
return createRole(
|
|
adapter,
|
|
async (innerStart: StartStep, msgs: WorkflowMessage[]) => explorerPrompt(innerStart, msgs),
|
|
explorerMetaSchema,
|
|
extract,
|
|
);
|
|
}
|