bonecode 1.2.3 → 1.4.1
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/README.md +62 -0
- package/compat/opencode_adapter.ts +69 -8
- package/dist/compat/opencode_adapter.js +63 -7
- package/dist/compat/opencode_adapter.js.map +1 -1
- package/dist/src/db_adapter.js +30 -0
- package/dist/src/db_adapter.js.map +1 -1
- package/dist/src/engine/agent/prompt/compaction.txt +9 -0
- package/dist/src/engine/agent/prompt/explore.txt +18 -0
- package/dist/src/engine/agent/prompt/scout.txt +36 -0
- package/dist/src/engine/agent/prompt/summary.txt +11 -0
- package/dist/src/engine/agent/prompt/title.txt +44 -0
- package/dist/src/engine/session/build_mode.d.ts +83 -0
- package/dist/src/engine/session/build_mode.js +789 -0
- package/dist/src/engine/session/build_mode.js.map +1 -0
- package/dist/src/engine/session/build_mode_helpers.d.ts +6 -0
- package/dist/src/engine/session/build_mode_helpers.js +61 -0
- package/dist/src/engine/session/build_mode_helpers.js.map +1 -0
- package/dist/src/engine/session/prompt/anthropic.txt +105 -0
- package/dist/src/engine/session/prompt/beast.txt +147 -0
- package/dist/src/engine/session/prompt/bonescript.txt +402 -0
- package/dist/src/engine/session/prompt/build-switch.txt +5 -0
- package/dist/src/engine/session/prompt/codex.txt +79 -0
- package/dist/src/engine/session/prompt/copilot-gpt-5.txt +143 -0
- package/dist/src/engine/session/prompt/default.txt +105 -0
- package/dist/src/engine/session/prompt/gemini.txt +155 -0
- package/dist/src/engine/session/prompt/gpt.txt +107 -0
- package/dist/src/engine/session/prompt/kimi.txt +95 -0
- package/dist/src/engine/session/prompt/max-steps.txt +16 -0
- package/dist/src/engine/session/prompt/plan-reminder-anthropic.txt +67 -0
- package/dist/src/engine/session/prompt/plan.txt +26 -0
- package/dist/src/engine/session/prompt/trinity.txt +97 -0
- package/dist/src/engine/session/prompt.js +92 -4
- package/dist/src/engine/session/prompt.js.map +1 -1
- package/dist/src/engine/skill/prompt/customize-opencode.md +377 -0
- package/dist/src/engine/tool/apply_patch.txt +33 -0
- package/dist/src/engine/tool/edit.txt +10 -0
- package/dist/src/engine/tool/glob.txt +6 -0
- package/dist/src/engine/tool/grep.txt +8 -0
- package/dist/src/engine/tool/lsp.txt +24 -0
- package/dist/src/engine/tool/plan-enter.txt +14 -0
- package/dist/src/engine/tool/plan-exit.txt +13 -0
- package/dist/src/engine/tool/question.txt +10 -0
- package/dist/src/engine/tool/read.txt +14 -0
- package/dist/src/engine/tool/repo_clone.txt +5 -0
- package/dist/src/engine/tool/repo_overview.txt +4 -0
- package/dist/src/engine/tool/shell/shell.txt +77 -0
- package/dist/src/engine/tool/skill.txt +5 -0
- package/dist/src/engine/tool/task.txt +58 -0
- package/dist/src/engine/tool/task_status.txt +13 -0
- package/dist/src/engine/tool/todowrite.txt +167 -0
- package/dist/src/engine/tool/tool/apply_patch.txt +33 -0
- package/dist/src/engine/tool/tool/edit.txt +10 -0
- package/dist/src/engine/tool/tool/glob.txt +6 -0
- package/dist/src/engine/tool/tool/grep.txt +8 -0
- package/dist/src/engine/tool/tool/lsp.txt +24 -0
- package/dist/src/engine/tool/tool/plan-enter.txt +14 -0
- package/dist/src/engine/tool/tool/plan-exit.txt +13 -0
- package/dist/src/engine/tool/tool/question.txt +10 -0
- package/dist/src/engine/tool/tool/read.txt +14 -0
- package/dist/src/engine/tool/tool/repo_clone.txt +5 -0
- package/dist/src/engine/tool/tool/repo_overview.txt +4 -0
- package/dist/src/engine/tool/tool/shell/shell.txt +77 -0
- package/dist/src/engine/tool/tool/skill.txt +5 -0
- package/dist/src/engine/tool/tool/task.txt +58 -0
- package/dist/src/engine/tool/tool/task_status.txt +13 -0
- package/dist/src/engine/tool/tool/todowrite.txt +167 -0
- package/dist/src/engine/tool/tool/webfetch.txt +13 -0
- package/dist/src/engine/tool/tool/websearch.txt +14 -0
- package/dist/src/engine/tool/tool/write.txt +8 -0
- package/dist/src/engine/tool/webfetch.txt +13 -0
- package/dist/src/engine/tool/websearch.txt +14 -0
- package/dist/src/engine/tool/write.txt +8 -0
- package/dist/src/tui.js +146 -9
- package/dist/src/tui.js.map +1 -1
- package/package.json +2 -2
- package/scripts/copy_prompts.js +58 -0
- package/scripts/test_bonescript_primer.js +111 -0
- package/scripts/test_build_fallback.js +221 -0
- package/scripts/test_build_mode.js +301 -0
- package/src/db_adapter.ts +29 -0
- package/src/engine/session/build_mode.ts +895 -0
- package/src/engine/session/build_mode_helpers.ts +72 -0
- package/src/engine/session/prompt/bonescript.txt +402 -0
- package/src/engine/session/prompt.ts +105 -4
- package/src/tui.ts +147 -9
|
@@ -0,0 +1,789 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Autonomous Build Mode — small-model-friendly project orchestrator
|
|
4
|
+
*
|
|
5
|
+
* Why: small/local models (8-20B) struggle with open-ended "build me X" prompts.
|
|
6
|
+
* They produce prose, hallucinate edits, and forget what they were doing across
|
|
7
|
+
* turns. This module replaces the single-turn agent loop with a deterministic
|
|
8
|
+
* state machine that drives the model through narrow, focused stages.
|
|
9
|
+
*
|
|
10
|
+
* State flow:
|
|
11
|
+
*
|
|
12
|
+
* ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐
|
|
13
|
+
* │ CLARIFY │ ──▶ │ PLAN │ ──▶ │ EXECUTE │ ──▶ │ VERIFY │
|
|
14
|
+
* │ (Q&A) │ │ (todos) │ │ (loop) │ │ (yes/no) │
|
|
15
|
+
* └──────────┘ └──────────┘ └─────┬────┘ └─────┬────┘
|
|
16
|
+
* │ │
|
|
17
|
+
* ▼ ▼
|
|
18
|
+
* ┌────────┐ ┌────────┐
|
|
19
|
+
* │ DONE │ ◀──── │ all ok │
|
|
20
|
+
* └────────┘ └────────┘
|
|
21
|
+
* │
|
|
22
|
+
* │ failures
|
|
23
|
+
* ▼
|
|
24
|
+
* back to PLAN
|
|
25
|
+
*
|
|
26
|
+
* Each stage uses a tightly scoped prompt with a structured-output requirement
|
|
27
|
+
* (JSON we parse deterministically). The agent's natural "describe what I'd do"
|
|
28
|
+
* tendency is replaced by short, mechanical answers.
|
|
29
|
+
*
|
|
30
|
+
* State is persisted to the sessions table (in build_state JSON column) so
|
|
31
|
+
* the loop can resume across restarts and is visible to the user via the UI.
|
|
32
|
+
*/
|
|
33
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
34
|
+
if (k2 === undefined) k2 = k;
|
|
35
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
36
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
37
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
38
|
+
}
|
|
39
|
+
Object.defineProperty(o, k2, desc);
|
|
40
|
+
}) : (function(o, m, k, k2) {
|
|
41
|
+
if (k2 === undefined) k2 = k;
|
|
42
|
+
o[k2] = m[k];
|
|
43
|
+
}));
|
|
44
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
45
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
46
|
+
}) : function(o, v) {
|
|
47
|
+
o["default"] = v;
|
|
48
|
+
});
|
|
49
|
+
var __importStar = (this && this.__importStar) || function (mod) {
|
|
50
|
+
if (mod && mod.__esModule) return mod;
|
|
51
|
+
var result = {};
|
|
52
|
+
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
|
|
53
|
+
__setModuleDefault(result, mod);
|
|
54
|
+
return result;
|
|
55
|
+
};
|
|
56
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
57
|
+
exports.isBuildPrompt = exports.runBuildMode = exports.continueAfterClarification = void 0;
|
|
58
|
+
const ai_1 = require("ai");
|
|
59
|
+
const uuid_1 = require("uuid");
|
|
60
|
+
const db_1 = require("../../../bone/output/session/src/db");
|
|
61
|
+
const websocket_1 = require("../../../bone/output/session/src/websocket");
|
|
62
|
+
const logger_1 = require("../../../bone/output/session/src/logger");
|
|
63
|
+
const prompt_1 = require("./prompt");
|
|
64
|
+
// ─── Persistence ──────────────────────────────────────────────────────────────
|
|
65
|
+
async function loadState(session_id) {
|
|
66
|
+
try {
|
|
67
|
+
const r = await db_1.pool.query(`SELECT build_state FROM sessions WHERE id = $1`, [session_id]);
|
|
68
|
+
const raw = r.rows[0]?.build_state;
|
|
69
|
+
if (!raw)
|
|
70
|
+
return null;
|
|
71
|
+
return typeof raw === "string" ? JSON.parse(raw) : raw;
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
async function saveState(session_id, state) {
|
|
78
|
+
// Try with the dedicated column first; fall back to permission_ruleset.build
|
|
79
|
+
// if the column doesn't exist yet (older schemas).
|
|
80
|
+
try {
|
|
81
|
+
await db_1.pool.query(`UPDATE sessions SET build_state = $2::jsonb, updated_at = NOW() WHERE id = $1`, [session_id, JSON.stringify(state)]);
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
try {
|
|
85
|
+
await db_1.pool.query(`UPDATE sessions
|
|
86
|
+
SET permission_ruleset = jsonb_set(COALESCE(permission_ruleset, '{}'::jsonb), '{build}', $2::jsonb),
|
|
87
|
+
updated_at = NOW()
|
|
88
|
+
WHERE id = $1`, [session_id, JSON.stringify(state)]);
|
|
89
|
+
}
|
|
90
|
+
catch { }
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
function emit(session_id, event, data) {
|
|
94
|
+
(0, websocket_1.broadcastToChannel)("session_events", { type: event, session_id, ...data });
|
|
95
|
+
}
|
|
96
|
+
// ─── Structured-output helpers ────────────────────────────────────────────────
|
|
97
|
+
/**
|
|
98
|
+
* Ask the model a focused question with a JSON-only response requirement.
|
|
99
|
+
* Strips any prose/markdown around the JSON and parses it. Returns null on
|
|
100
|
+
* failure so the caller can decide whether to retry.
|
|
101
|
+
*/
|
|
102
|
+
async function askJson(input) {
|
|
103
|
+
const { getLanguageModel } = await Promise.resolve().then(() => __importStar(require("./build_mode_helpers")));
|
|
104
|
+
const model = getLanguageModel(input.provider_id, input.model_id);
|
|
105
|
+
const fullSystem = [
|
|
106
|
+
input.system,
|
|
107
|
+
"",
|
|
108
|
+
"OUTPUT REQUIREMENTS:",
|
|
109
|
+
`- Reply with a single JSON object only.`,
|
|
110
|
+
`- Expected shape: ${input.schema_hint}`,
|
|
111
|
+
`- Do NOT include any prose before or after the JSON.`,
|
|
112
|
+
`- Do NOT wrap in markdown code fences.`,
|
|
113
|
+
`- Do NOT explain. Output only the JSON object.`,
|
|
114
|
+
].join("\n");
|
|
115
|
+
try {
|
|
116
|
+
const { text } = await (0, ai_1.generateText)({
|
|
117
|
+
model,
|
|
118
|
+
system: fullSystem,
|
|
119
|
+
prompt: input.user,
|
|
120
|
+
temperature: 0.1,
|
|
121
|
+
maxTokens: 2048,
|
|
122
|
+
});
|
|
123
|
+
return parseJsonLoose(text);
|
|
124
|
+
}
|
|
125
|
+
catch (e) {
|
|
126
|
+
logger_1.logger.error("build_mode_json_failed", { event: "askJson", metadata: { error: e.message } });
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Parse JSON from a model response that might be wrapped in markdown fences,
|
|
132
|
+
* have extra prose around it, or contain partial output. Returns null if no
|
|
133
|
+
* recoverable JSON object is found.
|
|
134
|
+
*/
|
|
135
|
+
function parseJsonLoose(raw) {
|
|
136
|
+
if (!raw)
|
|
137
|
+
return null;
|
|
138
|
+
// Strip <think>...</think> blocks (some reasoning models include them)
|
|
139
|
+
let s = raw.replace(/<think>[\s\S]*?<\/think>/gi, "").trim();
|
|
140
|
+
// Strip code fences
|
|
141
|
+
s = s.replace(/^```(?:json)?\s*/i, "").replace(/```\s*$/, "").trim();
|
|
142
|
+
// Find first { and matching close
|
|
143
|
+
const start = s.indexOf("{");
|
|
144
|
+
if (start === -1) {
|
|
145
|
+
// Maybe an array
|
|
146
|
+
const arrStart = s.indexOf("[");
|
|
147
|
+
if (arrStart === -1)
|
|
148
|
+
return null;
|
|
149
|
+
return tryParse(extractBalanced(s, arrStart, "[", "]"));
|
|
150
|
+
}
|
|
151
|
+
return tryParse(extractBalanced(s, start, "{", "}"));
|
|
152
|
+
}
|
|
153
|
+
function extractBalanced(s, start, open, close) {
|
|
154
|
+
let depth = 0;
|
|
155
|
+
let inStr = false;
|
|
156
|
+
let escape = false;
|
|
157
|
+
for (let i = start; i < s.length; i++) {
|
|
158
|
+
const ch = s[i];
|
|
159
|
+
if (escape) {
|
|
160
|
+
escape = false;
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
if (ch === "\\") {
|
|
164
|
+
escape = true;
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
if (ch === '"') {
|
|
168
|
+
inStr = !inStr;
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
if (inStr)
|
|
172
|
+
continue;
|
|
173
|
+
if (ch === open)
|
|
174
|
+
depth++;
|
|
175
|
+
else if (ch === close) {
|
|
176
|
+
depth--;
|
|
177
|
+
if (depth === 0)
|
|
178
|
+
return s.slice(start, i + 1);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
return s.slice(start); // unbalanced — return what we have
|
|
182
|
+
}
|
|
183
|
+
function tryParse(s) {
|
|
184
|
+
try {
|
|
185
|
+
return JSON.parse(s);
|
|
186
|
+
}
|
|
187
|
+
catch {
|
|
188
|
+
return null;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
// ─── Stage 1: Clarify ─────────────────────────────────────────────────────────
|
|
192
|
+
async function stageClarify(state, input) {
|
|
193
|
+
emit(input.session_id, "build.stage", { stage: "clarify" });
|
|
194
|
+
// Ask the model: do you have enough info? If not, what 1-3 questions?
|
|
195
|
+
const result = await askJson({
|
|
196
|
+
model_id: input.model_id,
|
|
197
|
+
provider_id: input.provider_id,
|
|
198
|
+
system: [
|
|
199
|
+
"You are a senior engineer scoping a project.",
|
|
200
|
+
"The user has given you a prompt. Decide if you have enough to start.",
|
|
201
|
+
"If yes, propose a concrete design document.",
|
|
202
|
+
"If no, ask 1-3 specific questions.",
|
|
203
|
+
"",
|
|
204
|
+
"RULES:",
|
|
205
|
+
"- Never ask more than 3 questions in one round.",
|
|
206
|
+
"- Questions must be answerable in one sentence each.",
|
|
207
|
+
"- If the prompt is concrete enough (mentions specific tech, scope, constraints), set sufficient=true.",
|
|
208
|
+
].join("\n"),
|
|
209
|
+
user: `User prompt:\n${input.prompt}`,
|
|
210
|
+
schema_hint: `{ "sufficient": boolean, "questions": string[], "proposed_design": { "goal": string, "requirements": string[], "constraints": string[], "artifacts": string[] } }`,
|
|
211
|
+
});
|
|
212
|
+
if (!result) {
|
|
213
|
+
state.error = "Could not parse model response during clarification.";
|
|
214
|
+
state.stage = "failed";
|
|
215
|
+
return state;
|
|
216
|
+
}
|
|
217
|
+
if (result.sufficient && result.proposed_design) {
|
|
218
|
+
state.design = result.proposed_design;
|
|
219
|
+
state.stage = "plan";
|
|
220
|
+
emit(input.session_id, "build.design", { design: result.proposed_design });
|
|
221
|
+
return state;
|
|
222
|
+
}
|
|
223
|
+
// Ask the user the questions
|
|
224
|
+
state.pending_clarification = result.questions.join("\n");
|
|
225
|
+
emit(input.session_id, "build.questions", { questions: result.questions });
|
|
226
|
+
return state;
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* Called when the user has answered the clarification questions.
|
|
230
|
+
* Combines the original prompt + answers into a finalized design.
|
|
231
|
+
*/
|
|
232
|
+
async function continueAfterClarification(session_id, user_answer, model_id, provider_id) {
|
|
233
|
+
const state = await loadState(session_id);
|
|
234
|
+
if (!state || state.stage !== "clarify")
|
|
235
|
+
return null;
|
|
236
|
+
const result = await askJson({
|
|
237
|
+
model_id,
|
|
238
|
+
provider_id,
|
|
239
|
+
system: [
|
|
240
|
+
"You are scoping a project. Below is the original user prompt, the questions you asked, and the user's answers.",
|
|
241
|
+
"Produce a concrete design document.",
|
|
242
|
+
"",
|
|
243
|
+
"REQUIREMENTS list must be specific and verifiable (each item must be answerable yes/no).",
|
|
244
|
+
"ARTIFACTS list must be concrete file paths the project should produce.",
|
|
245
|
+
].join("\n"),
|
|
246
|
+
user: [
|
|
247
|
+
`Original prompt:\n${state.original_prompt}`,
|
|
248
|
+
``,
|
|
249
|
+
`Questions asked:\n${state.pending_clarification ?? ""}`,
|
|
250
|
+
``,
|
|
251
|
+
`User's answers:\n${user_answer}`,
|
|
252
|
+
].join("\n"),
|
|
253
|
+
schema_hint: `{ "goal": string, "requirements": string[], "constraints": string[], "artifacts": string[] }`,
|
|
254
|
+
});
|
|
255
|
+
if (!result) {
|
|
256
|
+
state.error = "Could not finalize design from clarification answers.";
|
|
257
|
+
state.stage = "failed";
|
|
258
|
+
}
|
|
259
|
+
else {
|
|
260
|
+
state.design = result;
|
|
261
|
+
state.pending_clarification = undefined;
|
|
262
|
+
state.stage = "plan";
|
|
263
|
+
emit(session_id, "build.design", { design: result });
|
|
264
|
+
}
|
|
265
|
+
await saveState(session_id, state);
|
|
266
|
+
return state;
|
|
267
|
+
}
|
|
268
|
+
exports.continueAfterClarification = continueAfterClarification;
|
|
269
|
+
// ─── Stage 2: Plan ────────────────────────────────────────────────────────────
|
|
270
|
+
async function stagePlan(state, input) {
|
|
271
|
+
if (!state.design) {
|
|
272
|
+
state.stage = "failed";
|
|
273
|
+
state.error = "No design to plan from.";
|
|
274
|
+
return state;
|
|
275
|
+
}
|
|
276
|
+
emit(input.session_id, "build.stage", { stage: "plan" });
|
|
277
|
+
const result = await askJson({
|
|
278
|
+
model_id: input.model_id,
|
|
279
|
+
provider_id: input.provider_id,
|
|
280
|
+
system: [
|
|
281
|
+
"You are turning a design document into an ordered todo list.",
|
|
282
|
+
"",
|
|
283
|
+
"RULES:",
|
|
284
|
+
"- Each todo must be a concrete, single-file action when possible.",
|
|
285
|
+
"- Use tool-friendly verbs: 'Write', 'Edit', 'Run', 'Create'.",
|
|
286
|
+
"- Order todos by dependency: schema/config first, then implementation, then tests.",
|
|
287
|
+
"- Aim for 5-15 todos. Don't split trivial work.",
|
|
288
|
+
"- Each title fits on one line, max 80 chars.",
|
|
289
|
+
"- Each description gives the file path and what to do, in 1-2 sentences.",
|
|
290
|
+
].join("\n"),
|
|
291
|
+
user: [
|
|
292
|
+
`Design:`,
|
|
293
|
+
`Goal: ${state.design.goal}`,
|
|
294
|
+
`Requirements:\n${state.design.requirements.map((r) => `- ${r}`).join("\n")}`,
|
|
295
|
+
`Constraints:\n${state.design.constraints.map((c) => `- ${c}`).join("\n")}`,
|
|
296
|
+
`Expected artifacts:\n${state.design.artifacts.map((a) => `- ${a}`).join("\n")}`,
|
|
297
|
+
].join("\n"),
|
|
298
|
+
schema_hint: `{ "todos": [{ "title": string, "description": string }] }`,
|
|
299
|
+
});
|
|
300
|
+
if (!result || !Array.isArray(result.todos) || result.todos.length === 0) {
|
|
301
|
+
state.error = "Could not produce a todo list.";
|
|
302
|
+
state.stage = "failed";
|
|
303
|
+
return state;
|
|
304
|
+
}
|
|
305
|
+
state.todos = result.todos.map((t) => ({
|
|
306
|
+
id: (0, uuid_1.v4)(),
|
|
307
|
+
title: t.title,
|
|
308
|
+
description: t.description,
|
|
309
|
+
status: "pending",
|
|
310
|
+
failure_count: 0,
|
|
311
|
+
}));
|
|
312
|
+
state.stage = "execute";
|
|
313
|
+
emit(input.session_id, "build.plan", { todos: state.todos });
|
|
314
|
+
return state;
|
|
315
|
+
}
|
|
316
|
+
// ─── Stage 3: Execute ─────────────────────────────────────────────────────────
|
|
317
|
+
const MAX_TODO_RETRIES = 3;
|
|
318
|
+
async function stageExecute(state, input) {
|
|
319
|
+
emit(input.session_id, "build.stage", { stage: "execute" });
|
|
320
|
+
// Detect tool-calling capability with a probe. Small/local models often can't
|
|
321
|
+
// emit tool calls in the OpenAI format. When that's the case, switch to
|
|
322
|
+
// a content-fallback path where we ask the model for a JSON manifest of
|
|
323
|
+
// files to write and apply them ourselves via fs.
|
|
324
|
+
if (state.tool_capable === undefined) {
|
|
325
|
+
state.tool_capable = await probeToolCapability(input);
|
|
326
|
+
if (!state.tool_capable) {
|
|
327
|
+
emit(input.session_id, "session.warning", {
|
|
328
|
+
message: `Model ${input.model_id} cannot emit tool calls — using JSON-manifest fallback mode (file content provided directly by the model, applied by the orchestrator).`,
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
// Track consecutive tool-call failures so we can bail out fast instead
|
|
333
|
+
// of grinding through 30 iterations against a model that can't do tools.
|
|
334
|
+
let consecutiveZeroToolCallTodos = 0;
|
|
335
|
+
const ABORT_AFTER_CONSECUTIVE_ZERO = 2;
|
|
336
|
+
while (state.iteration < state.max_iterations) {
|
|
337
|
+
state.iteration++;
|
|
338
|
+
const next = state.todos.find((t) => t.status === "pending");
|
|
339
|
+
if (!next) {
|
|
340
|
+
// All todos done — move to verification
|
|
341
|
+
state.stage = "verify";
|
|
342
|
+
return state;
|
|
343
|
+
}
|
|
344
|
+
next.status = "in_progress";
|
|
345
|
+
emit(input.session_id, "build.todo.start", { todo: next });
|
|
346
|
+
let succeeded = false;
|
|
347
|
+
let toolCallsCount = 0;
|
|
348
|
+
let errorMsg = "";
|
|
349
|
+
if (state.tool_capable) {
|
|
350
|
+
// ── Tool-calling path ────────────────────────────────────────────────
|
|
351
|
+
const focusedPrompt = [
|
|
352
|
+
`<build-task>`,
|
|
353
|
+
`Title: ${next.title}`,
|
|
354
|
+
`Description: ${next.description}`,
|
|
355
|
+
``,
|
|
356
|
+
`This is one task in a larger build. Complete this task NOW by calling the appropriate tools.`,
|
|
357
|
+
`Do not describe what you would do — call the tools.`,
|
|
358
|
+
`</build-task>`,
|
|
359
|
+
].join("\n");
|
|
360
|
+
const taskMsgId = (0, uuid_1.v4)();
|
|
361
|
+
await db_1.pool.query(`INSERT INTO messages (id, session_id, role) VALUES ($1, $2, 'user')`, [taskMsgId, input.session_id]);
|
|
362
|
+
const taskPartId = (0, uuid_1.v4)();
|
|
363
|
+
await db_1.pool.query(`INSERT INTO parts (id, message_id, session_id, part_type, data, order_index) VALUES ($1, $2, $3, 'text', $4, 0)`, [taskPartId, input.session_id, input.session_id, JSON.stringify({ text: focusedPrompt, synthetic: true })]);
|
|
364
|
+
const result = await (0, prompt_1.runAgentLoop)({
|
|
365
|
+
session_id: input.session_id,
|
|
366
|
+
message_id: taskMsgId,
|
|
367
|
+
content: focusedPrompt,
|
|
368
|
+
model_id: input.model_id,
|
|
369
|
+
provider_id: input.provider_id,
|
|
370
|
+
agent_name: "build",
|
|
371
|
+
});
|
|
372
|
+
toolCallsCount = await countToolCallsSince(input.session_id, taskMsgId);
|
|
373
|
+
succeeded = result.ok && toolCallsCount > 0;
|
|
374
|
+
errorMsg = result.error || "no tool calls";
|
|
375
|
+
}
|
|
376
|
+
else {
|
|
377
|
+
// ── JSON-manifest fallback ───────────────────────────────────────────
|
|
378
|
+
const fallback = await executeFallback(state, next, input);
|
|
379
|
+
succeeded = fallback.ok;
|
|
380
|
+
toolCallsCount = fallback.filesWritten;
|
|
381
|
+
errorMsg = fallback.error || "no files produced";
|
|
382
|
+
}
|
|
383
|
+
if (succeeded) {
|
|
384
|
+
next.status = "completed";
|
|
385
|
+
next.evidence = state.tool_capable
|
|
386
|
+
? `${toolCallsCount} tool call(s) made`
|
|
387
|
+
: `${toolCallsCount} file(s) written via fallback`;
|
|
388
|
+
consecutiveZeroToolCallTodos = 0;
|
|
389
|
+
emit(input.session_id, "build.todo.done", { todo: next });
|
|
390
|
+
}
|
|
391
|
+
else {
|
|
392
|
+
next.failure_count++;
|
|
393
|
+
if (next.failure_count >= MAX_TODO_RETRIES) {
|
|
394
|
+
next.status = "failed";
|
|
395
|
+
consecutiveZeroToolCallTodos++;
|
|
396
|
+
emit(input.session_id, "build.todo.failed", { todo: next, reason: errorMsg });
|
|
397
|
+
}
|
|
398
|
+
else {
|
|
399
|
+
next.status = "pending";
|
|
400
|
+
emit(input.session_id, "build.todo.retry", { todo: next, attempt: next.failure_count });
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
await saveState(input.session_id, state);
|
|
404
|
+
// Early bailout — if N consecutive todos fail completely, this model can't
|
|
405
|
+
// do the job. Stop so the user can switch models instead of waiting through
|
|
406
|
+
// 30 useless iterations.
|
|
407
|
+
if (consecutiveZeroToolCallTodos >= ABORT_AFTER_CONSECUTIVE_ZERO) {
|
|
408
|
+
state.stage = "failed";
|
|
409
|
+
state.error = `Model failed ${consecutiveZeroToolCallTodos} consecutive todos with no progress. ${state.tool_capable
|
|
410
|
+
? "Try MODEL_SUPPORTS_TOOLS=false to enable JSON-manifest fallback."
|
|
411
|
+
: "The model cannot produce structured output for this task. Try a larger or more instruction-tuned model."}`;
|
|
412
|
+
emit(input.session_id, "session.warning", { message: state.error });
|
|
413
|
+
return state;
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
// Hit max iterations — bail out
|
|
417
|
+
if (state.todos.some((t) => t.status === "pending" || t.status === "in_progress")) {
|
|
418
|
+
state.stage = "failed";
|
|
419
|
+
state.error = `Hit iteration limit (${state.max_iterations}) with todos still pending.`;
|
|
420
|
+
}
|
|
421
|
+
else {
|
|
422
|
+
state.stage = "verify";
|
|
423
|
+
}
|
|
424
|
+
return state;
|
|
425
|
+
}
|
|
426
|
+
async function countToolCallsSince(session_id, since_message_id) {
|
|
427
|
+
try {
|
|
428
|
+
const r = await db_1.pool.query(`SELECT COUNT(*)::int AS n FROM tool_calls
|
|
429
|
+
WHERE session_id = $1
|
|
430
|
+
AND created_at >= (SELECT created_at FROM messages WHERE id = $2)`, [session_id, since_message_id]);
|
|
431
|
+
return r.rows[0]?.n || 0;
|
|
432
|
+
}
|
|
433
|
+
catch {
|
|
434
|
+
return 0;
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
// ─── Tool-capability probe & JSON-manifest fallback ───────────────────────────
|
|
438
|
+
/**
|
|
439
|
+
* Detect whether the model can produce a structured tool call. Honors the
|
|
440
|
+
* MODEL_SUPPORTS_TOOLS env override so users with known-good models skip the
|
|
441
|
+
* probe entirely. Otherwise, runs a tiny smoke test: ask the model to write a
|
|
442
|
+
* trivial file via the `write` tool and check whether tool_calls were recorded.
|
|
443
|
+
*
|
|
444
|
+
* Result is cached on the BuildState so we only probe once per build.
|
|
445
|
+
*/
|
|
446
|
+
async function probeToolCapability(input) {
|
|
447
|
+
if (process.env.MODEL_SUPPORTS_TOOLS === "true")
|
|
448
|
+
return true;
|
|
449
|
+
if (process.env.MODEL_SUPPORTS_TOOLS === "false")
|
|
450
|
+
return false;
|
|
451
|
+
// Heuristic: known-good model families
|
|
452
|
+
const id = input.model_id.toLowerCase();
|
|
453
|
+
if (id.includes("gpt-4") ||
|
|
454
|
+
id.includes("gpt-5") ||
|
|
455
|
+
id.includes("claude") ||
|
|
456
|
+
id.includes("gemini-1.5") ||
|
|
457
|
+
id.includes("gemini-2") ||
|
|
458
|
+
id.includes("gemini-3")) {
|
|
459
|
+
return true;
|
|
460
|
+
}
|
|
461
|
+
// For everything else, probe live. Run a one-shot agent call asking for a
|
|
462
|
+
// single trivial tool invocation and see if the DB records it.
|
|
463
|
+
try {
|
|
464
|
+
const probeMsgId = (0, uuid_1.v4)();
|
|
465
|
+
await db_1.pool.query(`INSERT INTO messages (id, session_id, role) VALUES ($1, $2, 'user')`, [probeMsgId, input.session_id]);
|
|
466
|
+
const probePartId = (0, uuid_1.v4)();
|
|
467
|
+
const probePrompt = "PROBE: Write a single line of text 'probe' to a file at .bonecode-probe using the write tool. Do not respond with prose. Call the write tool exactly once.";
|
|
468
|
+
await db_1.pool.query(`INSERT INTO parts (id, message_id, session_id, part_type, data, order_index) VALUES ($1, $2, $3, 'text', $4, 0)`, [probePartId, probeMsgId, input.session_id, JSON.stringify({ text: probePrompt, synthetic: true })]);
|
|
469
|
+
await (0, prompt_1.runAgentLoop)({
|
|
470
|
+
session_id: input.session_id,
|
|
471
|
+
message_id: probeMsgId,
|
|
472
|
+
content: probePrompt,
|
|
473
|
+
model_id: input.model_id,
|
|
474
|
+
provider_id: input.provider_id,
|
|
475
|
+
agent_name: "build",
|
|
476
|
+
});
|
|
477
|
+
const calls = await countToolCallsSince(input.session_id, probeMsgId);
|
|
478
|
+
return calls > 0;
|
|
479
|
+
}
|
|
480
|
+
catch {
|
|
481
|
+
return false;
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
/**
|
|
485
|
+
* JSON-manifest fallback for models that can't emit tool calls.
|
|
486
|
+
*
|
|
487
|
+
* Asks the model for a manifest:
|
|
488
|
+
* { "files": [{ "path": "...", "content": "..." }], "commands": ["..."] }
|
|
489
|
+
*
|
|
490
|
+
* Then applies it directly via fs/exec. The model never has to format an
|
|
491
|
+
* OpenAI-style tool call — it just produces a structured JSON document, which
|
|
492
|
+
* smaller/abliterated models handle much better.
|
|
493
|
+
*/
|
|
494
|
+
async function executeFallback(state, todo, input) {
|
|
495
|
+
const fs = require("fs/promises");
|
|
496
|
+
const path = require("path");
|
|
497
|
+
const { execSync } = require("child_process");
|
|
498
|
+
// Resolve worktree from the session
|
|
499
|
+
const sessionRow = await db_1.pool.query(`SELECT s.directory, p.worktree FROM sessions s
|
|
500
|
+
LEFT JOIN projects p ON p.id = s.project_id
|
|
501
|
+
WHERE s.id = $1`, [input.session_id]);
|
|
502
|
+
const worktree = sessionRow.rows[0]?.directory || sessionRow.rows[0]?.worktree || process.cwd();
|
|
503
|
+
// Build a focused prompt with the design context so the model knows what
|
|
504
|
+
// it's contributing to.
|
|
505
|
+
const designContext = state.design
|
|
506
|
+
? [
|
|
507
|
+
`Design goal: ${state.design.goal}`,
|
|
508
|
+
`Constraints: ${state.design.constraints.join("; ") || "(none)"}`,
|
|
509
|
+
`Expected artifacts: ${state.design.artifacts.join(", ") || "(unspecified)"}`,
|
|
510
|
+
].join("\n")
|
|
511
|
+
: "";
|
|
512
|
+
const completedFiles = state.todos
|
|
513
|
+
.filter((t) => t.status === "completed" && t.evidence)
|
|
514
|
+
.map((t) => `- ${t.title}`)
|
|
515
|
+
.join("\n");
|
|
516
|
+
const result = await askJson({
|
|
517
|
+
model_id: input.model_id,
|
|
518
|
+
provider_id: input.provider_id,
|
|
519
|
+
system: [
|
|
520
|
+
"You are completing one task in a project build. Produce a JSON manifest of the files to create or update and shell commands to run for THIS task only.",
|
|
521
|
+
"",
|
|
522
|
+
"RULES:",
|
|
523
|
+
"- Output a single JSON object: { \"files\": [...], \"commands\": [...] }",
|
|
524
|
+
"- Each file must have a relative `path` and full `content`. Do not abbreviate file content.",
|
|
525
|
+
"- File paths must be relative to the project root (no leading slash, no '..').",
|
|
526
|
+
"- Commands run in the project root. Use them only for compilation, package install, or migrations.",
|
|
527
|
+
"- Do not include explanatory prose. The JSON IS the entire response.",
|
|
528
|
+
].join("\n"),
|
|
529
|
+
user: [
|
|
530
|
+
`<design>`,
|
|
531
|
+
designContext,
|
|
532
|
+
`</design>`,
|
|
533
|
+
``,
|
|
534
|
+
`<completed-tasks>`,
|
|
535
|
+
completedFiles || "(none yet)",
|
|
536
|
+
`</completed-tasks>`,
|
|
537
|
+
``,
|
|
538
|
+
`<current-task>`,
|
|
539
|
+
`Title: ${todo.title}`,
|
|
540
|
+
`Description: ${todo.description}`,
|
|
541
|
+
`</current-task>`,
|
|
542
|
+
].join("\n"),
|
|
543
|
+
schema_hint: `{ "files": [{ "path": string, "content": string }], "commands": string[] }`,
|
|
544
|
+
});
|
|
545
|
+
if (!result || (!Array.isArray(result.files) && !Array.isArray(result.commands))) {
|
|
546
|
+
return { ok: false, filesWritten: 0, error: "Model did not produce a valid manifest" };
|
|
547
|
+
}
|
|
548
|
+
let filesWritten = 0;
|
|
549
|
+
const errors = [];
|
|
550
|
+
// Write files
|
|
551
|
+
for (const f of result.files ?? []) {
|
|
552
|
+
if (!f || typeof f.path !== "string" || typeof f.content !== "string")
|
|
553
|
+
continue;
|
|
554
|
+
// Sanity: no traversal, no absolute paths
|
|
555
|
+
if (f.path.includes("..") || path.isAbsolute(f.path)) {
|
|
556
|
+
errors.push(`refused unsafe path: ${f.path}`);
|
|
557
|
+
continue;
|
|
558
|
+
}
|
|
559
|
+
const target = path.resolve(worktree, f.path);
|
|
560
|
+
if (!target.startsWith(path.resolve(worktree))) {
|
|
561
|
+
errors.push(`refused path outside worktree: ${f.path}`);
|
|
562
|
+
continue;
|
|
563
|
+
}
|
|
564
|
+
try {
|
|
565
|
+
await fs.mkdir(path.dirname(target), { recursive: true });
|
|
566
|
+
await fs.writeFile(target, f.content, "utf-8");
|
|
567
|
+
filesWritten++;
|
|
568
|
+
// Surface a tool.completed event so the TUI shows an Edit/Write line
|
|
569
|
+
const callId = `fallback-${(0, uuid_1.v4)()}`;
|
|
570
|
+
const broadcastModule = await Promise.resolve().then(() => __importStar(require("../../../bone/output/session/src/websocket")));
|
|
571
|
+
broadcastModule.broadcastToChannel("part_stream", {
|
|
572
|
+
type: "tool.requested",
|
|
573
|
+
session_id: input.session_id,
|
|
574
|
+
tool_call_id: callId,
|
|
575
|
+
tool_name: "write",
|
|
576
|
+
tool_input: { path: f.path, content: f.content.slice(0, 200) },
|
|
577
|
+
});
|
|
578
|
+
broadcastModule.broadcastToChannel("part_stream", {
|
|
579
|
+
type: "tool.completed",
|
|
580
|
+
session_id: input.session_id,
|
|
581
|
+
tool_call_id: callId,
|
|
582
|
+
tool_name: "write",
|
|
583
|
+
tool_input: { path: f.path },
|
|
584
|
+
duration_ms: 0,
|
|
585
|
+
});
|
|
586
|
+
}
|
|
587
|
+
catch (e) {
|
|
588
|
+
errors.push(`${f.path}: ${e.message}`);
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
// Run commands
|
|
592
|
+
for (const cmd of result.commands ?? []) {
|
|
593
|
+
if (typeof cmd !== "string" || !cmd.trim())
|
|
594
|
+
continue;
|
|
595
|
+
try {
|
|
596
|
+
execSync(cmd, { cwd: worktree, stdio: "pipe", timeout: 60000 });
|
|
597
|
+
const callId = `fallback-${(0, uuid_1.v4)()}`;
|
|
598
|
+
const broadcastModule = await Promise.resolve().then(() => __importStar(require("../../../bone/output/session/src/websocket")));
|
|
599
|
+
broadcastModule.broadcastToChannel("part_stream", {
|
|
600
|
+
type: "tool.requested",
|
|
601
|
+
session_id: input.session_id,
|
|
602
|
+
tool_call_id: callId,
|
|
603
|
+
tool_name: "bash",
|
|
604
|
+
tool_input: { command: cmd },
|
|
605
|
+
});
|
|
606
|
+
broadcastModule.broadcastToChannel("part_stream", {
|
|
607
|
+
type: "tool.completed",
|
|
608
|
+
session_id: input.session_id,
|
|
609
|
+
tool_call_id: callId,
|
|
610
|
+
tool_name: "bash",
|
|
611
|
+
tool_input: { command: cmd },
|
|
612
|
+
duration_ms: 0,
|
|
613
|
+
});
|
|
614
|
+
}
|
|
615
|
+
catch (e) {
|
|
616
|
+
errors.push(`command failed: ${cmd} → ${e.message}`);
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
return {
|
|
620
|
+
ok: filesWritten > 0 || (result.commands?.length ?? 0) > 0,
|
|
621
|
+
filesWritten,
|
|
622
|
+
error: errors.length ? errors.join("; ") : undefined,
|
|
623
|
+
};
|
|
624
|
+
}
|
|
625
|
+
// ─── Stage 4: Verify ──────────────────────────────────────────────────────────
|
|
626
|
+
async function stageVerify(state, input) {
|
|
627
|
+
if (!state.design) {
|
|
628
|
+
state.stage = "failed";
|
|
629
|
+
state.error = "No design to verify against.";
|
|
630
|
+
return state;
|
|
631
|
+
}
|
|
632
|
+
emit(input.session_id, "build.stage", { stage: "verify" });
|
|
633
|
+
const results = [];
|
|
634
|
+
for (const requirement of state.design.requirements) {
|
|
635
|
+
// Ask the model: is this requirement satisfied? Yes/no plus evidence.
|
|
636
|
+
const r = await askJson({
|
|
637
|
+
model_id: input.model_id,
|
|
638
|
+
provider_id: input.provider_id,
|
|
639
|
+
system: [
|
|
640
|
+
"You are auditing whether a single requirement has been satisfied by the project so far.",
|
|
641
|
+
"",
|
|
642
|
+
"RULES:",
|
|
643
|
+
"- Answer with a yes/no verdict and one-line evidence.",
|
|
644
|
+
"- Evidence should reference concrete files or behavior.",
|
|
645
|
+
"- If you cannot tell, set satisfied=false and explain in evidence what's missing.",
|
|
646
|
+
"- Do not assume anything not visible in the project.",
|
|
647
|
+
].join("\n"),
|
|
648
|
+
user: [
|
|
649
|
+
`Requirement: ${requirement}`,
|
|
650
|
+
``,
|
|
651
|
+
`Original goal: ${state.design.goal}`,
|
|
652
|
+
``,
|
|
653
|
+
`Expected artifacts:\n${state.design.artifacts.map((a) => `- ${a}`).join("\n")}`,
|
|
654
|
+
``,
|
|
655
|
+
`Completed work:\n${state.todos
|
|
656
|
+
.filter((t) => t.status === "completed")
|
|
657
|
+
.map((t) => `- ${t.title}`)
|
|
658
|
+
.join("\n") || "(none)"}`,
|
|
659
|
+
].join("\n"),
|
|
660
|
+
schema_hint: `{ "satisfied": boolean, "evidence": string }`,
|
|
661
|
+
});
|
|
662
|
+
const result = r
|
|
663
|
+
? { requirement, satisfied: r.satisfied, evidence: r.evidence }
|
|
664
|
+
: { requirement, satisfied: false, evidence: "Could not verify (no response)." };
|
|
665
|
+
results.push(result);
|
|
666
|
+
emit(input.session_id, "build.verify.item", { requirement, satisfied: result.satisfied, evidence: result.evidence });
|
|
667
|
+
}
|
|
668
|
+
state.verification_results = results;
|
|
669
|
+
const allOk = results.every((r) => r.satisfied);
|
|
670
|
+
if (allOk) {
|
|
671
|
+
state.stage = "done";
|
|
672
|
+
emit(input.session_id, "build.done", { verifications: results });
|
|
673
|
+
}
|
|
674
|
+
else {
|
|
675
|
+
// Re-plan only the unsatisfied requirements
|
|
676
|
+
const failures = results.filter((r) => !r.satisfied);
|
|
677
|
+
if (state.iteration >= state.max_iterations) {
|
|
678
|
+
state.stage = "failed";
|
|
679
|
+
state.error = `${failures.length} requirement(s) unsatisfied after ${state.max_iterations} iterations.`;
|
|
680
|
+
}
|
|
681
|
+
else {
|
|
682
|
+
// Generate new todos to address failures
|
|
683
|
+
const newPlan = await askJson({
|
|
684
|
+
model_id: input.model_id,
|
|
685
|
+
provider_id: input.provider_id,
|
|
686
|
+
system: "Some requirements are not yet satisfied. Produce a short todo list to fix them.",
|
|
687
|
+
user: [
|
|
688
|
+
`Unsatisfied requirements:`,
|
|
689
|
+
...failures.map((f) => `- ${f.requirement} (missing: ${f.evidence})`),
|
|
690
|
+
].join("\n"),
|
|
691
|
+
schema_hint: `{ "todos": [{ "title": string, "description": string }] }`,
|
|
692
|
+
});
|
|
693
|
+
if (newPlan && Array.isArray(newPlan.todos) && newPlan.todos.length > 0) {
|
|
694
|
+
for (const t of newPlan.todos) {
|
|
695
|
+
state.todos.push({
|
|
696
|
+
id: (0, uuid_1.v4)(),
|
|
697
|
+
title: t.title,
|
|
698
|
+
description: t.description,
|
|
699
|
+
status: "pending",
|
|
700
|
+
failure_count: 0,
|
|
701
|
+
});
|
|
702
|
+
}
|
|
703
|
+
state.stage = "execute";
|
|
704
|
+
emit(input.session_id, "build.replan", { added: newPlan.todos.length });
|
|
705
|
+
}
|
|
706
|
+
else {
|
|
707
|
+
state.stage = "failed";
|
|
708
|
+
state.error = `Cannot generate fix-up tasks for ${failures.length} unsatisfied requirement(s).`;
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
return state;
|
|
713
|
+
}
|
|
714
|
+
// ─── Driver ───────────────────────────────────────────────────────────────────
|
|
715
|
+
async function runBuildMode(input) {
|
|
716
|
+
let state = (await loadState(input.session_id)) ?? {
|
|
717
|
+
stage: "clarify",
|
|
718
|
+
original_prompt: input.prompt,
|
|
719
|
+
design: null,
|
|
720
|
+
todos: [],
|
|
721
|
+
iteration: 0,
|
|
722
|
+
max_iterations: 30,
|
|
723
|
+
};
|
|
724
|
+
// Resume from saved state if applicable. If the user is sending a new prompt
|
|
725
|
+
// and we're already in clarify with pending questions, treat the prompt as
|
|
726
|
+
// the answer and continue.
|
|
727
|
+
if (state.stage === "clarify" && state.pending_clarification && input.prompt !== state.original_prompt) {
|
|
728
|
+
const next = await continueAfterClarification(input.session_id, input.prompt, input.model_id, input.provider_id);
|
|
729
|
+
if (next)
|
|
730
|
+
state = next;
|
|
731
|
+
}
|
|
732
|
+
// Run stages until we hit a terminal state, a user prompt, or iteration cap.
|
|
733
|
+
// Each stage advances state.stage. We save after every stage transition.
|
|
734
|
+
let safety = 0;
|
|
735
|
+
while (state.stage !== "done" && state.stage !== "failed" && safety < 50) {
|
|
736
|
+
safety++;
|
|
737
|
+
const before = state.stage;
|
|
738
|
+
if (state.stage === "clarify") {
|
|
739
|
+
state = await stageClarify(state, input);
|
|
740
|
+
await saveState(input.session_id, state);
|
|
741
|
+
// If we asked the user questions, exit and wait for their answer.
|
|
742
|
+
if (state.pending_clarification)
|
|
743
|
+
break;
|
|
744
|
+
}
|
|
745
|
+
else if (state.stage === "plan") {
|
|
746
|
+
state = await stagePlan(state, input);
|
|
747
|
+
await saveState(input.session_id, state);
|
|
748
|
+
}
|
|
749
|
+
else if (state.stage === "execute") {
|
|
750
|
+
state = await stageExecute(state, input);
|
|
751
|
+
await saveState(input.session_id, state);
|
|
752
|
+
}
|
|
753
|
+
else if (state.stage === "verify") {
|
|
754
|
+
state = await stageVerify(state, input);
|
|
755
|
+
await saveState(input.session_id, state);
|
|
756
|
+
}
|
|
757
|
+
if (state.stage === before)
|
|
758
|
+
break; // safety: no progress
|
|
759
|
+
}
|
|
760
|
+
return state;
|
|
761
|
+
}
|
|
762
|
+
exports.runBuildMode = runBuildMode;
|
|
763
|
+
// ─── Trigger detection ────────────────────────────────────────────────────────
|
|
764
|
+
/**
|
|
765
|
+
* Heuristic: should this prompt go through build mode rather than the
|
|
766
|
+
* regular agent loop? Build-mode prompts are project-scoped — "build me",
|
|
767
|
+
* "create a", "design and implement", "make a full" — vs ad-hoc questions.
|
|
768
|
+
*/
|
|
769
|
+
function isBuildPrompt(prompt) {
|
|
770
|
+
const p = prompt.toLowerCase().trim();
|
|
771
|
+
if (p.length < 20)
|
|
772
|
+
return false;
|
|
773
|
+
const triggers = [
|
|
774
|
+
/\bbuild\s+(me|a|an|the)\b/,
|
|
775
|
+
/\bcreate\s+(a|an|the|me)\s+(?:full|complete|whole|new)\b/,
|
|
776
|
+
/\bcreate\s+(?:a|an|the)\b.*\bfrom\s+scratch\b/,
|
|
777
|
+
/\bdesign\s+and\s+(?:implement|build|create)\b/,
|
|
778
|
+
/\bimplement\s+(?:a|an|the)\s+(?:full|complete|whole)\b/,
|
|
779
|
+
/\bmake\s+(?:a|an|the)\s+(?:full|complete|whole|new)\b/,
|
|
780
|
+
/\bproject\s+(?:from\s+scratch|to)\b/,
|
|
781
|
+
/\bsimulation\s+(?:with|using|of)\b/,
|
|
782
|
+
/\bbackend\s+(?:for|with|using)\b/,
|
|
783
|
+
/\bspec(?:ification)?\s+(?:for|of)\b/,
|
|
784
|
+
/\bend[- ]to[- ]end\b/,
|
|
785
|
+
];
|
|
786
|
+
return triggers.some((re) => re.test(p));
|
|
787
|
+
}
|
|
788
|
+
exports.isBuildPrompt = isBuildPrompt;
|
|
789
|
+
//# sourceMappingURL=build_mode.js.map
|