astrocode-workflow 0.1.59 → 0.2.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/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 +106 -101
- package/dist/ui/inject.d.ts +7 -1
- package/dist/ui/inject.js +86 -38
- package/dist/workflow/state-machine.d.ts +25 -9
- package/dist/workflow/state-machine.js +97 -106
- package/package.json +1 -1
- package/src/state/db.ts +63 -4
- package/src/tools/injects.ts +147 -53
- package/src/tools/workflow.ts +147 -140
- package/src/ui/inject.ts +107 -40
- package/src/workflow/state-machine.ts +127 -137
package/dist/tools/workflow.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
+
// src/tools/workflow.ts
|
|
1
2
|
import { tool } from "@opencode-ai/plugin/tool";
|
|
2
3
|
import { withTx } from "../state/db";
|
|
3
4
|
import { buildContextSnapshot } from "../workflow/context";
|
|
4
|
-
import { decideNextAction, createRunForStory, startStage, completeRun, getActiveRun, EVENT_TYPES } from "../workflow/state-machine";
|
|
5
|
+
import { decideNextAction, createRunForStory, startStage, completeRun, failRun, getActiveRun, EVENT_TYPES, } from "../workflow/state-machine";
|
|
5
6
|
import { buildStageDirective, directiveHash } from "../workflow/directives";
|
|
6
7
|
import { injectChatPrompt } from "../ui/inject";
|
|
7
8
|
import { nowISO } from "../shared/time";
|
|
@@ -31,24 +32,20 @@ export function resolveAgentName(stageKey, config, agents, warnings) {
|
|
|
31
32
|
// Validate that the agent actually exists in the registry
|
|
32
33
|
if (agents && !agents[candidate]) {
|
|
33
34
|
const warning = `Agent "${candidate}" not found in registry for stage "${stageKey}". Falling back to General.`;
|
|
34
|
-
if (warnings)
|
|
35
|
+
if (warnings)
|
|
35
36
|
warnings.push(warning);
|
|
36
|
-
|
|
37
|
-
else {
|
|
37
|
+
else
|
|
38
38
|
console.warn(`[Astrocode] ${warning}`);
|
|
39
|
-
}
|
|
40
39
|
candidate = "General";
|
|
41
40
|
}
|
|
42
41
|
// Final guard: ensure General exists, fallback to built-in "general" if not
|
|
43
42
|
if (agents && !agents[candidate]) {
|
|
44
43
|
const finalWarning = `Critical: General agent not found in registry. Falling back to built-in "general".`;
|
|
45
|
-
if (warnings)
|
|
44
|
+
if (warnings)
|
|
46
45
|
warnings.push(finalWarning);
|
|
47
|
-
|
|
48
|
-
else {
|
|
46
|
+
else
|
|
49
47
|
console.warn(`[Astrocode] ${finalWarning}`);
|
|
50
|
-
|
|
51
|
-
return "general"; // built-in, guaranteed by OpenCode
|
|
48
|
+
return "general";
|
|
52
49
|
}
|
|
53
50
|
return candidate;
|
|
54
51
|
}
|
|
@@ -84,9 +81,6 @@ function stageConstraints(stage, cfg) {
|
|
|
84
81
|
}
|
|
85
82
|
return common;
|
|
86
83
|
}
|
|
87
|
-
function agentNameForStage(stage, cfg) {
|
|
88
|
-
return cfg.agents.stage_agent_names[stage];
|
|
89
|
-
}
|
|
90
84
|
function buildDelegationPrompt(opts) {
|
|
91
85
|
const { stageDirective, run_id, stage_key, stage_agent_name } = opts;
|
|
92
86
|
const stageUpper = stage_key.toUpperCase();
|
|
@@ -108,10 +102,49 @@ function buildDelegationPrompt(opts) {
|
|
|
108
102
|
``,
|
|
109
103
|
`Important: do NOT do any stage work yourself in orchestrator mode.`,
|
|
110
104
|
].join("\n").trim();
|
|
111
|
-
// Debug log the delegation prompt to troubleshoot agent output issues
|
|
112
105
|
debug(`Delegating stage ${stage_key} to agent ${stage_agent_name}`, { prompt_length: prompt.length });
|
|
113
106
|
return prompt;
|
|
114
107
|
}
|
|
108
|
+
function buildUiMessage(e) {
|
|
109
|
+
switch (e.kind) {
|
|
110
|
+
case "stage_started": {
|
|
111
|
+
const agent = e.agent_name ? ` (${e.agent_name})` : "";
|
|
112
|
+
const title = "Astrocode";
|
|
113
|
+
const message = `Stage started: ${e.stage_key}${agent}`;
|
|
114
|
+
const chatText = [
|
|
115
|
+
`[SYSTEM DIRECTIVE: ASTROCODE — STAGE_STARTED]`,
|
|
116
|
+
``,
|
|
117
|
+
`Run: ${e.run_id}`,
|
|
118
|
+
`Stage: ${e.stage_key}${agent}`,
|
|
119
|
+
].join("\n");
|
|
120
|
+
return { title, message, variant: "info", chatText };
|
|
121
|
+
}
|
|
122
|
+
case "run_completed": {
|
|
123
|
+
const title = "Astrocode";
|
|
124
|
+
const message = `Run completed: ${e.run_id}`;
|
|
125
|
+
const chatText = [
|
|
126
|
+
`[SYSTEM DIRECTIVE: ASTROCODE — RUN_COMPLETED]`,
|
|
127
|
+
``,
|
|
128
|
+
`Run: ${e.run_id}`,
|
|
129
|
+
`Story: ${e.story_key}`,
|
|
130
|
+
].join("\n");
|
|
131
|
+
return { title, message, variant: "success", chatText };
|
|
132
|
+
}
|
|
133
|
+
case "run_failed": {
|
|
134
|
+
const title = "Astrocode";
|
|
135
|
+
const message = `Run failed: ${e.run_id} (${e.stage_key})`;
|
|
136
|
+
const chatText = [
|
|
137
|
+
`[SYSTEM DIRECTIVE: ASTROCODE — RUN_FAILED]`,
|
|
138
|
+
``,
|
|
139
|
+
`Run: ${e.run_id}`,
|
|
140
|
+
`Story: ${e.story_key}`,
|
|
141
|
+
`Stage: ${e.stage_key}`,
|
|
142
|
+
`Error: ${e.error_text}`,
|
|
143
|
+
].join("\n");
|
|
144
|
+
return { title, message, variant: "error", chatText };
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
115
148
|
export function createAstroWorkflowProceedTool(opts) {
|
|
116
149
|
const { ctx, config, db, agents } = opts;
|
|
117
150
|
const toasts = createToastManager({ ctx, throttleMs: config.ui.toasts.throttle_ms });
|
|
@@ -127,6 +160,9 @@ export function createAstroWorkflowProceedTool(opts) {
|
|
|
127
160
|
const actions = [];
|
|
128
161
|
const warnings = [];
|
|
129
162
|
const startedAt = nowISO();
|
|
163
|
+
// Collect UI events emitted inside state-machine functions, then flush AFTER tx.
|
|
164
|
+
const uiEvents = [];
|
|
165
|
+
const emit = (e) => uiEvents.push(e);
|
|
130
166
|
for (let i = 0; i < steps; i++) {
|
|
131
167
|
const next = decideNextAction(db, config);
|
|
132
168
|
if (next.kind === "idle") {
|
|
@@ -134,60 +170,24 @@ export function createAstroWorkflowProceedTool(opts) {
|
|
|
134
170
|
break;
|
|
135
171
|
}
|
|
136
172
|
if (next.kind === "start_run") {
|
|
173
|
+
// SINGLE tx boundary: caller owns tx, state-machine is pure.
|
|
137
174
|
const { run_id } = withTx(db, () => createRunForStory(db, config, next.story_key));
|
|
138
175
|
actions.push(`started run ${run_id} for story ${next.story_key}`);
|
|
139
|
-
if (config.ui.toasts.enabled && config.ui.toasts.show_run_started) {
|
|
140
|
-
await toasts.show({ title: "Astrocode", message: `Run started (${run_id})`, variant: "success" });
|
|
141
|
-
}
|
|
142
176
|
if (mode === "step")
|
|
143
177
|
break;
|
|
144
178
|
continue;
|
|
145
179
|
}
|
|
146
180
|
if (next.kind === "complete_run") {
|
|
147
|
-
withTx(db, () => completeRun(db, next.run_id));
|
|
181
|
+
withTx(db, () => completeRun(db, next.run_id, emit));
|
|
148
182
|
actions.push(`completed run ${next.run_id}`);
|
|
149
|
-
if (
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
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
|
-
await injectChatPrompt({
|
|
184
|
-
ctx,
|
|
185
|
-
sessionId,
|
|
186
|
-
text: nextDirective,
|
|
187
|
-
agent: "Astro"
|
|
188
|
-
});
|
|
189
|
-
actions.push(`injected directive to start next story ${nextStory.story_key}`);
|
|
190
|
-
}
|
|
183
|
+
if (mode === "step")
|
|
184
|
+
break;
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
if (next.kind === "failed") {
|
|
188
|
+
// Ensure DB state reflects failure in one tx; emit UI event.
|
|
189
|
+
withTx(db, () => failRun(db, next.run_id, next.stage_key, next.error_text, emit));
|
|
190
|
+
actions.push(`failed: ${next.stage_key} — ${next.error_text}`);
|
|
191
191
|
if (mode === "step")
|
|
192
192
|
break;
|
|
193
193
|
continue;
|
|
@@ -198,53 +198,37 @@ export function createAstroWorkflowProceedTool(opts) {
|
|
|
198
198
|
throw new Error("Invariant: delegate_stage but no active run.");
|
|
199
199
|
const run = db.prepare("SELECT * FROM runs WHERE run_id=?").get(active.run_id);
|
|
200
200
|
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
201
|
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
202
|
const agentExists = (name) => {
|
|
207
|
-
|
|
208
|
-
if (agents && agents[name]) {
|
|
209
|
-
return true;
|
|
210
|
-
}
|
|
211
|
-
// Check system config agent map
|
|
212
|
-
if (systemConfig.agent && systemConfig.agent[name]) {
|
|
203
|
+
if (agents && agents[name])
|
|
213
204
|
return true;
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
const knownStageAgents = ["Frame", "Plan", "Spec", "Implement", "Review", "Verify", "Close"];
|
|
217
|
-
if (knownStageAgents.includes(name)) {
|
|
205
|
+
const knownStageAgents = ["Frame", "Plan", "Spec", "Implement", "Review", "Verify", "Close", "General", "Astro", "general"];
|
|
206
|
+
if (knownStageAgents.includes(name))
|
|
218
207
|
return true;
|
|
219
|
-
}
|
|
220
208
|
return false;
|
|
221
209
|
};
|
|
222
210
|
if (!agentExists(agentName)) {
|
|
223
211
|
const originalAgent = agentName;
|
|
224
|
-
console.warn(`[Astrocode] Agent ${agentName} not found
|
|
225
|
-
// First fallback: orchestrator
|
|
212
|
+
console.warn(`[Astrocode] Agent ${agentName} not found. Falling back to orchestrator.`);
|
|
226
213
|
agentName = config.agents?.orchestrator_name || "Astro";
|
|
227
214
|
if (!agentExists(agentName)) {
|
|
228
215
|
console.warn(`[Astrocode] Orchestrator ${agentName} not available. Falling back to General.`);
|
|
229
|
-
// Final fallback: General (guaranteed to exist)
|
|
230
216
|
agentName = "General";
|
|
231
217
|
if (!agentExists(agentName)) {
|
|
232
218
|
throw new Error(`Critical: No agents available for delegation. Primary: ${originalAgent}, Orchestrator: ${config.agents?.orchestrator_name || "Astro"}, General: unavailable`);
|
|
233
219
|
}
|
|
234
220
|
}
|
|
235
221
|
}
|
|
222
|
+
// NOTE: startStage owns its own tx (state-machine.ts).
|
|
236
223
|
withTx(db, () => {
|
|
237
|
-
startStage(db, active.run_id, next.stage_key, { subagent_type: agentName });
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
224
|
+
startStage(db, active.run_id, next.stage_key, { subagent_type: agentName }, emit);
|
|
225
|
+
});
|
|
226
|
+
const context = buildContextSnapshot({
|
|
227
|
+
db,
|
|
228
|
+
config,
|
|
229
|
+
run_id: active.run_id,
|
|
230
|
+
next_action: `delegate stage ${next.stage_key}`,
|
|
243
231
|
});
|
|
244
|
-
if (config.ui.toasts.enabled && config.ui.toasts.show_stage_started) {
|
|
245
|
-
await toasts.show({ title: "Astrocode", message: `Stage started: ${next.stage_key}`, variant: "info" });
|
|
246
|
-
}
|
|
247
|
-
const context = buildContextSnapshot({ db, config, run_id: active.run_id, next_action: `delegate stage ${next.stage_key}` });
|
|
248
232
|
const stageDirective = buildStageDirective({
|
|
249
233
|
config,
|
|
250
234
|
stage_key: next.stage_key,
|
|
@@ -262,22 +246,22 @@ export function createAstroWorkflowProceedTool(opts) {
|
|
|
262
246
|
stage_key: next.stage_key,
|
|
263
247
|
stage_agent_name: agentName,
|
|
264
248
|
});
|
|
265
|
-
// Record
|
|
249
|
+
// Record continuation (best-effort; no tx wrapper needed but safe either way)
|
|
266
250
|
const h = directiveHash(delegatePrompt);
|
|
267
251
|
const now = nowISO();
|
|
268
252
|
if (sessionId) {
|
|
253
|
+
// This assumes continuations table exists in vNext schema.
|
|
269
254
|
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);
|
|
270
255
|
}
|
|
271
|
-
// Visible injection so user can see state
|
|
256
|
+
// Visible injection so user can see state (awaited)
|
|
272
257
|
if (sessionId) {
|
|
273
258
|
await injectChatPrompt({ ctx, sessionId, text: delegatePrompt, agent: "Astro" });
|
|
274
|
-
// Inject continuation guidance
|
|
275
259
|
const continueMessage = [
|
|
276
260
|
`[SYSTEM DIRECTIVE: ASTROCODE — AWAIT_STAGE_COMPLETION]`,
|
|
277
261
|
``,
|
|
278
|
-
`Stage
|
|
262
|
+
`Stage \`${next.stage_key}\` delegated to \`${agentName}\`.`,
|
|
279
263
|
``,
|
|
280
|
-
`When
|
|
264
|
+
`When \`${agentName}\` completes, call:`,
|
|
281
265
|
`astro_stage_complete(run_id="${active.run_id}", stage_key="${next.stage_key}", output_text="[paste subagent output here]")`,
|
|
282
266
|
``,
|
|
283
267
|
`This advances the workflow.`,
|
|
@@ -290,13 +274,17 @@ export function createAstroWorkflowProceedTool(opts) {
|
|
|
290
274
|
}
|
|
291
275
|
if (next.kind === "await_stage_completion") {
|
|
292
276
|
actions.push(`await stage completion: ${next.stage_key}`);
|
|
293
|
-
// Optionally nudge with a short directive
|
|
294
277
|
if (sessionId) {
|
|
295
|
-
const context = buildContextSnapshot({
|
|
278
|
+
const context = buildContextSnapshot({
|
|
279
|
+
db,
|
|
280
|
+
config,
|
|
281
|
+
run_id: next.run_id,
|
|
282
|
+
next_action: `complete stage ${next.stage_key}`,
|
|
283
|
+
});
|
|
296
284
|
const prompt = [
|
|
297
285
|
`[SYSTEM DIRECTIVE: ASTROCODE — AWAIT_STAGE_OUTPUT]`,
|
|
298
286
|
``,
|
|
299
|
-
`Run
|
|
287
|
+
`Run \`${next.run_id}\` is waiting for stage \`${next.stage_key}\` output.`,
|
|
300
288
|
`If you have the subagent output, call astro_stage_complete with output_text=the FULL output.`,
|
|
301
289
|
``,
|
|
302
290
|
`Context snapshot:`,
|
|
@@ -309,14 +297,31 @@ export function createAstroWorkflowProceedTool(opts) {
|
|
|
309
297
|
}
|
|
310
298
|
break;
|
|
311
299
|
}
|
|
312
|
-
if (next.kind === "failed") {
|
|
313
|
-
actions.push(`failed: ${next.stage_key} — ${next.error_text}`);
|
|
314
|
-
break;
|
|
315
|
-
}
|
|
316
|
-
// safety
|
|
317
300
|
actions.push(`unhandled next action: ${next.kind}`);
|
|
318
301
|
break;
|
|
319
302
|
}
|
|
303
|
+
// Flush UI events (toast + prompt) AFTER state transitions
|
|
304
|
+
if (uiEvents.length > 0) {
|
|
305
|
+
for (const e of uiEvents) {
|
|
306
|
+
const msg = buildUiMessage(e);
|
|
307
|
+
if (config.ui.toasts.enabled) {
|
|
308
|
+
await toasts.show({
|
|
309
|
+
title: msg.title,
|
|
310
|
+
message: msg.message,
|
|
311
|
+
variant: msg.variant,
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
if (ctx?.sessionID) {
|
|
315
|
+
await injectChatPrompt({
|
|
316
|
+
ctx,
|
|
317
|
+
sessionId: ctx.sessionID,
|
|
318
|
+
text: msg.chatText,
|
|
319
|
+
agent: "Astro",
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
actions.push(`ui: flushed ${uiEvents.length} event(s)`);
|
|
324
|
+
}
|
|
320
325
|
// Housekeeping event
|
|
321
326
|
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());
|
|
322
327
|
const active = getActiveRun(db);
|
package/dist/ui/inject.d.ts
CHANGED
|
@@ -1,6 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Inject a visible prompt into the conversation.
|
|
3
|
+
* - Deterministic ordering per session
|
|
4
|
+
* - Correct SDK binding (prevents `this._client` undefined)
|
|
5
|
+
* - Awaitable: resolves when delivered, rejects after max retries
|
|
6
|
+
*/
|
|
1
7
|
export declare function injectChatPrompt(opts: {
|
|
2
8
|
ctx: any;
|
|
3
|
-
sessionId
|
|
9
|
+
sessionId?: string;
|
|
4
10
|
text: string;
|
|
5
11
|
agent?: string;
|
|
6
12
|
}): Promise<void>;
|
package/dist/ui/inject.js
CHANGED
|
@@ -1,47 +1,95 @@
|
|
|
1
|
-
|
|
2
|
-
const
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
1
|
+
const MAX_ATTEMPTS = 4;
|
|
2
|
+
const RETRY_DELAYS_MS = [250, 500, 1000, 2000]; // attempt 1..4
|
|
3
|
+
// Per-session queues so one stuck session doesn't block others
|
|
4
|
+
const queues = new Map();
|
|
5
|
+
const running = new Set();
|
|
6
|
+
function sleep(ms) {
|
|
7
|
+
return new Promise((r) => setTimeout(r, ms));
|
|
8
|
+
}
|
|
9
|
+
function getPromptInvoker(ctx) {
|
|
10
|
+
const session = ctx?.client?.session;
|
|
11
|
+
const prompt = session?.prompt;
|
|
12
|
+
if (!session || typeof prompt !== "function") {
|
|
13
|
+
throw new Error("API not available (ctx.client.session.prompt)");
|
|
11
14
|
}
|
|
15
|
+
return { session, prompt };
|
|
16
|
+
}
|
|
17
|
+
async function tryInjectOnce(item) {
|
|
18
|
+
const { ctx, sessionId, text, agent = "Astro" } = item;
|
|
19
|
+
const prefixedText = `[${agent}]\n\n${text}`;
|
|
20
|
+
const { session, prompt } = getPromptInvoker(ctx);
|
|
21
|
+
// IMPORTANT: force correct `this` binding
|
|
22
|
+
await prompt.call(session, {
|
|
23
|
+
path: { id: sessionId },
|
|
24
|
+
body: {
|
|
25
|
+
parts: [{ type: "text", text: prefixedText }],
|
|
26
|
+
agent,
|
|
27
|
+
},
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
async function runSessionQueue(sessionId) {
|
|
31
|
+
if (running.has(sessionId))
|
|
32
|
+
return;
|
|
33
|
+
running.add(sessionId);
|
|
12
34
|
try {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
35
|
+
// eslint-disable-next-line no-constant-condition
|
|
36
|
+
while (true) {
|
|
37
|
+
const q = queues.get(sessionId);
|
|
38
|
+
if (!q || q.length === 0)
|
|
39
|
+
break;
|
|
40
|
+
const item = q.shift();
|
|
41
|
+
try {
|
|
42
|
+
await tryInjectOnce(item);
|
|
43
|
+
item.resolve();
|
|
44
|
+
}
|
|
45
|
+
catch (err) {
|
|
46
|
+
item.attempts += 1;
|
|
47
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
48
|
+
const delay = RETRY_DELAYS_MS[Math.min(item.attempts - 1, RETRY_DELAYS_MS.length - 1)] ?? 2000;
|
|
49
|
+
if (item.attempts >= MAX_ATTEMPTS) {
|
|
50
|
+
console.warn(`[Astrocode] Injection failed permanently after ${item.attempts} attempts: ${msg}`);
|
|
51
|
+
item.reject(err);
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
console.warn(`[Astrocode] Injection attempt ${item.attempts}/${MAX_ATTEMPTS} failed: ${msg}; retrying in ${delay}ms`);
|
|
55
|
+
await sleep(delay);
|
|
56
|
+
// Requeue at front to preserve order (and avoid starving later messages)
|
|
57
|
+
const q2 = queues.get(sessionId) ?? [];
|
|
58
|
+
q2.unshift(item);
|
|
59
|
+
queues.set(sessionId, q2);
|
|
60
|
+
}
|
|
19
61
|
}
|
|
20
|
-
if (!ctx?.client?.session?.prompt) {
|
|
21
|
-
console.warn("[Astrocode] Skipping injection: API not available (ctx.client.session.prompt)");
|
|
22
|
-
return;
|
|
23
|
-
}
|
|
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
62
|
}
|
|
36
63
|
finally {
|
|
37
|
-
|
|
38
|
-
// Process next item immediately
|
|
39
|
-
if (injectionQueue.length > 0) {
|
|
40
|
-
setImmediate(processQueue);
|
|
41
|
-
}
|
|
64
|
+
running.delete(sessionId);
|
|
42
65
|
}
|
|
43
66
|
}
|
|
67
|
+
/**
|
|
68
|
+
* Inject a visible prompt into the conversation.
|
|
69
|
+
* - Deterministic ordering per session
|
|
70
|
+
* - Correct SDK binding (prevents `this._client` undefined)
|
|
71
|
+
* - Awaitable: resolves when delivered, rejects after max retries
|
|
72
|
+
*/
|
|
44
73
|
export async function injectChatPrompt(opts) {
|
|
45
|
-
|
|
46
|
-
|
|
74
|
+
const sessionId = opts.sessionId ?? opts.ctx?.sessionID;
|
|
75
|
+
if (!sessionId) {
|
|
76
|
+
console.warn("[Astrocode] Skipping injection: No sessionId provided");
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
return new Promise((resolve, reject) => {
|
|
80
|
+
const item = {
|
|
81
|
+
ctx: opts.ctx,
|
|
82
|
+
sessionId,
|
|
83
|
+
text: opts.text,
|
|
84
|
+
agent: opts.agent,
|
|
85
|
+
attempts: 0,
|
|
86
|
+
resolve,
|
|
87
|
+
reject,
|
|
88
|
+
};
|
|
89
|
+
const q = queues.get(sessionId) ?? [];
|
|
90
|
+
q.push(item);
|
|
91
|
+
queues.set(sessionId, q);
|
|
92
|
+
// Fire worker (don't await here; caller awaits the returned Promise)
|
|
93
|
+
void runSessionQueue(sessionId);
|
|
94
|
+
});
|
|
47
95
|
}
|
|
@@ -10,23 +10,39 @@ export declare const EVENT_TYPES: {
|
|
|
10
10
|
readonly STAGE_STARTED: "stage.started";
|
|
11
11
|
readonly WORKFLOW_PROCEED: "workflow.proceed";
|
|
12
12
|
};
|
|
13
|
+
export type UiEmitEvent = {
|
|
14
|
+
kind: "stage_started";
|
|
15
|
+
run_id: string;
|
|
16
|
+
stage_key: StageKey;
|
|
17
|
+
agent_name?: string;
|
|
18
|
+
} | {
|
|
19
|
+
kind: "run_completed";
|
|
20
|
+
run_id: string;
|
|
21
|
+
story_key: string;
|
|
22
|
+
} | {
|
|
23
|
+
kind: "run_failed";
|
|
24
|
+
run_id: string;
|
|
25
|
+
story_key: string;
|
|
26
|
+
stage_key: StageKey;
|
|
27
|
+
error_text: string;
|
|
28
|
+
};
|
|
29
|
+
export type UiEmit = (e: UiEmitEvent) => void;
|
|
13
30
|
/**
|
|
14
31
|
* PLANNING-FIRST REDESIGN
|
|
15
32
|
* ----------------------
|
|
16
|
-
*
|
|
17
|
-
* New behavior: NEVER mutate story title/body.
|
|
18
|
-
*
|
|
19
|
-
* Planning-first is a run-scoped directive stored in `injects` (scope = run:<run_id>).
|
|
33
|
+
* Never mutate story title/body.
|
|
20
34
|
*
|
|
21
|
-
* Deterministic trigger
|
|
35
|
+
* Deterministic trigger:
|
|
22
36
|
* - config.workflow.genesis_planning:
|
|
23
37
|
* - "off" => never attach directive
|
|
24
|
-
* - "first_story_only"=>
|
|
38
|
+
* - "first_story_only"=> only when story_key === "S-0001"
|
|
25
39
|
* - "always" => attach for every run
|
|
26
40
|
*
|
|
27
41
|
* Contract: DB is already initialized before workflow is used:
|
|
28
42
|
* - schema tables exist
|
|
29
43
|
* - repo_state singleton row (id=1) exists
|
|
44
|
+
*
|
|
45
|
+
* IMPORTANT: Do NOT call withTx() in here. The caller owns transaction boundaries.
|
|
30
46
|
*/
|
|
31
47
|
export type NextAction = {
|
|
32
48
|
kind: "idle";
|
|
@@ -64,7 +80,7 @@ export declare function createRunForStory(db: SqliteDb, config: AstrocodeConfig,
|
|
|
64
80
|
export declare function startStage(db: SqliteDb, runId: string, stageKey: StageKey, meta?: {
|
|
65
81
|
subagent_type?: string;
|
|
66
82
|
subagent_session_id?: string;
|
|
67
|
-
}): void;
|
|
68
|
-
export declare function completeRun(db: SqliteDb, runId: string): void;
|
|
69
|
-
export declare function failRun(db: SqliteDb, runId: string, stageKey: StageKey, errorText: string): void;
|
|
83
|
+
}, emit?: UiEmit): void;
|
|
84
|
+
export declare function completeRun(db: SqliteDb, runId: string, emit?: UiEmit): void;
|
|
85
|
+
export declare function failRun(db: SqliteDb, runId: string, stageKey: StageKey, errorText: string, emit?: UiEmit): void;
|
|
70
86
|
export declare function abortRun(db: SqliteDb, runId: string, reason: string): void;
|