@gr8ful/spf 0.1.0

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.
Files changed (153) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +321 -0
  3. package/assets/defaults/spf.config.yaml +141 -0
  4. package/assets/prompts/builder/system.md +13 -0
  5. package/assets/prompts/builder/user.md +34 -0
  6. package/assets/prompts/documenter/system.md +17 -0
  7. package/assets/prompts/documenter/user.md +48 -0
  8. package/assets/prompts/planner/system.md +21 -0
  9. package/assets/prompts/planner/user.md +45 -0
  10. package/assets/prompts/reviewer/system.md +16 -0
  11. package/assets/prompts/reviewer/user.md +44 -0
  12. package/assets/prompts/scout/system.md +20 -0
  13. package/assets/prompts/scout/user.md +34 -0
  14. package/assets/skill/SKILL.md +80 -0
  15. package/assets/skill/cookbooks/authoring_chains.md +193 -0
  16. package/assets/skill/cookbooks/how_to_prompt_for_the_eng.md +109 -0
  17. package/assets/skill/cookbooks/roster.md +197 -0
  18. package/assets/skill/cookbooks/run_adw.md +92 -0
  19. package/assets/skill/cookbooks/spf_overview.md +111 -0
  20. package/assets/skill/references/config.md +188 -0
  21. package/assets/skill/references/handoff.md +162 -0
  22. package/assets/skill/references/observability.md +184 -0
  23. package/dist/chains/adw_build.d.ts +12 -0
  24. package/dist/chains/adw_build.js +27 -0
  25. package/dist/chains/adw_build_review.d.ts +21 -0
  26. package/dist/chains/adw_build_review.js +55 -0
  27. package/dist/chains/adw_build_test.d.ts +21 -0
  28. package/dist/chains/adw_build_test.js +67 -0
  29. package/dist/chains/adw_document.d.ts +23 -0
  30. package/dist/chains/adw_document.js +59 -0
  31. package/dist/chains/adw_plan.d.ts +12 -0
  32. package/dist/chains/adw_plan.js +27 -0
  33. package/dist/chains/adw_plan_build.d.ts +12 -0
  34. package/dist/chains/adw_plan_build.js +30 -0
  35. package/dist/chains/adw_plan_build_test.d.ts +16 -0
  36. package/dist/chains/adw_plan_build_test.js +65 -0
  37. package/dist/chains/adw_plan_build_test_quality.d.ts +18 -0
  38. package/dist/chains/adw_plan_build_test_quality.js +66 -0
  39. package/dist/chains/adw_prompt.d.ts +12 -0
  40. package/dist/chains/adw_prompt.js +25 -0
  41. package/dist/chains/adw_quality.d.ts +12 -0
  42. package/dist/chains/adw_quality.js +32 -0
  43. package/dist/chains/adw_scout.d.ts +12 -0
  44. package/dist/chains/adw_scout.js +27 -0
  45. package/dist/chains/adw_simple_sdlc.d.ts +43 -0
  46. package/dist/chains/adw_simple_sdlc.js +147 -0
  47. package/dist/chains/context.d.ts +19 -0
  48. package/dist/chains/context.js +1 -0
  49. package/dist/chains/index.d.ts +19 -0
  50. package/dist/chains/index.js +116 -0
  51. package/dist/cli/bin.d.ts +15 -0
  52. package/dist/cli/bin.js +24 -0
  53. package/dist/cli/commands/abort.d.ts +1 -0
  54. package/dist/cli/commands/abort.js +42 -0
  55. package/dist/cli/commands/doctor.d.ts +1 -0
  56. package/dist/cli/commands/doctor.js +165 -0
  57. package/dist/cli/commands/eject.d.ts +1 -0
  58. package/dist/cli/commands/eject.js +55 -0
  59. package/dist/cli/commands/events.d.ts +1 -0
  60. package/dist/cli/commands/events.js +49 -0
  61. package/dist/cli/commands/init.d.ts +1 -0
  62. package/dist/cli/commands/init.js +62 -0
  63. package/dist/cli/commands/install-skill.d.ts +1 -0
  64. package/dist/cli/commands/install-skill.js +122 -0
  65. package/dist/cli/commands/list.d.ts +1 -0
  66. package/dist/cli/commands/list.js +13 -0
  67. package/dist/cli/commands/migrate.d.ts +1 -0
  68. package/dist/cli/commands/migrate.js +167 -0
  69. package/dist/cli/commands/phases.d.ts +1 -0
  70. package/dist/cli/commands/phases.js +25 -0
  71. package/dist/cli/commands/run.d.ts +3 -0
  72. package/dist/cli/commands/run.js +27 -0
  73. package/dist/cli/commands/sessions.d.ts +1 -0
  74. package/dist/cli/commands/sessions.js +20 -0
  75. package/dist/cli/commands/trace.d.ts +8 -0
  76. package/dist/cli/commands/trace.js +10 -0
  77. package/dist/cli/commands/ui.d.ts +1 -0
  78. package/dist/cli/commands/ui.js +35 -0
  79. package/dist/cli/commands/version.d.ts +1 -0
  80. package/dist/cli/commands/version.js +8 -0
  81. package/dist/cli/commands/watch.d.ts +8 -0
  82. package/dist/cli/commands/watch.js +210 -0
  83. package/dist/cli/gitignore.d.ts +1 -0
  84. package/dist/cli/gitignore.js +14 -0
  85. package/dist/cli/index.d.ts +1 -0
  86. package/dist/cli/index.js +156 -0
  87. package/dist/core/agent_cc.d.ts +75 -0
  88. package/dist/core/agent_cc.js +322 -0
  89. package/dist/core/agent_flue.d.ts +66 -0
  90. package/dist/core/agent_flue.js +321 -0
  91. package/dist/core/agents.d.ts +64 -0
  92. package/dist/core/agents.js +456 -0
  93. package/dist/core/changes.d.ts +35 -0
  94. package/dist/core/changes.js +98 -0
  95. package/dist/core/console.d.ts +36 -0
  96. package/dist/core/console.js +156 -0
  97. package/dist/core/data_types.d.ts +562 -0
  98. package/dist/core/data_types.js +382 -0
  99. package/dist/core/gates.d.ts +33 -0
  100. package/dist/core/gates.js +144 -0
  101. package/dist/core/git_helper.d.ts +59 -0
  102. package/dist/core/git_helper.js +115 -0
  103. package/dist/core/issues/github_provider.d.ts +52 -0
  104. package/dist/core/issues/github_provider.js +211 -0
  105. package/dist/core/issues/provider.d.ts +90 -0
  106. package/dist/core/issues/provider.js +14 -0
  107. package/dist/core/paths.d.ts +78 -0
  108. package/dist/core/paths.js +108 -0
  109. package/dist/core/permissions.d.ts +78 -0
  110. package/dist/core/permissions.js +187 -0
  111. package/dist/core/prompts.d.ts +4 -0
  112. package/dist/core/prompts.js +17 -0
  113. package/dist/core/quality.d.ts +65 -0
  114. package/dist/core/quality.js +194 -0
  115. package/dist/core/runner.d.ts +72 -0
  116. package/dist/core/runner.js +168 -0
  117. package/dist/core/session.d.ts +16 -0
  118. package/dist/core/session.js +60 -0
  119. package/dist/core/sqlite.d.ts +55 -0
  120. package/dist/core/sqlite.js +106 -0
  121. package/dist/core/tracer.d.ts +56 -0
  122. package/dist/core/tracer.js +246 -0
  123. package/dist/core/utils.d.ts +40 -0
  124. package/dist/core/utils.js +108 -0
  125. package/dist/core/watch.d.ts +44 -0
  126. package/dist/core/watch.js +194 -0
  127. package/dist/test/agent_cc.test.d.ts +1 -0
  128. package/dist/test/agent_cc.test.js +95 -0
  129. package/dist/test/agent_flue.test.d.ts +1 -0
  130. package/dist/test/agent_flue.test.js +83 -0
  131. package/dist/test/data_types.test.d.ts +10 -0
  132. package/dist/test/data_types.test.js +49 -0
  133. package/dist/test/ui_server.test.d.ts +1 -0
  134. package/dist/test/ui_server.test.js +119 -0
  135. package/dist/test/watch.test.d.ts +1 -0
  136. package/dist/test/watch.test.js +227 -0
  137. package/dist/ui/server/app.d.ts +3 -0
  138. package/dist/ui/server/app.js +98 -0
  139. package/dist/ui/server/db.d.ts +82 -0
  140. package/dist/ui/server/db.js +333 -0
  141. package/dist/ui/server/serve.d.ts +13 -0
  142. package/dist/ui/server/serve.js +80 -0
  143. package/dist/ui/server/static.d.ts +5 -0
  144. package/dist/ui/server/static.js +55 -0
  145. package/dist/ui/shared/types.d.ts +263 -0
  146. package/dist/ui/shared/types.js +8 -0
  147. package/package.json +52 -0
  148. package/web/assets/index-C7nF068F.css +1 -0
  149. package/web/assets/index-mzSArcnQ.js +11 -0
  150. package/web/assets/play-latin-400-normal-GKW-4YV7.woff2 +0 -0
  151. package/web/assets/play-latin-700-normal-DyPlLDbb.woff2 +0 -0
  152. package/web/index.html +14 -0
  153. package/web/logo.svg +6 -0
