astrocode-workflow 0.1.59 → 0.3.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/dist/state/db.d.ts +1 -1
- package/dist/state/db.js +62 -4
- package/dist/tools/injects.js +90 -26
- package/dist/tools/workflow.d.ts +1 -1
- package/dist/tools/workflow.js +114 -94
- package/dist/ui/inject.d.ts +1 -1
- package/dist/ui/inject.js +74 -33
- package/dist/workflow/state-machine.d.ts +33 -17
- package/dist/workflow/state-machine.js +116 -40
- package/package.json +1 -1
- package/src/state/db.ts +63 -4
- package/src/tools/injects.ts +147 -53
- package/src/tools/workflow.ts +155 -136
- package/src/tools/workflow.ts.backup +681 -0
- package/src/ui/inject.ts +86 -41
- package/src/workflow/state-machine.ts +161 -67
package/dist/tools/workflow.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
+
// src/tools/workflow.ts
|
|
1
2
|
import { tool } from "@opencode-ai/plugin/tool";
|
|
2
|
-
import { withTx } from "../state/db";
|
|
3
3
|
import { buildContextSnapshot } from "../workflow/context";
|
|
4
4
|
import { decideNextAction, createRunForStory, startStage, completeRun, getActiveRun, EVENT_TYPES } from "../workflow/state-machine";
|
|
5
5
|
import { buildStageDirective, directiveHash } from "../workflow/directives";
|
|
@@ -31,24 +31,20 @@ export function resolveAgentName(stageKey, config, agents, warnings) {
|
|
|
31
31
|
// Validate that the agent actually exists in the registry
|
|
32
32
|
if (agents && !agents[candidate]) {
|
|
33
33
|
const warning = `Agent "${candidate}" not found in registry for stage "${stageKey}". Falling back to General.`;
|
|
34
|
-
if (warnings)
|
|
34
|
+
if (warnings)
|
|
35
35
|
warnings.push(warning);
|
|
36
|
-
|
|
37
|
-
else {
|
|
36
|
+
else
|
|
38
37
|
console.warn(`[Astrocode] ${warning}`);
|
|
39
|
-
}
|
|
40
38
|
candidate = "General";
|
|
41
39
|
}
|
|
42
40
|
// Final guard: ensure General exists, fallback to built-in "general" if not
|
|
43
41
|
if (agents && !agents[candidate]) {
|
|
44
42
|
const finalWarning = `Critical: General agent not found in registry. Falling back to built-in "general".`;
|
|
45
|
-
if (warnings)
|
|
43
|
+
if (warnings)
|
|
46
44
|
warnings.push(finalWarning);
|
|
47
|
-
|
|
48
|
-
else {
|
|
45
|
+
else
|
|
49
46
|
console.warn(`[Astrocode] ${finalWarning}`);
|
|
50
|
-
|
|
51
|
-
return "general"; // built-in, guaranteed by OpenCode
|
|
47
|
+
return "general";
|
|
52
48
|
}
|
|
53
49
|
return candidate;
|
|
54
50
|
}
|
|
@@ -84,9 +80,6 @@ function stageConstraints(stage, cfg) {
|
|
|
84
80
|
}
|
|
85
81
|
return common;
|
|
86
82
|
}
|
|
87
|
-
function agentNameForStage(stage, cfg) {
|
|
88
|
-
return cfg.agents.stage_agent_names[stage];
|
|
89
|
-
}
|
|
90
83
|
function buildDelegationPrompt(opts) {
|
|
91
84
|
const { stageDirective, run_id, stage_key, stage_agent_name } = opts;
|
|
92
85
|
const stageUpper = stage_key.toUpperCase();
|
|
@@ -108,7 +101,6 @@ function buildDelegationPrompt(opts) {
|
|
|
108
101
|
``,
|
|
109
102
|
`Important: do NOT do any stage work yourself in orchestrator mode.`,
|
|
110
103
|
].join("\n").trim();
|
|
111
|
-
// Debug log the delegation prompt to troubleshoot agent output issues
|
|
112
104
|
debug(`Delegating stage ${stage_key} to agent ${stage_agent_name}`, { prompt_length: prompt.length });
|
|
113
105
|
return prompt;
|
|
114
106
|
}
|
|
@@ -134,59 +126,54 @@ export function createAstroWorkflowProceedTool(opts) {
|
|
|
134
126
|
break;
|
|
135
127
|
}
|
|
136
128
|
if (next.kind === "start_run") {
|
|
137
|
-
|
|
129
|
+
// NOTE: createRunForStory owns its own tx (state-machine.ts).
|
|
130
|
+
const { run_id } = createRunForStory(db, config, next.story_key);
|
|
138
131
|
actions.push(`started run ${run_id} for story ${next.story_key}`);
|
|
139
132
|
if (config.ui.toasts.enabled && config.ui.toasts.show_run_started) {
|
|
140
133
|
await toasts.show({ title: "Astrocode", message: `Run started (${run_id})`, variant: "success" });
|
|
141
134
|
}
|
|
135
|
+
if (sessionId) {
|
|
136
|
+
await injectChatPrompt({
|
|
137
|
+
ctx,
|
|
138
|
+
sessionId,
|
|
139
|
+
agent: "Astro",
|
|
140
|
+
text: [
|
|
141
|
+
`[SYSTEM DIRECTIVE: ASTROCODE — RUN_STARTED]`,
|
|
142
|
+
``,
|
|
143
|
+
`Run started: \`${run_id}\``,
|
|
144
|
+
`Story: \`${next.story_key}\``,
|
|
145
|
+
``,
|
|
146
|
+
`Next: call **astro_workflow_proceed** again to delegate the first stage.`,
|
|
147
|
+
].join("\n"),
|
|
148
|
+
});
|
|
149
|
+
actions.push(`injected run started message for ${run_id}`);
|
|
150
|
+
}
|
|
142
151
|
if (mode === "step")
|
|
143
152
|
break;
|
|
144
153
|
continue;
|
|
145
154
|
}
|
|
146
155
|
if (next.kind === "complete_run") {
|
|
147
|
-
|
|
156
|
+
// NOTE: completeRun owns its own tx (state-machine.ts).
|
|
157
|
+
completeRun(db, next.run_id);
|
|
148
158
|
actions.push(`completed run ${next.run_id}`);
|
|
149
159
|
if (config.ui.toasts.enabled && config.ui.toasts.show_run_completed) {
|
|
150
160
|
await toasts.show({ title: "Astrocode", message: `Run completed (${next.run_id})`, variant: "success" });
|
|
151
161
|
}
|
|
152
|
-
//
|
|
162
|
+
// ✅ explicit injection on completeRun (requested)
|
|
153
163
|
if (sessionId) {
|
|
154
|
-
const continueDirective = [
|
|
155
|
-
`[SYSTEM DIRECTIVE: ASTROCODE — CONTINUE]`,
|
|
156
|
-
``,
|
|
157
|
-
`Run ${next.run_id} completed successfully.`,
|
|
158
|
-
``,
|
|
159
|
-
`The Clara Forms implementation run has finished all stages. The spec has been analyzed and decomposed into prioritized implementation stories.`,
|
|
160
|
-
``,
|
|
161
|
-
`Next actions: Review the generated stories and approve the next one to continue development.`,
|
|
162
|
-
].join("\n");
|
|
163
|
-
await injectChatPrompt({
|
|
164
|
-
ctx,
|
|
165
|
-
sessionId,
|
|
166
|
-
text: continueDirective,
|
|
167
|
-
agent: "Astro"
|
|
168
|
-
});
|
|
169
|
-
actions.push(`injected continuation directive for completed run ${next.run_id}`);
|
|
170
|
-
}
|
|
171
|
-
// Check for next approved story to start
|
|
172
|
-
const nextStory = db.prepare("SELECT story_key, title FROM stories WHERE state = 'approved' ORDER BY priority DESC, created_at ASC LIMIT 1").get();
|
|
173
|
-
if (nextStory && sessionId) {
|
|
174
|
-
const nextDirective = [
|
|
175
|
-
`[SYSTEM DIRECTIVE: ASTROCODE — START_NEXT_STORY]`,
|
|
176
|
-
``,
|
|
177
|
-
`The previous run completed successfully. Start the next approved story.`,
|
|
178
|
-
``,
|
|
179
|
-
`Next Story: ${nextStory.story_key} — ${nextStory.title}`,
|
|
180
|
-
``,
|
|
181
|
-
`Action: Call astro_story_approve with story_key="${nextStory.story_key}" to start it, or select a different story.`,
|
|
182
|
-
].join("\n");
|
|
183
164
|
await injectChatPrompt({
|
|
184
165
|
ctx,
|
|
185
166
|
sessionId,
|
|
186
|
-
|
|
187
|
-
|
|
167
|
+
agent: "Astro",
|
|
168
|
+
text: [
|
|
169
|
+
`[SYSTEM DIRECTIVE: ASTROCODE — RUN_COMPLETED]`,
|
|
170
|
+
``,
|
|
171
|
+
`Run \`${next.run_id}\` completed.`,
|
|
172
|
+
``,
|
|
173
|
+
`Next: call **astro_workflow_proceed** (mode=step) to start the next approved story (if any).`,
|
|
174
|
+
].join("\n"),
|
|
188
175
|
});
|
|
189
|
-
actions.push(`injected
|
|
176
|
+
actions.push(`injected run completed message for ${next.run_id}`);
|
|
190
177
|
}
|
|
191
178
|
if (mode === "step")
|
|
192
179
|
break;
|
|
@@ -198,53 +185,55 @@ export function createAstroWorkflowProceedTool(opts) {
|
|
|
198
185
|
throw new Error("Invariant: delegate_stage but no active run.");
|
|
199
186
|
const run = db.prepare("SELECT * FROM runs WHERE run_id=?").get(active.run_id);
|
|
200
187
|
const story = db.prepare("SELECT * FROM stories WHERE story_key=?").get(run.story_key);
|
|
201
|
-
// Mark stage started + set subagent_type to the stage agent.
|
|
202
188
|
let agentName = resolveAgentName(next.stage_key, config, agents, warnings);
|
|
203
|
-
// Validate agent availability with fallback chain
|
|
204
|
-
const systemConfig = config;
|
|
205
|
-
// Check both the system config agent map (if present) OR the local agents map passed to the tool
|
|
206
189
|
const agentExists = (name) => {
|
|
207
|
-
|
|
208
|
-
if (agents && agents[name]) {
|
|
190
|
+
if (agents && agents[name])
|
|
209
191
|
return true;
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
if (systemConfig.agent && systemConfig.agent[name]) {
|
|
213
|
-
return true;
|
|
214
|
-
}
|
|
215
|
-
// For known stage agents, assume they exist (they are system-provided subagents)
|
|
216
|
-
const knownStageAgents = ["Frame", "Plan", "Spec", "Implement", "Review", "Verify", "Close"];
|
|
217
|
-
if (knownStageAgents.includes(name)) {
|
|
192
|
+
const knownStageAgents = ["Frame", "Plan", "Spec", "Implement", "Review", "Verify", "Close", "General", "Astro", "general"];
|
|
193
|
+
if (knownStageAgents.includes(name))
|
|
218
194
|
return true;
|
|
219
|
-
}
|
|
220
195
|
return false;
|
|
221
196
|
};
|
|
222
197
|
if (!agentExists(agentName)) {
|
|
223
198
|
const originalAgent = agentName;
|
|
224
|
-
console.warn(`[Astrocode] Agent ${agentName} not found
|
|
225
|
-
// First fallback: orchestrator
|
|
199
|
+
console.warn(`[Astrocode] Agent ${agentName} not found. Falling back to orchestrator.`);
|
|
226
200
|
agentName = config.agents?.orchestrator_name || "Astro";
|
|
227
201
|
if (!agentExists(agentName)) {
|
|
228
202
|
console.warn(`[Astrocode] Orchestrator ${agentName} not available. Falling back to General.`);
|
|
229
|
-
// Final fallback: General (guaranteed to exist)
|
|
230
203
|
agentName = "General";
|
|
231
204
|
if (!agentExists(agentName)) {
|
|
232
205
|
throw new Error(`Critical: No agents available for delegation. Primary: ${originalAgent}, Orchestrator: ${config.agents?.orchestrator_name || "Astro"}, General: unavailable`);
|
|
233
206
|
}
|
|
234
207
|
}
|
|
235
208
|
}
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
if (config.debug?.telemetry?.enabled) {
|
|
240
|
-
// eslint-disable-next-line no-console
|
|
241
|
-
console.log(`[Astrocode:delegate] run_id=${active.run_id} stage=${next.stage_key} agent=${agentName} fallback=${agentName !== resolveAgentName(next.stage_key, config, agents) ? 'yes' : 'no'}`);
|
|
242
|
-
}
|
|
243
|
-
});
|
|
209
|
+
// NOTE: startStage owns its own tx (state-machine.ts).
|
|
210
|
+
startStage(db, active.run_id, next.stage_key, { subagent_type: agentName });
|
|
211
|
+
actions.push(`stage started: ${next.stage_key}`);
|
|
244
212
|
if (config.ui.toasts.enabled && config.ui.toasts.show_stage_started) {
|
|
245
213
|
await toasts.show({ title: "Astrocode", message: `Stage started: ${next.stage_key}`, variant: "info" });
|
|
246
214
|
}
|
|
247
|
-
|
|
215
|
+
// ✅ explicit injection on startStage (requested)
|
|
216
|
+
if (sessionId) {
|
|
217
|
+
await injectChatPrompt({
|
|
218
|
+
ctx,
|
|
219
|
+
sessionId,
|
|
220
|
+
agent: "Astro",
|
|
221
|
+
text: [
|
|
222
|
+
`[SYSTEM DIRECTIVE: ASTROCODE — STAGE_STARTED]`,
|
|
223
|
+
``,
|
|
224
|
+
`Run: \`${active.run_id}\``,
|
|
225
|
+
`Stage: \`${next.stage_key}\``,
|
|
226
|
+
`Delegated to: \`${agentName}\``,
|
|
227
|
+
].join("\n"),
|
|
228
|
+
});
|
|
229
|
+
actions.push(`injected stage started message for ${next.stage_key}`);
|
|
230
|
+
}
|
|
231
|
+
const context = buildContextSnapshot({
|
|
232
|
+
db,
|
|
233
|
+
config,
|
|
234
|
+
run_id: active.run_id,
|
|
235
|
+
next_action: `delegate stage ${next.stage_key}`,
|
|
236
|
+
});
|
|
248
237
|
const stageDirective = buildStageDirective({
|
|
249
238
|
config,
|
|
250
239
|
stage_key: next.stage_key,
|
|
@@ -262,25 +251,29 @@ export function createAstroWorkflowProceedTool(opts) {
|
|
|
262
251
|
stage_key: next.stage_key,
|
|
263
252
|
stage_agent_name: agentName,
|
|
264
253
|
});
|
|
265
|
-
//
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
254
|
+
// Best-effort: continuations table may not exist on older DBs.
|
|
255
|
+
try {
|
|
256
|
+
const h = directiveHash(delegatePrompt);
|
|
257
|
+
const now = nowISO();
|
|
258
|
+
if (sessionId) {
|
|
259
|
+
db.prepare("INSERT INTO continuations (session_id, run_id, directive_hash, kind, reason, created_at) VALUES (?, ?, ?, 'stage', ?, ?)").run(sessionId, active.run_id, h, `delegate ${next.stage_key}`, now);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
catch (e) {
|
|
263
|
+
warnings.push(`continuations insert failed (non-fatal): ${String(e)}`);
|
|
270
264
|
}
|
|
271
265
|
// Visible injection so user can see state
|
|
272
266
|
if (sessionId) {
|
|
273
267
|
await injectChatPrompt({ ctx, sessionId, text: delegatePrompt, agent: "Astro" });
|
|
274
|
-
// Inject continuation guidance
|
|
275
268
|
const continueMessage = [
|
|
276
269
|
`[SYSTEM DIRECTIVE: ASTROCODE — AWAIT_STAGE_COMPLETION]`,
|
|
277
270
|
``,
|
|
278
|
-
`Stage
|
|
271
|
+
`Stage \`${next.stage_key}\` delegated to \`${agentName}\`.`,
|
|
279
272
|
``,
|
|
280
|
-
`When
|
|
273
|
+
`When \`${agentName}\` completes, call:`,
|
|
281
274
|
`astro_stage_complete(run_id="${active.run_id}", stage_key="${next.stage_key}", output_text="[paste subagent output here]")`,
|
|
282
275
|
``,
|
|
283
|
-
`
|
|
276
|
+
`Then run **astro_workflow_proceed** again.`,
|
|
284
277
|
].join("\n");
|
|
285
278
|
await injectChatPrompt({ ctx, sessionId, text: continueMessage, agent: "Astro" });
|
|
286
279
|
}
|
|
@@ -290,35 +283,62 @@ export function createAstroWorkflowProceedTool(opts) {
|
|
|
290
283
|
}
|
|
291
284
|
if (next.kind === "await_stage_completion") {
|
|
292
285
|
actions.push(`await stage completion: ${next.stage_key}`);
|
|
293
|
-
// Optionally nudge with a short directive
|
|
294
286
|
if (sessionId) {
|
|
295
|
-
const context = buildContextSnapshot({
|
|
287
|
+
const context = buildContextSnapshot({
|
|
288
|
+
db,
|
|
289
|
+
config,
|
|
290
|
+
run_id: next.run_id,
|
|
291
|
+
next_action: `complete stage ${next.stage_key}`,
|
|
292
|
+
});
|
|
296
293
|
const prompt = [
|
|
297
294
|
`[SYSTEM DIRECTIVE: ASTROCODE — AWAIT_STAGE_OUTPUT]`,
|
|
298
295
|
``,
|
|
299
|
-
`Run
|
|
296
|
+
`Run \`${next.run_id}\` is waiting for stage \`${next.stage_key}\` output.`,
|
|
300
297
|
`If you have the subagent output, call astro_stage_complete with output_text=the FULL output.`,
|
|
301
298
|
``,
|
|
302
299
|
`Context snapshot:`,
|
|
303
300
|
context,
|
|
304
301
|
].join("\n").trim();
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
302
|
+
try {
|
|
303
|
+
const h = directiveHash(prompt);
|
|
304
|
+
const now = nowISO();
|
|
305
|
+
db.prepare("INSERT INTO continuations (session_id, run_id, directive_hash, kind, reason, created_at) VALUES (?, ?, ?, 'continue', ?, ?)").run(sessionId, next.run_id, h, `await ${next.stage_key}`, now);
|
|
306
|
+
}
|
|
307
|
+
catch (e) {
|
|
308
|
+
warnings.push(`continuations insert failed (non-fatal): ${String(e)}`);
|
|
309
|
+
}
|
|
308
310
|
await injectChatPrompt({ ctx, sessionId, text: prompt, agent: "Astro" });
|
|
309
311
|
}
|
|
310
312
|
break;
|
|
311
313
|
}
|
|
312
314
|
if (next.kind === "failed") {
|
|
313
315
|
actions.push(`failed: ${next.stage_key} — ${next.error_text}`);
|
|
316
|
+
if (sessionId) {
|
|
317
|
+
await injectChatPrompt({
|
|
318
|
+
ctx,
|
|
319
|
+
sessionId,
|
|
320
|
+
agent: "Astro",
|
|
321
|
+
text: [
|
|
322
|
+
`[SYSTEM DIRECTIVE: ASTROCODE — RUN_FAILED]`,
|
|
323
|
+
``,
|
|
324
|
+
`Run \`${next.run_id}\` failed at stage \`${next.stage_key}\`.`,
|
|
325
|
+
`Error: ${next.error_text}`,
|
|
326
|
+
].join("\n"),
|
|
327
|
+
});
|
|
328
|
+
actions.push(`injected run failed message for ${next.run_id}`);
|
|
329
|
+
}
|
|
314
330
|
break;
|
|
315
331
|
}
|
|
316
|
-
// safety
|
|
317
332
|
actions.push(`unhandled next action: ${next.kind}`);
|
|
318
333
|
break;
|
|
319
334
|
}
|
|
320
|
-
// Housekeeping event
|
|
321
|
-
|
|
335
|
+
// Housekeeping event (best-effort)
|
|
336
|
+
try {
|
|
337
|
+
db.prepare("INSERT INTO events (event_id, run_id, stage_key, type, body_json, created_at) VALUES (?, NULL, NULL, ?, ?, ?)").run(newEventId(), EVENT_TYPES.WORKFLOW_PROCEED, JSON.stringify({ started_at: startedAt, mode, max_steps: steps, actions }), nowISO());
|
|
338
|
+
}
|
|
339
|
+
catch (e) {
|
|
340
|
+
warnings.push(`workflow.proceed event insert failed (non-fatal): ${String(e)}`);
|
|
341
|
+
}
|
|
322
342
|
const active = getActiveRun(db);
|
|
323
343
|
const lines = [];
|
|
324
344
|
lines.push(`# astro_workflow_proceed`);
|
package/dist/ui/inject.d.ts
CHANGED
package/dist/ui/inject.js
CHANGED
|
@@ -1,47 +1,88 @@
|
|
|
1
|
+
// src/ui/inject.ts
|
|
1
2
|
let isInjecting = false;
|
|
2
3
|
const injectionQueue = [];
|
|
4
|
+
function sleep(ms) {
|
|
5
|
+
return new Promise((r) => setTimeout(r, ms));
|
|
6
|
+
}
|
|
7
|
+
function resolveSessionId(ctx, sessionId) {
|
|
8
|
+
if (sessionId)
|
|
9
|
+
return sessionId;
|
|
10
|
+
const direct = ctx?.sessionID ?? ctx?.sessionId ?? ctx?.session?.id;
|
|
11
|
+
if (typeof direct === "string" && direct.length > 0)
|
|
12
|
+
return direct;
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
async function tryInjectOnce(opts) {
|
|
16
|
+
const { ctx, sessionId, text, agent } = opts;
|
|
17
|
+
// Prefer explicit chat prompt API
|
|
18
|
+
const promptApi = ctx?.client?.session?.prompt;
|
|
19
|
+
if (!promptApi) {
|
|
20
|
+
throw new Error("API not available (ctx.client.session.prompt)");
|
|
21
|
+
}
|
|
22
|
+
const prefixedText = `[${agent}]\n\n${text}`;
|
|
23
|
+
// Some hosts reject unknown fields; keep body minimal and stable.
|
|
24
|
+
await promptApi({
|
|
25
|
+
path: { id: sessionId },
|
|
26
|
+
body: {
|
|
27
|
+
parts: [{ type: "text", text: prefixedText }],
|
|
28
|
+
},
|
|
29
|
+
});
|
|
30
|
+
}
|
|
3
31
|
async function processQueue() {
|
|
4
|
-
if (isInjecting
|
|
32
|
+
if (isInjecting)
|
|
5
33
|
return;
|
|
6
|
-
|
|
7
|
-
const opts = injectionQueue.shift();
|
|
8
|
-
if (!opts) {
|
|
9
|
-
isInjecting = false;
|
|
34
|
+
if (injectionQueue.length === 0)
|
|
10
35
|
return;
|
|
11
|
-
|
|
36
|
+
isInjecting = true;
|
|
12
37
|
try {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
38
|
+
while (injectionQueue.length > 0) {
|
|
39
|
+
const item = injectionQueue.shift();
|
|
40
|
+
if (!item)
|
|
41
|
+
continue;
|
|
42
|
+
const { ctx, text, agent = "Astro" } = item;
|
|
43
|
+
const sessionId = resolveSessionId(ctx, item.sessionId);
|
|
44
|
+
if (!sessionId) {
|
|
45
|
+
// Drop on floor: we cannot recover without a session id.
|
|
46
|
+
// Keep draining the queue so we don't stall.
|
|
47
|
+
// eslint-disable-next-line no-console
|
|
48
|
+
console.warn("[Astrocode] Injection skipped: no sessionId");
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
const maxAttempts = item.retry?.maxAttempts ?? 4;
|
|
52
|
+
const baseDelayMs = item.retry?.baseDelayMs ?? 250;
|
|
53
|
+
let lastErr = null;
|
|
54
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
55
|
+
try {
|
|
56
|
+
await tryInjectOnce({ ctx, sessionId, text, agent });
|
|
57
|
+
lastErr = null;
|
|
58
|
+
break;
|
|
59
|
+
}
|
|
60
|
+
catch (e) {
|
|
61
|
+
lastErr = e;
|
|
62
|
+
const delay = baseDelayMs * Math.pow(2, attempt - 1); // 250, 500, 1000, 2000
|
|
63
|
+
// eslint-disable-next-line no-console
|
|
64
|
+
console.warn(`[Astrocode] Injection attempt ${attempt}/${maxAttempts} failed: ${String(e)}; retrying in ${delay}ms`);
|
|
65
|
+
await sleep(delay);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
if (lastErr) {
|
|
69
|
+
// eslint-disable-next-line no-console
|
|
70
|
+
console.warn(`[Astrocode] Injection failed permanently after ${maxAttempts} attempts: ${String(lastErr)}`);
|
|
71
|
+
}
|
|
23
72
|
}
|
|
24
|
-
await ctx.client.session.prompt({
|
|
25
|
-
path: { id: sessionId },
|
|
26
|
-
body: {
|
|
27
|
-
parts: [{ type: "text", text: prefixedText }],
|
|
28
|
-
// Pass agent context for systems that support it
|
|
29
|
-
agent: agent,
|
|
30
|
-
},
|
|
31
|
-
});
|
|
32
|
-
}
|
|
33
|
-
catch (error) {
|
|
34
|
-
console.warn(`[Astrocode] Injection failed: ${error}`);
|
|
35
73
|
}
|
|
36
74
|
finally {
|
|
37
75
|
isInjecting = false;
|
|
38
|
-
// Process next item immediately
|
|
39
|
-
if (injectionQueue.length > 0) {
|
|
40
|
-
setImmediate(processQueue);
|
|
41
|
-
}
|
|
42
76
|
}
|
|
43
77
|
}
|
|
44
78
|
export async function injectChatPrompt(opts) {
|
|
45
|
-
injectionQueue.push(
|
|
46
|
-
|
|
79
|
+
injectionQueue.push({
|
|
80
|
+
ctx: opts.ctx,
|
|
81
|
+
sessionId: opts.sessionId,
|
|
82
|
+
text: opts.text,
|
|
83
|
+
agent: opts.agent ?? "Astro",
|
|
84
|
+
retry: { maxAttempts: 4, baseDelayMs: 250 },
|
|
85
|
+
});
|
|
86
|
+
// Fire-and-forget; queue drain is serialized by isInjecting.
|
|
87
|
+
void processQueue();
|
|
47
88
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { AstrocodeConfig } from "../config/schema";
|
|
2
2
|
import type { SqliteDb } from "../state/db";
|
|
3
3
|
import type { RunRow, StageKey, StageRunRow, StoryRow } from "../state/types";
|
|
4
|
+
import type { ToastOptions } from "../ui/toasts";
|
|
4
5
|
export declare const EVENT_TYPES: {
|
|
5
6
|
readonly RUN_STARTED: "run.started";
|
|
6
7
|
readonly RUN_COMPLETED: "run.completed";
|
|
@@ -10,23 +11,28 @@ export declare const EVENT_TYPES: {
|
|
|
10
11
|
readonly STAGE_STARTED: "stage.started";
|
|
11
12
|
readonly WORKFLOW_PROCEED: "workflow.proceed";
|
|
12
13
|
};
|
|
14
|
+
/**
|
|
15
|
+
* UI HOOKS
|
|
16
|
+
* --------
|
|
17
|
+
* This workflow module is DB-first. UI emission is optional and happens AFTER the DB tx commits.
|
|
18
|
+
*
|
|
19
|
+
* Contract:
|
|
20
|
+
* - If you pass ui.ctx + ui.sessionId, we will inject a visible chat message deterministically.
|
|
21
|
+
* - If you pass ui.toast, we will also toast (throttling is handled by the toast manager).
|
|
22
|
+
*/
|
|
23
|
+
export type WorkflowUi = {
|
|
24
|
+
ctx: any;
|
|
25
|
+
sessionId: string;
|
|
26
|
+
agentName?: string;
|
|
27
|
+
toast?: (t: ToastOptions) => Promise<void>;
|
|
28
|
+
};
|
|
13
29
|
/**
|
|
14
30
|
* PLANNING-FIRST REDESIGN
|
|
15
31
|
* ----------------------
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
* Deterministic trigger (config-driven):
|
|
22
|
-
* - config.workflow.genesis_planning:
|
|
23
|
-
* - "off" => never attach directive
|
|
24
|
-
* - "first_story_only"=> attach only when story_key === "S-0001"
|
|
25
|
-
* - "always" => attach for every run
|
|
26
|
-
*
|
|
27
|
-
* Contract: DB is already initialized before workflow is used:
|
|
28
|
-
* - schema tables exist
|
|
29
|
-
* - repo_state singleton row (id=1) exists
|
|
32
|
+
* - Never mutate story title/body.
|
|
33
|
+
* - Planning-first becomes a run-scoped inject stored in `injects` (scope = run:<run_id>).
|
|
34
|
+
* - Trigger is deterministic via config.workflow.genesis_planning:
|
|
35
|
+
* - "off" | "first_story_only" | "always"
|
|
30
36
|
*/
|
|
31
37
|
export type NextAction = {
|
|
32
38
|
kind: "idle";
|
|
@@ -61,10 +67,20 @@ export declare function decideNextAction(db: SqliteDb, config: AstrocodeConfig):
|
|
|
61
67
|
export declare function createRunForStory(db: SqliteDb, config: AstrocodeConfig, storyKey: string): {
|
|
62
68
|
run_id: string;
|
|
63
69
|
};
|
|
70
|
+
/**
|
|
71
|
+
* STAGE MOVEMENT (START) — now async so UI injection is deterministic.
|
|
72
|
+
*/
|
|
64
73
|
export declare function startStage(db: SqliteDb, runId: string, stageKey: StageKey, meta?: {
|
|
65
74
|
subagent_type?: string;
|
|
66
75
|
subagent_session_id?: string;
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
76
|
+
ui?: WorkflowUi;
|
|
77
|
+
}): Promise<void>;
|
|
78
|
+
/**
|
|
79
|
+
* STAGE CLOSED (RUN COMPLETED) — now async so UI injection is deterministic.
|
|
80
|
+
*/
|
|
81
|
+
export declare function completeRun(db: SqliteDb, runId: string, ui?: WorkflowUi): Promise<void>;
|
|
82
|
+
/**
|
|
83
|
+
* STAGE CLOSED (RUN FAILED) — now async so UI injection is deterministic.
|
|
84
|
+
*/
|
|
85
|
+
export declare function failRun(db: SqliteDb, runId: string, stageKey: StageKey, errorText: string, ui?: WorkflowUi): Promise<void>;
|
|
70
86
|
export declare function abortRun(db: SqliteDb, runId: string, reason: string): void;
|