@capagents/forgeqe 1.0.4 → 1.0.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/refine.ts ADDED
@@ -0,0 +1,116 @@
1
+ import { realpathSync } from "node:fs"
2
+
3
+ import type { AppConfig } from "./config.ts"
4
+ import { openSession, type OpencodeSession } from "./code/runtime.ts"
5
+ import { privateEnvNames } from "./childEnv.ts"
6
+ import { MCP_NAME, TOOL_TIMEOUT_MS, localToolServer } from "./mcp/server.ts"
7
+ import { TOOLS, environmentNotes, type ToolContext } from "./mcp/tools.ts"
8
+ import { prompt } from "./prompts.ts"
9
+ import type { CodeEvent } from "./types.ts"
10
+
11
+ /**
12
+ * The shell's Refine (ctrl+r): what the user typed becomes a precise request for ForgeQE's agent, checked against what
13
+ * ForgeQE knows (its capabilities, the configured systems and project, the domain, product knowledge, recent runs)
14
+ * with read-only tools. It never runs anything, and the user decides what is sent.
15
+ */
16
+
17
+ /** The only tools the refiner may call: each reads, none changes anything or starts a run. */
18
+ export const REFINE_TOOLS = ["capabilities", "query_api", "find_test_cases", "list_sprints", "run_details"]
19
+
20
+ export type Refined = {
21
+ refined: string
22
+ grounded: { fact: string; checked: boolean }[]
23
+ assumed: string[]
24
+ missing: string[]
25
+ }
26
+
27
+ export const REFINE_SCHEMA = {
28
+ type: "object",
29
+ properties: {
30
+ refined: { type: "string", description: "The request, precise and complete, as the user would say it" },
31
+ grounded: {
32
+ type: "array",
33
+ items: { type: "object", properties: { fact: { type: "string" }, checked: { type: "boolean" } }, required: ["fact", "checked"] },
34
+ },
35
+ assumed: { type: "array", items: { type: "string" } },
36
+ missing: { type: "array", items: { type: "string" } },
37
+ },
38
+ required: ["refined", "grounded", "assumed", "missing"],
39
+ } as const
40
+
41
+ const REFINE_AGENT = {
42
+ name: "forgeqe-refine",
43
+ description: "Turns what the user typed into a precise request, checked against what ForgeQE knows",
44
+ get prompt() {
45
+ return prompt("refine")
46
+ },
47
+ permission: { edit: "deny", bash: "deny", webfetch: "deny" },
48
+ steps: 8,
49
+ }
50
+
51
+ export function refineRequest(text: string, recent: { user: string; reply: string }[]): string {
52
+ const turns = recent.slice(-4).flatMap((t) => [`User: ${t.user.slice(0, 600)}`, `ForgeQE: ${t.reply.slice(0, 600)}`])
53
+ return [turns.length ? `The conversation so far:\n${turns.join("\n")}\n` : "", `Refine this request:\n${text}`].filter(Boolean).join("\n")
54
+ }
55
+
56
+ /** Keeps only what the schema allows, so the shell never shows a malformed answer. */
57
+ export function cleanRefined(r: Partial<Refined> | null | undefined, original: string): Refined {
58
+ const strings = (v: unknown) => (Array.isArray(v) ? v.map(String).map((s) => s.trim()).filter(Boolean) : [])
59
+ return {
60
+ refined: String(r?.refined || "").trim() || original,
61
+ grounded: Array.isArray(r?.grounded) ? r!.grounded.filter((g) => g && String(g.fact || "").trim()).map((g) => ({ fact: String(g.fact).trim(), checked: g.checked === true })) : [],
62
+ assumed: strings(r?.assumed),
63
+ missing: strings(r?.missing),
64
+ }
65
+ }
66
+
67
+ export async function refine(opts: {
68
+ config: AppConfig
69
+ cwd: string
70
+ text: string
71
+ recent: { user: string; reply: string }[]
72
+ signal?: AbortSignal
73
+ onEvent?: (e: CodeEvent) => void
74
+ /** Replaces openSession in tests. */
75
+ open?: (o: Parameters<typeof openSession>[0]) => Promise<OpencodeSession<Refined>>
76
+ }): Promise<Refined> {
77
+ const cwd = (() => {
78
+ try {
79
+ return realpathSync(opts.cwd)
80
+ } catch {
81
+ return opts.cwd
82
+ }
83
+ })()
84
+ const context = (): ToolContext => ({ config: opts.config, cwd, session: "shell", writeTests: false })
85
+ const tools = localToolServer({ tools: TOOLS.filter((t) => REFINE_TOOLS.includes(t.name)), context, instructions: () => environmentNotes(context()), timeoutMs: TOOL_TIMEOUT_MS })
86
+ try {
87
+ const session = await (opts.open || openSession<Refined>)({
88
+ repo: cwd,
89
+ profile: opts.config.llm,
90
+ // The refiner works on the request, not the domain: ForgeQE's agent applies that when it handles it.
91
+ domain: null,
92
+ model: opts.config.code.model,
93
+ bin: opts.config.code.opencode,
94
+ readDirs: [cwd],
95
+ hide: privateEnvNames(opts.config),
96
+ role: "refiner",
97
+ signal: opts.signal,
98
+ onEvent: opts.onEvent,
99
+ agent: { ...REFINE_AGENT, prompt: `${REFINE_AGENT.prompt}\n\nThis session:\n${environmentNotes(context())}` },
100
+ schema: REFINE_SCHEMA as unknown as Record<string, unknown>,
101
+ mcp: { [MCP_NAME]: tools.entry },
102
+ hideTool: (tool) => tool.startsWith(`${MCP_NAME}_`),
103
+ title: "ForgeQE refine",
104
+ })
105
+ try {
106
+ let out = await session.prompt(refineRequest(opts.text, opts.recent))
107
+ if (!out.report && !out.error && !opts.signal?.aborted) out = await session.prompt("Give the structured result: refined, grounded, assumed, and missing.")
108
+ if (!out.report) throw new Error(out.error ? `Refine failed: ${out.error}` : "Refine gave no result.")
109
+ return cleanRefined(out.report, opts.text)
110
+ } finally {
111
+ await session.close()
112
+ }
113
+ } finally {
114
+ tools.stop()
115
+ }
116
+ }