@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.
- package/LICENSE +21 -0
- package/README.md +321 -0
- package/assets/defaults/spf.config.yaml +141 -0
- package/assets/prompts/builder/system.md +13 -0
- package/assets/prompts/builder/user.md +34 -0
- package/assets/prompts/documenter/system.md +17 -0
- package/assets/prompts/documenter/user.md +48 -0
- package/assets/prompts/planner/system.md +21 -0
- package/assets/prompts/planner/user.md +45 -0
- package/assets/prompts/reviewer/system.md +16 -0
- package/assets/prompts/reviewer/user.md +44 -0
- package/assets/prompts/scout/system.md +20 -0
- package/assets/prompts/scout/user.md +34 -0
- package/assets/skill/SKILL.md +80 -0
- package/assets/skill/cookbooks/authoring_chains.md +193 -0
- package/assets/skill/cookbooks/how_to_prompt_for_the_eng.md +109 -0
- package/assets/skill/cookbooks/roster.md +197 -0
- package/assets/skill/cookbooks/run_adw.md +92 -0
- package/assets/skill/cookbooks/spf_overview.md +111 -0
- package/assets/skill/references/config.md +188 -0
- package/assets/skill/references/handoff.md +162 -0
- package/assets/skill/references/observability.md +184 -0
- package/dist/chains/adw_build.d.ts +12 -0
- package/dist/chains/adw_build.js +27 -0
- package/dist/chains/adw_build_review.d.ts +21 -0
- package/dist/chains/adw_build_review.js +55 -0
- package/dist/chains/adw_build_test.d.ts +21 -0
- package/dist/chains/adw_build_test.js +67 -0
- package/dist/chains/adw_document.d.ts +23 -0
- package/dist/chains/adw_document.js +59 -0
- package/dist/chains/adw_plan.d.ts +12 -0
- package/dist/chains/adw_plan.js +27 -0
- package/dist/chains/adw_plan_build.d.ts +12 -0
- package/dist/chains/adw_plan_build.js +30 -0
- package/dist/chains/adw_plan_build_test.d.ts +16 -0
- package/dist/chains/adw_plan_build_test.js +65 -0
- package/dist/chains/adw_plan_build_test_quality.d.ts +18 -0
- package/dist/chains/adw_plan_build_test_quality.js +66 -0
- package/dist/chains/adw_prompt.d.ts +12 -0
- package/dist/chains/adw_prompt.js +25 -0
- package/dist/chains/adw_quality.d.ts +12 -0
- package/dist/chains/adw_quality.js +32 -0
- package/dist/chains/adw_scout.d.ts +12 -0
- package/dist/chains/adw_scout.js +27 -0
- package/dist/chains/adw_simple_sdlc.d.ts +43 -0
- package/dist/chains/adw_simple_sdlc.js +147 -0
- package/dist/chains/context.d.ts +19 -0
- package/dist/chains/context.js +1 -0
- package/dist/chains/index.d.ts +19 -0
- package/dist/chains/index.js +116 -0
- package/dist/cli/bin.d.ts +15 -0
- package/dist/cli/bin.js +24 -0
- package/dist/cli/commands/abort.d.ts +1 -0
- package/dist/cli/commands/abort.js +42 -0
- package/dist/cli/commands/doctor.d.ts +1 -0
- package/dist/cli/commands/doctor.js +165 -0
- package/dist/cli/commands/eject.d.ts +1 -0
- package/dist/cli/commands/eject.js +55 -0
- package/dist/cli/commands/events.d.ts +1 -0
- package/dist/cli/commands/events.js +49 -0
- package/dist/cli/commands/init.d.ts +1 -0
- package/dist/cli/commands/init.js +62 -0
- package/dist/cli/commands/install-skill.d.ts +1 -0
- package/dist/cli/commands/install-skill.js +122 -0
- package/dist/cli/commands/list.d.ts +1 -0
- package/dist/cli/commands/list.js +13 -0
- package/dist/cli/commands/migrate.d.ts +1 -0
- package/dist/cli/commands/migrate.js +167 -0
- package/dist/cli/commands/phases.d.ts +1 -0
- package/dist/cli/commands/phases.js +25 -0
- package/dist/cli/commands/run.d.ts +3 -0
- package/dist/cli/commands/run.js +27 -0
- package/dist/cli/commands/sessions.d.ts +1 -0
- package/dist/cli/commands/sessions.js +20 -0
- package/dist/cli/commands/trace.d.ts +8 -0
- package/dist/cli/commands/trace.js +10 -0
- package/dist/cli/commands/ui.d.ts +1 -0
- package/dist/cli/commands/ui.js +35 -0
- package/dist/cli/commands/version.d.ts +1 -0
- package/dist/cli/commands/version.js +8 -0
- package/dist/cli/commands/watch.d.ts +8 -0
- package/dist/cli/commands/watch.js +210 -0
- package/dist/cli/gitignore.d.ts +1 -0
- package/dist/cli/gitignore.js +14 -0
- package/dist/cli/index.d.ts +1 -0
- package/dist/cli/index.js +156 -0
- package/dist/core/agent_cc.d.ts +75 -0
- package/dist/core/agent_cc.js +322 -0
- package/dist/core/agent_flue.d.ts +66 -0
- package/dist/core/agent_flue.js +321 -0
- package/dist/core/agents.d.ts +64 -0
- package/dist/core/agents.js +456 -0
- package/dist/core/changes.d.ts +35 -0
- package/dist/core/changes.js +98 -0
- package/dist/core/console.d.ts +36 -0
- package/dist/core/console.js +156 -0
- package/dist/core/data_types.d.ts +562 -0
- package/dist/core/data_types.js +382 -0
- package/dist/core/gates.d.ts +33 -0
- package/dist/core/gates.js +144 -0
- package/dist/core/git_helper.d.ts +59 -0
- package/dist/core/git_helper.js +115 -0
- package/dist/core/issues/github_provider.d.ts +52 -0
- package/dist/core/issues/github_provider.js +211 -0
- package/dist/core/issues/provider.d.ts +90 -0
- package/dist/core/issues/provider.js +14 -0
- package/dist/core/paths.d.ts +78 -0
- package/dist/core/paths.js +108 -0
- package/dist/core/permissions.d.ts +78 -0
- package/dist/core/permissions.js +187 -0
- package/dist/core/prompts.d.ts +4 -0
- package/dist/core/prompts.js +17 -0
- package/dist/core/quality.d.ts +65 -0
- package/dist/core/quality.js +194 -0
- package/dist/core/runner.d.ts +72 -0
- package/dist/core/runner.js +168 -0
- package/dist/core/session.d.ts +16 -0
- package/dist/core/session.js +60 -0
- package/dist/core/sqlite.d.ts +55 -0
- package/dist/core/sqlite.js +106 -0
- package/dist/core/tracer.d.ts +56 -0
- package/dist/core/tracer.js +246 -0
- package/dist/core/utils.d.ts +40 -0
- package/dist/core/utils.js +108 -0
- package/dist/core/watch.d.ts +44 -0
- package/dist/core/watch.js +194 -0
- package/dist/test/agent_cc.test.d.ts +1 -0
- package/dist/test/agent_cc.test.js +95 -0
- package/dist/test/agent_flue.test.d.ts +1 -0
- package/dist/test/agent_flue.test.js +83 -0
- package/dist/test/data_types.test.d.ts +10 -0
- package/dist/test/data_types.test.js +49 -0
- package/dist/test/ui_server.test.d.ts +1 -0
- package/dist/test/ui_server.test.js +119 -0
- package/dist/test/watch.test.d.ts +1 -0
- package/dist/test/watch.test.js +227 -0
- package/dist/ui/server/app.d.ts +3 -0
- package/dist/ui/server/app.js +98 -0
- package/dist/ui/server/db.d.ts +82 -0
- package/dist/ui/server/db.js +333 -0
- package/dist/ui/server/serve.d.ts +13 -0
- package/dist/ui/server/serve.js +80 -0
- package/dist/ui/server/static.d.ts +5 -0
- package/dist/ui/server/static.js +55 -0
- package/dist/ui/shared/types.d.ts +263 -0
- package/dist/ui/shared/types.js +8 -0
- package/package.json +52 -0
- package/web/assets/index-C7nF068F.css +1 -0
- package/web/assets/index-mzSArcnQ.js +11 -0
- package/web/assets/play-latin-400-normal-GKW-4YV7.woff2 +0 -0
- package/web/assets/play-latin-700-normal-DyPlLDbb.woff2 +0 -0
- package/web/index.html +14 -0
- package/web/logo.svg +6 -0
|
@@ -0,0 +1,321 @@
|
|
|
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 { AgentRunError, createBashTool, createEditTool, createGlobTool, createGrepTool, createReadTool, createWriteTool, init, observe, useDataWriter, useModel, useSandbox, useTool, } from "@flue/runtime";
|
|
26
|
+
import { local, sqlite, start } from "@flue/runtime/node";
|
|
27
|
+
import { UsageBreakdown, makeAgentResult } from "./data_types.js";
|
|
28
|
+
import { nowIso, operatorEnv } from "./utils.js";
|
|
29
|
+
const RESULT_SNIPPET_CHARS = 20_000; // tool output rides along whole; clip only guards pathological cases
|
|
30
|
+
const ARG_VALUE_CHARS = 20_000; // args too — the UI scrolls, it must not be handed cut-off data
|
|
31
|
+
const LABEL_CHARS = 80; // "bash: <command>" shown as the event name
|
|
32
|
+
// The arg that identifies a call at a glance, in the order tools tend to use.
|
|
33
|
+
// Unverified against Flue's own built-in tool argument names beyond what
|
|
34
|
+
// their published schemas show (path/command/pattern) — kept from pi's list
|
|
35
|
+
// since the overlap is exact where it matters; a mismatch here only means a
|
|
36
|
+
// less specific label, never a wrong one.
|
|
37
|
+
const PRIMARY_ARGS = ["command", "path", "file_path", "pattern", "query", "url"];
|
|
38
|
+
function hasMessage(value) {
|
|
39
|
+
return typeof value === "object" && value !== null && typeof value.message === "string";
|
|
40
|
+
}
|
|
41
|
+
function clipText(text, limit) {
|
|
42
|
+
return text.length <= limit ? text : text.slice(0, limit).trimEnd() + "…";
|
|
43
|
+
}
|
|
44
|
+
/** One-line human name for a tool call: `bash: ls -la src`. */
|
|
45
|
+
function labelFor(tool, args) {
|
|
46
|
+
let value = "";
|
|
47
|
+
for (const key of PRIMARY_ARGS) {
|
|
48
|
+
if (typeof args[key] === "string" && args[key].trim()) {
|
|
49
|
+
value = args[key];
|
|
50
|
+
break;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
if (!value) {
|
|
54
|
+
for (const v of Object.values(args)) {
|
|
55
|
+
if (typeof v === "string" && v.trim()) {
|
|
56
|
+
value = v;
|
|
57
|
+
break;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
value = String(value).split(/\s+/).filter(Boolean).join(" ");
|
|
62
|
+
return value ? `${tool}: ${clipText(value, LABEL_CHARS)}` : tool;
|
|
63
|
+
}
|
|
64
|
+
/** `tool-output`'s `output` is typed `unknown` — normalize whatever shape it turns out to be. */
|
|
65
|
+
function snippetOf(output) {
|
|
66
|
+
if (output == null)
|
|
67
|
+
return "";
|
|
68
|
+
if (typeof output === "string")
|
|
69
|
+
return output;
|
|
70
|
+
try {
|
|
71
|
+
return JSON.stringify(output);
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
return String(output);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Folds Flue's `tool-input` + `tool-output`/`tool-output-error` chunk pair
|
|
79
|
+
* into ONE normalized record per completed call — the same shape
|
|
80
|
+
* agent_pi.ts's tracker produced, so agents.ts's eventForwarder (and the
|
|
81
|
+
* tracer/visualizer downstream of it) need no changes. `tool-output` never
|
|
82
|
+
* carries the tool's name, which is exactly why folding is still required.
|
|
83
|
+
*/
|
|
84
|
+
export class ToolCallTracker {
|
|
85
|
+
open = new Map();
|
|
86
|
+
/** Returns the record for a finished tool call, else null. */
|
|
87
|
+
observe(chunk) {
|
|
88
|
+
if (chunk.type === "tool-input") {
|
|
89
|
+
this.open.set(chunk.toolCallId, {
|
|
90
|
+
tool: chunk.toolName,
|
|
91
|
+
args: chunk.input || {},
|
|
92
|
+
started_at: chunk.timestamp || nowIso(),
|
|
93
|
+
});
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
if (chunk.type === "tool-output") {
|
|
97
|
+
return this.finish(chunk.toolCallId, chunk.output, true, chunk.durationMs);
|
|
98
|
+
}
|
|
99
|
+
if (chunk.type === "tool-output-error") {
|
|
100
|
+
return this.finish(chunk.toolCallId, chunk.errorText, false, chunk.durationMs);
|
|
101
|
+
}
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
finish(callId, output, ok, durationMs) {
|
|
105
|
+
const opened = this.open.get(callId);
|
|
106
|
+
this.open.delete(callId);
|
|
107
|
+
const tool = opened?.tool || "tool";
|
|
108
|
+
const args = opened?.args || {};
|
|
109
|
+
const record = {
|
|
110
|
+
tool,
|
|
111
|
+
tool_call_id: callId,
|
|
112
|
+
args: Object.fromEntries(Object.entries(args).map(([key, value]) => [key, typeof value === "string" ? clipText(value, ARG_VALUE_CHARS) : value])),
|
|
113
|
+
ok,
|
|
114
|
+
label: labelFor(tool, args),
|
|
115
|
+
};
|
|
116
|
+
const snippet = snippetOf(output);
|
|
117
|
+
if (snippet)
|
|
118
|
+
record.result_snippet = clipText(snippet, RESULT_SNIPPET_CHARS);
|
|
119
|
+
record.ended_at = nowIso();
|
|
120
|
+
if (durationMs !== undefined)
|
|
121
|
+
record.duration_ms = Math.round(durationMs);
|
|
122
|
+
if (opened?.started_at)
|
|
123
|
+
record.started_at = opened.started_at;
|
|
124
|
+
return record;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
// ── tool-name resolution ─────────────────────────────────────────────────────
|
|
128
|
+
const BUILTIN_TOOLS = {
|
|
129
|
+
read: createReadTool,
|
|
130
|
+
write: createWriteTool,
|
|
131
|
+
edit: createEditTool,
|
|
132
|
+
bash: createBashTool,
|
|
133
|
+
grep: createGrepTool,
|
|
134
|
+
glob: createGlobTool,
|
|
135
|
+
};
|
|
136
|
+
// pi's vocabulary -> Flue's. "ls" has no Flue built-in (bash/glob cover it);
|
|
137
|
+
// it is a KNOWN name that resolves to nothing, not an unknown one.
|
|
138
|
+
const TOOL_ALIASES = { find: "glob" };
|
|
139
|
+
const DROPPED_TOOLS = new Set(["ls"]);
|
|
140
|
+
/** Used by agents.validate() so a typo'd tool name fails before anything spawns. */
|
|
141
|
+
export function isKnownToolName(name) {
|
|
142
|
+
return DROPPED_TOOLS.has(name) || (TOOL_ALIASES[name] ?? name) in BUILTIN_TOOLS;
|
|
143
|
+
}
|
|
144
|
+
function resolveBuiltinTools(names) {
|
|
145
|
+
const factories = [];
|
|
146
|
+
for (const name of names) {
|
|
147
|
+
if (DROPPED_TOOLS.has(name))
|
|
148
|
+
continue;
|
|
149
|
+
const factory = BUILTIN_TOOLS[TOOL_ALIASES[name] ?? name];
|
|
150
|
+
if (factory)
|
|
151
|
+
factories.push(factory);
|
|
152
|
+
}
|
|
153
|
+
return factories;
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* `undefined`/`null` toolNames = every builtin (Flue's own default when no
|
|
157
|
+
* `tools` override is given — matches SPF's "unset = all tools usable").
|
|
158
|
+
* `local()`'s env does NOT inherit process.env by default (only PATH/HOME/
|
|
159
|
+
* USER/LANG/TERM/TMPDIR) — operatorEnv() restores today's actual behavior.
|
|
160
|
+
*/
|
|
161
|
+
function sandboxFor(toolNames, cwd) {
|
|
162
|
+
const base = local({ cwd, env: operatorEnv() });
|
|
163
|
+
if (!toolNames)
|
|
164
|
+
return base;
|
|
165
|
+
const factories = resolveBuiltinTools(toolNames);
|
|
166
|
+
return { ...base, tools: (env) => factories.map((f) => f(env)) };
|
|
167
|
+
}
|
|
168
|
+
// ── model pattern validation ─────────────────────────────────────────────────
|
|
169
|
+
/**
|
|
170
|
+
* Flue exposes no public catalog of registered models (only pi's --list-models
|
|
171
|
+
* CLI did, and that command is gone with pi). This checks only the STATIC
|
|
172
|
+
* `provider/model-id` shape — the same shape SPF's config docs already ask
|
|
173
|
+
* for — not that the provider is reachable or the id exists. An actually
|
|
174
|
+
* wrong model now surfaces at the first real dispatch instead of at
|
|
175
|
+
* validate() time; that trade is made explicit here rather than pretending
|
|
176
|
+
* a catalog check still happens.
|
|
177
|
+
*/
|
|
178
|
+
export function resolveModel(pattern) {
|
|
179
|
+
const slash = pattern.indexOf("/");
|
|
180
|
+
if (slash <= 0 || slash === pattern.length - 1) {
|
|
181
|
+
throw new Error(`model ${JSON.stringify(pattern)} must be written as provider/model-id`);
|
|
182
|
+
}
|
|
183
|
+
return [pattern.slice(0, slash), pattern.slice(slash + 1)];
|
|
184
|
+
}
|
|
185
|
+
const REGISTRY = new Map();
|
|
186
|
+
function sfAgentRender({ id }) {
|
|
187
|
+
const spec = REGISTRY.get(id);
|
|
188
|
+
if (!spec)
|
|
189
|
+
throw new Error(`agent_flue: no render spec registered for conversation ${id} — run() must set it before dispatching`);
|
|
190
|
+
useModel(spec.model, { thinkingLevel: spec.thinking });
|
|
191
|
+
useSandbox(sandboxFor(spec.toolNames, spec.cwd));
|
|
192
|
+
const writeReport = useDataWriter("sf_report");
|
|
193
|
+
useTool({
|
|
194
|
+
name: "sf_report",
|
|
195
|
+
description: `Emit your final ${spec.outputTypeName} report as your last action. Call this exactly once — it ends your turn.`,
|
|
196
|
+
input: spec.outputSchema,
|
|
197
|
+
run: ({ data }) => {
|
|
198
|
+
writeReport(data);
|
|
199
|
+
return { terminate: true };
|
|
200
|
+
},
|
|
201
|
+
});
|
|
202
|
+
return spec.systemText;
|
|
203
|
+
}
|
|
204
|
+
const SfAgent = Object.assign(sfAgentRender, {
|
|
205
|
+
agentName: "spf-agent",
|
|
206
|
+
});
|
|
207
|
+
const pendingUsage = new Map();
|
|
208
|
+
let runtimePromise = null;
|
|
209
|
+
function ensureRuntime(flueDbPath) {
|
|
210
|
+
if (!runtimePromise) {
|
|
211
|
+
runtimePromise = start({ agents: [SfAgent], db: sqlite(flueDbPath) }).then((flue) => {
|
|
212
|
+
observe((event) => {
|
|
213
|
+
if (event.type !== "turn" || !event.submissionId)
|
|
214
|
+
return;
|
|
215
|
+
const slot = pendingUsage.get(event.submissionId);
|
|
216
|
+
if (!slot)
|
|
217
|
+
return; // not one of ours (or already collected) — ignore
|
|
218
|
+
const usage = event.response.usage;
|
|
219
|
+
if (!usage)
|
|
220
|
+
return;
|
|
221
|
+
const turnTokens = contextTokensOf(usage);
|
|
222
|
+
slot.usage.add_turn(usage, turnTokens);
|
|
223
|
+
// Occupancy is read off the last valid AGENT-purpose turn: compaction
|
|
224
|
+
// turns cost real money (folded into usage above) but say nothing
|
|
225
|
+
// about the agent's own conversation occupancy, and an errored turn's
|
|
226
|
+
// reading can't be trusted either way.
|
|
227
|
+
if (event.purpose === "agent" && turnTokens && !event.isError) {
|
|
228
|
+
slot.context_tokens = turnTokens;
|
|
229
|
+
}
|
|
230
|
+
});
|
|
231
|
+
return flue;
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
return runtimePromise;
|
|
235
|
+
}
|
|
236
|
+
/** Graceful shutdown — call once, at process exit. Safe to call if never started. */
|
|
237
|
+
export async function shutdown() {
|
|
238
|
+
if (!runtimePromise)
|
|
239
|
+
return;
|
|
240
|
+
const flue = await runtimePromise;
|
|
241
|
+
await flue.stop();
|
|
242
|
+
runtimePromise = null;
|
|
243
|
+
}
|
|
244
|
+
/**
|
|
245
|
+
* Mirrors pi's own `calculateContextTokens`: prefer the provider's
|
|
246
|
+
* totalTokens, else sum the parts. Cache reads count — cached prompt is
|
|
247
|
+
* still prompt. Verbatim from agent_pi.ts (Flue's PromptUsage shape matches
|
|
248
|
+
* pi-ai's field names one-for-one).
|
|
249
|
+
*/
|
|
250
|
+
function contextTokensOf(usage) {
|
|
251
|
+
if (usage.totalTokens)
|
|
252
|
+
return Math.trunc(usage.totalTokens);
|
|
253
|
+
return Math.trunc(usage.input + usage.output + usage.cacheRead + usage.cacheWrite);
|
|
254
|
+
}
|
|
255
|
+
/**
|
|
256
|
+
* Run one Flue dispatch+read turn against the given conversation id,
|
|
257
|
+
* creating it on first contact and continuing it on every later call with
|
|
258
|
+
* the same `session_id` — the same continuity `pi --session-id` gave.
|
|
259
|
+
*
|
|
260
|
+
* `onSpawn`/`onExit` bracket this call the way they bracketed pi's child
|
|
261
|
+
* process, but there is no child process here: Flue runs in-process, so
|
|
262
|
+
* both fire with this process's own pid. A future `spf abort` can still find
|
|
263
|
+
* and stop the right OS process; distinguishing WHICH in-flight submission
|
|
264
|
+
* that pid is running is a `processes.submission_id` column left for later,
|
|
265
|
+
* not attempted here.
|
|
266
|
+
*/
|
|
267
|
+
export async function run(request, onEvent, onSpawn, onExit) {
|
|
268
|
+
await ensureRuntime(request.flue_db_path);
|
|
269
|
+
REGISTRY.set(request.session_id, {
|
|
270
|
+
model: request.model,
|
|
271
|
+
thinking: request.thinking,
|
|
272
|
+
cwd: request.cwd,
|
|
273
|
+
toolNames: request.tools,
|
|
274
|
+
systemText: request.system_prompt,
|
|
275
|
+
outputSchema: request.output_schema,
|
|
276
|
+
outputTypeName: request.output_type_name,
|
|
277
|
+
});
|
|
278
|
+
const pid = process.pid ?? -1;
|
|
279
|
+
onSpawn?.(pid);
|
|
280
|
+
const handle = init(SfAgent, { id: request.session_id });
|
|
281
|
+
const receipt = await handle.dispatch(request.prompt);
|
|
282
|
+
const slot = { usage: new UsageBreakdown(), context_tokens: 0 };
|
|
283
|
+
pendingUsage.set(receipt.submissionId, slot);
|
|
284
|
+
try {
|
|
285
|
+
// Raw chunks pass straight through, exactly as agent_pi.ts's onEvent
|
|
286
|
+
// carried pi's raw JSONL events — the folding into one record per tool
|
|
287
|
+
// call is eventForwarder's job (agents.ts), via this module's own
|
|
288
|
+
// ToolCallTracker. Folding here too would fold twice.
|
|
289
|
+
const reply = await handle.read(receipt, { onEvent });
|
|
290
|
+
const reportEntries = reply.data["sf_report"];
|
|
291
|
+
const report = reportEntries && reportEntries.length > 0 ? reportEntries[reportEntries.length - 1] : null;
|
|
292
|
+
return makeAgentResult({
|
|
293
|
+
session_id: request.session_id,
|
|
294
|
+
text: reply.text,
|
|
295
|
+
report,
|
|
296
|
+
tokens: slot.usage.total_tokens,
|
|
297
|
+
cost: slot.usage.total_cost,
|
|
298
|
+
usage: slot.usage,
|
|
299
|
+
context_tokens: slot.context_tokens,
|
|
300
|
+
context_window: 0,
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
catch (error) {
|
|
304
|
+
if (error instanceof AgentRunError) {
|
|
305
|
+
// Verified by reproduction: a settled failure's `cause` is a plain
|
|
306
|
+
// durably-recorded error record — {name, message, type, details} —
|
|
307
|
+
// NOT a live Error instance (a durable settlement is read back from
|
|
308
|
+
// storage, not rethrown from the original throw site). Duck-type the
|
|
309
|
+
// message rather than checking `instanceof Error`, or every failure
|
|
310
|
+
// prints the unhelpful "[object Object]".
|
|
311
|
+
const cause = error.cause;
|
|
312
|
+
const detail = hasMessage(cause) ? cause.message : cause instanceof Error ? cause.message : cause ? JSON.stringify(cause) : error.message;
|
|
313
|
+
throw new Error(`flue submission ${error.outcome}: ${detail}`);
|
|
314
|
+
}
|
|
315
|
+
throw error;
|
|
316
|
+
}
|
|
317
|
+
finally {
|
|
318
|
+
pendingUsage.delete(receipt.submissionId);
|
|
319
|
+
onExit?.(pid);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Config loading/validation and agent execution.
|
|
3
|
+
*
|
|
4
|
+
* Every ADW validates its agents before running (fail fast, nothing spawns
|
|
5
|
+
* against a half-valid config). Every agent call parses against a concrete
|
|
6
|
+
* output type; parse failures and gate violations re-prompt the SAME session
|
|
7
|
+
* with a correction — context intact, bounded retries. Agent proposes, code
|
|
8
|
+
* disposes.
|
|
9
|
+
*/
|
|
10
|
+
import { GateReport, makeEventRecord, type AgentCall, type AgentConfig, type EnvelopeBase, type Phase, type SFConfig } from "./data_types.ts";
|
|
11
|
+
export declare class GateFailure extends Error {
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Load and merge every existing path in `configPaths`, in order — later
|
|
15
|
+
* paths override earlier ones. Built-in defaults first, an optional `.spf/`
|
|
16
|
+
* override second is the normal case; a single explicit `--config` path
|
|
17
|
+
* (the caller passes just that one path, no built-in) is used standalone.
|
|
18
|
+
* A path that doesn't exist is silently skipped, EXCEPT that if none of them
|
|
19
|
+
* exist the result is an all-defaults config — every field has a schema
|
|
20
|
+
* default, so this degrades to "zero agents defined" rather than a crash,
|
|
21
|
+
* and agents.validate() reports that plainly.
|
|
22
|
+
*/
|
|
23
|
+
export declare function loadConfig(configPaths: string[]): SFConfig;
|
|
24
|
+
export declare function resolve(cfg: SFConfig, name: string): AgentConfig;
|
|
25
|
+
/** Fail fast: every required name must resolve to a usable agent. */
|
|
26
|
+
export declare function validate(cfg: SFConfig, required: string[], requiredSuites?: string[], cwd?: string): void;
|
|
27
|
+
interface RunForAgents {
|
|
28
|
+
cfg: SFConfig;
|
|
29
|
+
adw_id: string;
|
|
30
|
+
repo_root: string;
|
|
31
|
+
spf_dir: string | null;
|
|
32
|
+
data_dir: string;
|
|
33
|
+
session_dir: string;
|
|
34
|
+
context_handoff_dir: string;
|
|
35
|
+
agent_map: Record<string, {
|
|
36
|
+
session_id: string;
|
|
37
|
+
model: string;
|
|
38
|
+
coding_agent: string;
|
|
39
|
+
}>;
|
|
40
|
+
tracer: {
|
|
41
|
+
event: (record: ReturnType<typeof makeEventRecord>) => string;
|
|
42
|
+
processStart: (adwId: string, kind: string, name: string, pid: number, command: string) => void;
|
|
43
|
+
processEnd: (adwId: string, pid: number) => void;
|
|
44
|
+
envelopeRow: (phase: Phase, agent: string, outputType: string, payloadJson: string, valid: boolean, attempt: number) => void;
|
|
45
|
+
gateRow: (phase: Phase, gate: string, report: GateReport, attempt: number) => void;
|
|
46
|
+
agentSessionRow: (adwId: string, agent: AgentConfig, sessionId: string, contextTokens?: number, contextWindow?: number) => void;
|
|
47
|
+
};
|
|
48
|
+
console: {
|
|
49
|
+
agentStarted: (name: string, model: string, sessionId: string) => void;
|
|
50
|
+
gateResult: (name: string, report: GateReport) => void;
|
|
51
|
+
retry: (name: string, attempt: number, limit: number, reason: string) => void;
|
|
52
|
+
envelopeSummary: (envelope: EnvelopeBase, typeName: string) => void;
|
|
53
|
+
agentFinished: (name: string, tokens: number, cost: number) => void;
|
|
54
|
+
};
|
|
55
|
+
addUsage: (tokens: number, cost: number) => void;
|
|
56
|
+
saveAgentMap: (agent: string, entry: {
|
|
57
|
+
session_id: string;
|
|
58
|
+
model: string;
|
|
59
|
+
coding_agent: string;
|
|
60
|
+
}) => void;
|
|
61
|
+
}
|
|
62
|
+
/** One agent call: render prompts -> pi run -> typed parse -> gates -> envelope. */
|
|
63
|
+
export declare function execute(run: RunForAgents, phase: Phase, call: AgentCall): Promise<EnvelopeBase>;
|
|
64
|
+
export {};
|