@@ -0,0 +1,322 @@
1
+ /**
2
+ * Claude Code coding agent interface — a second backend alongside
3
+ * `agent_flue.ts` (`coding_agent: claude_code` in config), for running
4
+ * agents on the `claude` CLI instead of Flue. Subprocess-based, on purpose:
5
+ * the official SDK's own platform-specific optional dependency is 300MB+
6
+ * per platform, on top of the `claude` CLI itself (which anyone using this
7
+ * backend needs installed anyway) — shelling out costs SPF zero new
8
+ * dependencies, the same trade the pre-Flue `agent_pi.ts` made for `pi`.
9
+ *
10
+ * Every flag below was verified against a REAL local run of this exact
11
+ * machine's `claude` CLI (v2.1.237) before being written — not assumed from
12
+ * the SDK's docs, which describe a related but separately-versioned
13
+ * product. Findings that shaped this file:
14
+ *
15
+ * - `--json-schema` is real, and structured output arrives as a BUILT-IN
16
+ * tool call named `StructuredOutput` (visible in the `system/init`
17
+ * message's `tools` list the moment `--json-schema` is passed) — no
18
+ * custom tool registration needed, unlike Flue's injected `sf_report`.
19
+ * The final `result` message's `structured_output` field carries the
20
+ * validated object directly.
21
+ * - `--strict-mcp-config` (with no `--mcp-config` given) is REQUIRED, not
22
+ * optional: without it, a spawned `claude` process inherits this
23
+ * machine's entire user-level MCP server configuration (GitHub, Slack,
24
+ * Google Drive, whatever the operator has configured globally) — a real
25
+ * capability leak into what's supposed to be a `tools:`-bounded headless
26
+ * agent. `--tools ""` alone does NOT strip these; confirmed by a live
27
+ * run showing `mcp__github__*`/`mcp__claude_ai_*` tools still listed in
28
+ * `system/init` without it, and gone with it.
29
+ * - `--resume <uuid>` genuinely continues context (confirmed: a follow-up
30
+ * call answered from a file read in the FIRST call, never re-reading it)
31
+ * — the same continuity Flue's `init(agent, {id})` gives, so the
32
+ * same-session-correction design carries over unchanged.
33
+ * - The result message's `total_cost_usd` has no per-component breakdown
34
+ * (unlike Flue/pi-ai's `cost.{input,output,cacheRead,cacheWrite}`) — it's
35
+ * folded into `UsageBreakdown` under `total_cost` only, honestly, rather
36
+ * than fabricating a split CC doesn't provide.
37
+ */
38
+ import { spawn } from "node:child_process";
39
+ import { createInterface } from "node:readline";
40
+ import { randomUUID } from "node:crypto";
41
+ import { toJsonSchema } from "@valibot/to-json-schema";
42
+ import { UsageBreakdown, makeAgentResult } from "./data_types.js";
43
+ import { nowIso, operatorEnv } from "./utils.js";
44
+ const RESULT_SNIPPET_CHARS = 20_000;
45
+ const ARG_VALUE_CHARS = 20_000;
46
+ const LABEL_CHARS = 80;
47
+ const PRIMARY_ARGS = ["command", "path", "file_path", "pattern", "query", "url"];
48
+ function clipText(text, limit) {
49
+ return text.length <= limit ? text : text.slice(0, limit).trimEnd() + "…";
50
+ }
51
+ function labelFor(tool, args) {
52
+ let value = "";
53
+ for (const key of PRIMARY_ARGS) {
54
+ if (typeof args[key] === "string" && args[key].trim()) {
55
+ value = args[key];
56
+ break;
57
+ }
58
+ }
59
+ if (!value) {
60
+ for (const v of Object.values(args)) {
61
+ if (typeof v === "string" && v.trim()) {
62
+ value = v;
63
+ break;
64
+ }
65
+ }
66
+ }
67
+ value = String(value).split(/\s+/).filter(Boolean).join(" ");
68
+ return value ? `${tool}: ${clipText(value, LABEL_CHARS)}` : tool;
69
+ }
70
+ /** `tool_result.content` is a string in every observed case; defensive for the array-of-blocks form Anthropic's spec also allows. */
71
+ function snippetOf(content) {
72
+ if (content == null)
73
+ return "";
74
+ if (typeof content === "string")
75
+ return content;
76
+ try {
77
+ return JSON.stringify(content);
78
+ }
79
+ catch {
80
+ return String(content);
81
+ }
82
+ }
83
+ /**
84
+ * Folds a `tool_use` content block (on an `assistant` message) + its
85
+ * matching `tool_result` block (on a LATER `user` message, linked by
86
+ * `tool_use_id`) into one record — the same shape
87
+ * `agent_flue.ToolCallTracker` produces, so `agents.ts`'s `eventForwarder`
88
+ * needs no backend-specific branching downstream of picking which tracker
89
+ * to use. `StructuredOutput` calls fold through here too, deliberately
90
+ * unfiltered — Flue's `sf_report` tool shows up as an ordinary `tool_call`
91
+ * event for the same reason, and that's useful trace information, not noise.
92
+ */
93
+ export class CcToolCallTracker {
94
+ open = new Map();
95
+ observe(message) {
96
+ if (message.type === "assistant") {
97
+ for (const block of message.message?.content ?? []) {
98
+ if (block.type === "tool_use") {
99
+ this.open.set(block.id, {
100
+ tool: block.name,
101
+ args: block.input || {},
102
+ started_at: message.timestamp || nowIso(),
103
+ });
104
+ }
105
+ }
106
+ return null;
107
+ }
108
+ if (message.type === "user") {
109
+ for (const block of message.message?.content ?? []) {
110
+ if (block.type === "tool_result") {
111
+ return this.finish(block.tool_use_id, block.content, block.is_error !== true, message.timestamp);
112
+ }
113
+ }
114
+ }
115
+ return null;
116
+ }
117
+ finish(callId, content, ok, endedAt) {
118
+ const opened = this.open.get(callId);
119
+ this.open.delete(callId);
120
+ const tool = opened?.tool || "tool";
121
+ const args = opened?.args || {};
122
+ const record = {
123
+ tool,
124
+ tool_call_id: callId,
125
+ args: Object.fromEntries(Object.entries(args).map(([key, value]) => [key, typeof value === "string" ? clipText(value, ARG_VALUE_CHARS) : value])),
126
+ ok,
127
+ label: labelFor(tool, args),
128
+ };
129
+ const snippet = snippetOf(content);
130
+ if (snippet)
131
+ record.result_snippet = clipText(snippet, RESULT_SNIPPET_CHARS);
132
+ record.ended_at = endedAt || nowIso();
133
+ if (opened?.started_at) {
134
+ record.started_at = opened.started_at;
135
+ const startMs = Date.parse(opened.started_at);
136
+ const endMs = Date.parse(record.ended_at);
137
+ if (!Number.isNaN(startMs) && !Number.isNaN(endMs))
138
+ record.duration_ms = Math.max(0, endMs - startMs);
139
+ }
140
+ return record;
141
+ }
142
+ }
143
+ // ── tool-name resolution ─────────────────────────────────────────────────────
144
+ // SPF's canonical lowercase vocabulary -> Claude Code's own tool names.
145
+ // "ls" has no confirmed CC built-in (bash/glob cover it) — treated the same
146
+ // way agent_flue.ts treats it: a known name that resolves to nothing, not an
147
+ // unknown one, so a roster entry mentioning it fails nothing at validate time.
148
+ const TOOL_NAME_MAP = {
149
+ read: "Read",
150
+ write: "Write",
151
+ edit: "Edit",
152
+ bash: "Bash",
153
+ grep: "Grep",
154
+ glob: "Glob",
155
+ };
156
+ const TOOL_ALIASES = { find: "glob" };
157
+ const DROPPED_TOOLS = new Set(["ls"]);
158
+ export function isKnownToolName(name) {
159
+ return DROPPED_TOOLS.has(name) || (TOOL_ALIASES[name] ?? name) in TOOL_NAME_MAP;
160
+ }
161
+ /** `null`/`undefined` = every built-in tool (CC's own `--tools default`). `[]` = none (`--tools ""`). */
162
+ function toolsFlagValue(toolNames) {
163
+ if (!toolNames)
164
+ return "default";
165
+ const resolved = toolNames.filter((n) => !DROPPED_TOOLS.has(n)).map((n) => TOOL_NAME_MAP[TOOL_ALIASES[n] ?? n]).filter(Boolean);
166
+ return resolved.join(",");
167
+ }
168
+ // SPF's off|minimal|low|medium|high|xhigh|max -> CC's --effort low|medium|high|xhigh|max.
169
+ // CC has no "disabled reasoning" level for a headless run; off/minimal both
170
+ // round down to CC's floor rather than omitting --effort (which would fall
171
+ // through to CC's own default, "high" — the opposite of what "off" asked for).
172
+ const EFFORT_MAP = {
173
+ off: "low",
174
+ minimal: "low",
175
+ low: "low",
176
+ medium: "medium",
177
+ high: "high",
178
+ xhigh: "xhigh",
179
+ max: "max",
180
+ };
181
+ // ── process lifecycle ────────────────────────────────────────────────────────
182
+ const inFlight = new Set();
183
+ /** Kill any still-running `claude` children — call once, at process exit. Safe if none are running. */
184
+ export async function shutdown() {
185
+ for (const child of inFlight)
186
+ child.kill("SIGTERM");
187
+ inFlight.clear();
188
+ }
189
+ class CcRunError extends Error {
190
+ }
191
+ /**
192
+ * Run one `claude -p` turn against `request.session_id` (a UUID minted by
193
+ * `agents.ts`'s `agentSessionId()` for a fresh session, or carried over from
194
+ * `agent_map.json` for a rejoin) — `--session-id` on first contact,
195
+ * `--resume` when `request.resume` says this session already exists. CC has
196
+ * no single create-or-continue flag the way Flue's `init(id)` is, which is
197
+ * exactly why `AgentRequest.resume` exists — see its doc comment in
198
+ * `data_types.ts`.
199
+ *
200
+ * `onEvent` receives each parsed stream-json line UNFOLDED, exactly as
201
+ * `agent_flue.run()` forwards raw `ConversationStreamChunk`s — folding into
202
+ * one record per tool call is `eventForwarder`'s job (`agents.ts`), via
203
+ * this module's `CcToolCallTracker`. Folding here too would fold twice.
204
+ */
205
+ export async function run(request, onEvent, onSpawn, onExit) {
206
+ const args = [
207
+ "-p",
208
+ request.prompt,
209
+ "--output-format",
210
+ "stream-json",
211
+ "--verbose",
212
+ "--json-schema",
213
+ // `request.output_schema` is the raw Valibot schema object (which has its
214
+ // own internal `kind`/`type`/`entries` fields) — CC's `--json-schema`
215
+ // wants a real JSON Schema document, not that. Caught by a live run:
216
+ // stringifying the Valibot object directly produced "unknown keyword:
217
+ // kind" from CC's strict-mode validator.
218
+ JSON.stringify(toJsonSchema(request.output_schema)),
219
+ "--system-prompt",
220
+ request.system_prompt,
221
+ "--model",
222
+ request.model,
223
+ "--effort",
224
+ EFFORT_MAP[request.thinking],
225
+ request.resume ? "--resume" : "--session-id",
226
+ request.session_id,
227
+ "--permission-mode",
228
+ "bypassPermissions",
229
+ "--dangerously-skip-permissions",
230
+ "--tools",
231
+ toolsFlagValue(request.tools),
232
+ "--strict-mcp-config", // see the module doc comment — required, not optional
233
+ ];
234
+ const child = spawn("claude", args, { cwd: request.cwd, env: operatorEnv() });
235
+ // The prompt travels as a positional argv element, not stdin — closing it
236
+ // immediately avoids a real, observed ~3s "no stdin data received" stall
237
+ // where `claude` otherwise waits to see whether anything is piped in.
238
+ child.stdin.end();
239
+ inFlight.add(child);
240
+ const pid = child.pid ?? -1;
241
+ onSpawn?.(pid);
242
+ const lines = createInterface({ input: child.stdout });
243
+ let result = null;
244
+ let lastTurnUsage;
245
+ let stderr = "";
246
+ child.stderr.on("data", (chunk) => {
247
+ stderr += String(chunk);
248
+ });
249
+ lines.on("line", (line) => {
250
+ if (!line.trim())
251
+ return;
252
+ let message;
253
+ try {
254
+ message = JSON.parse(line);
255
+ }
256
+ catch {
257
+ return; // a non-JSON stray line (shouldn't happen under stream-json) — not fatal
258
+ }
259
+ onEvent?.(message);
260
+ if (message.type === "assistant" && message.message?.usage)
261
+ lastTurnUsage = message.message.usage;
262
+ if (message.type === "result")
263
+ result = message;
264
+ });
265
+ const exitCode = await new Promise((resolve, reject) => {
266
+ child.on("error", reject); // spawn failure (e.g. claude not on PATH)
267
+ child.on("close", (code) => resolve(code ?? 1));
268
+ });
269
+ inFlight.delete(child);
270
+ onExit?.(pid);
271
+ if (exitCode !== 0 && !result) {
272
+ throw new CcRunError(`claude exited ${exitCode} before producing a result: ${stderr.slice(-2000) || "(no stderr)"}`);
273
+ }
274
+ if (!result) {
275
+ throw new CcRunError(`claude produced no result message (exit ${exitCode}): ${stderr.slice(-2000) || "(no stderr)"}`);
276
+ }
277
+ // A plain `const` handoff — `result` above is a closure-reassigned `let`,
278
+ // which TS's control-flow narrowing doesn't track across the `readline`
279
+ // callback boundary, so the null-check above alone doesn't narrow it.
280
+ const final = result;
281
+ if (final.is_error) {
282
+ const detail = final.result ?? (stderr.slice(-2000) || "no detail");
283
+ throw new CcRunError(`claude reported an error (${final.subtype ?? "unknown"}): ${detail}`);
284
+ }
285
+ const usage = new UsageBreakdown();
286
+ const u = final.usage ?? {};
287
+ const cacheRead = u.cache_read_input_tokens ?? 0;
288
+ const cacheWrite = u.cache_creation_input_tokens ?? 0;
289
+ const totalTokens = (u.input_tokens ?? 0) + (u.output_tokens ?? 0) + cacheRead + cacheWrite;
290
+ usage.add_turn({
291
+ input: u.input_tokens ?? 0,
292
+ output: u.output_tokens ?? 0,
293
+ cacheRead,
294
+ cacheWrite,
295
+ reasoning: u.output_tokens_details?.thinking_tokens ?? 0,
296
+ // CC gives one total cost, not a per-component breakdown like Flue/pi-ai
297
+ // do — folded honestly under `total`, not split up to fabricate one.
298
+ cost: { total: final.total_cost_usd ?? 0 },
299
+ }, totalTokens);
300
+ const modelUsages = final.modelUsage ?? {};
301
+ const contextWindow = Object.values(modelUsages)[0]?.contextWindow ?? 0;
302
+ const contextTokens = lastTurnUsage
303
+ ? (lastTurnUsage.input_tokens ?? 0) +
304
+ (lastTurnUsage.output_tokens ?? 0) +
305
+ (lastTurnUsage.cache_read_input_tokens ?? 0) +
306
+ (lastTurnUsage.cache_creation_input_tokens ?? 0)
307
+ : 0;
308
+ return makeAgentResult({
309
+ session_id: final.session_id ?? request.session_id,
310
+ text: typeof final.result === "string" ? final.result : "",
311
+ report: final.structured_output ?? null,
312
+ tokens: usage.total_tokens,
313
+ cost: usage.total_cost,
314
+ usage,
315
+ context_tokens: contextTokens,
316
+ context_window: contextWindow,
317
+ });
318
+ }
319
+ /** CC requires a real UUID for `--session-id`/`--resume` — `agents.ts`'s `agentSessionId()` calls this to mint a fresh one for a claude_code agent's first contact. */
320
+ export function newSessionId() {
321
+ return randomUUID();
322
+ }
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Flue coding agent interface — replaces agent_pi.ts.
3
+ *
4
+ * One generic Flue agent function (`SfAgent`), driven entirely by YAML: a
5
+ * module-level registry maps a Flue conversation id (== SPF's session id) to
6
+ * the model/thinking/sandbox/tools/instruction that call should use, set by
7
+ * `run()` just before dispatching. SPF's roster stays data; Flue never learns
8
+ * there are five different "kinds" of agent.
9
+ *
10
+ * Structured output has no Flue-native "constrain this agent's final
11
+ * result" mechanism (that exists only for a nested `harness.prompt()` call
12
+ * inside a tool) — so every call gets one injected tool, `sf_report`, whose
13
+ * `input` schema IS the envelope's Valibot schema. Flue validates the
14
+ * model's arguments against it before `run()` fires, and `run()` returns
15
+ * `{terminate: true}` — the same loop-ending contract Flue's own built-in
16
+ * `finish`/`give_up` tools use — so a report ends the turn structurally,
17
+ * not just by instruction. The captured data rides back via
18
+ * `useDataWriter`, which is what `AgentReply.data` surfaces to the caller.
19
+ * `agents.ts`'s existing JSON-extraction fallback stays as a safety net for
20
+ * a model that never calls the tool at all.
21
+ *
22
+ * `run()`'s signature deliberately mirrors the old agent_pi.ts `run()` so
23
+ * agents.ts's `send()` closure changes only its imports and field names.
24
+ */
25
+ import { type ConversationStreamChunk } from "@flue/runtime";
26
+ import type { AgentRequest, AgentResult } from "./data_types.ts";
27
+ /**
28
+ * Folds Flue's `tool-input` + `tool-output`/`tool-output-error` chunk pair
29
+ * into ONE normalized record per completed call — the same shape
30
+ * agent_pi.ts's tracker produced, so agents.ts's eventForwarder (and the
31
+ * tracer/visualizer downstream of it) need no changes. `tool-output` never
32
+ * carries the tool's name, which is exactly why folding is still required.
33
+ */
34
+ export declare class ToolCallTracker {
35
+ private open;
36
+ /** Returns the record for a finished tool call, else null. */
37
+ observe(chunk: ConversationStreamChunk): Record<string, any> | null;
38
+ private finish;
39
+ }
40
+ /** Used by agents.validate() so a typo'd tool name fails before anything spawns. */
41
+ export declare function isKnownToolName(name: string): boolean;
42
+ /**
43
+ * Flue exposes no public catalog of registered models (only pi's --list-models
44
+ * CLI did, and that command is gone with pi). This checks only the STATIC
45
+ * `provider/model-id` shape — the same shape SPF's config docs already ask
46
+ * for — not that the provider is reachable or the id exists. An actually
47
+ * wrong model now surfaces at the first real dispatch instead of at
48
+ * validate() time; that trade is made explicit here rather than pretending
49
+ * a catalog check still happens.
50
+ */
51
+ export declare function resolveModel(pattern: string): [string, string];
52
+ /** Graceful shutdown — call once, at process exit. Safe to call if never started. */
53
+ export declare function shutdown(): Promise<void>;
54
+ /**
55
+ * Run one Flue dispatch+read turn against the given conversation id,
56
+ * creating it on first contact and continuing it on every later call with
57
+ * the same `session_id` — the same continuity `pi --session-id` gave.
58
+ *
59
+ * `onSpawn`/`onExit` bracket this call the way they bracketed pi's child
60
+ * process, but there is no child process here: Flue runs in-process, so
61
+ * both fire with this process's own pid. A future `spf abort` can still find
62
+ * and stop the right OS process; distinguishing WHICH in-flight submission
63
+ * that pid is running is a `processes.submission_id` column left for later,
64
+ * not attempted here.
65
+ */
66
+ export declare function run(request: AgentRequest, onEvent?: (chunk: ConversationStreamChunk) => void, onSpawn?: (pid: number) => void, onExit?: (pid: number) => void): Promise<AgentResult>;