astrocode-workflow 0.2.0 → 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/workflow.d.ts +1 -1
- package/dist/tools/workflow.js +106 -101
- package/dist/ui/inject.d.ts +9 -17
- package/dist/ui/inject.js +79 -102
- package/dist/workflow/state-machine.d.ts +32 -32
- package/dist/workflow/state-machine.js +85 -170
- package/package.json +1 -1
- package/src/state/db.ts +63 -4
- package/src/tools/workflow.ts +147 -140
- package/src/ui/inject.ts +98 -105
- package/src/workflow/state-machine.ts +123 -227
package/src/state/db.ts
CHANGED
|
@@ -48,7 +48,32 @@ export function configurePragmas(db: SqliteDb, pragmas: Record<string, any>) {
|
|
|
48
48
|
if (pragmas.temp_store) db.pragma(`temp_store = ${pragmas.temp_store}`);
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
-
/**
|
|
51
|
+
/**
|
|
52
|
+
* Re-entrant transaction helper.
|
|
53
|
+
*
|
|
54
|
+
* SQLite rejects BEGIN inside BEGIN. We use:
|
|
55
|
+
* - depth=0: BEGIN IMMEDIATE ... COMMIT/ROLLBACK
|
|
56
|
+
* - depth>0: SAVEPOINT sp_n ... RELEASE / ROLLBACK TO + RELEASE
|
|
57
|
+
*
|
|
58
|
+
* This allows callers to safely nest withTx across layers (tools -> workflow -> state machine)
|
|
59
|
+
* without "cannot start a transaction within a transaction".
|
|
60
|
+
*/
|
|
61
|
+
const TX_DEPTH = new WeakMap<object, number>();
|
|
62
|
+
|
|
63
|
+
function getDepth(db: SqliteDb): number {
|
|
64
|
+
return TX_DEPTH.get(db as any) ?? 0;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function setDepth(db: SqliteDb, depth: number) {
|
|
68
|
+
if (depth <= 0) TX_DEPTH.delete(db as any);
|
|
69
|
+
else TX_DEPTH.set(db as any, depth);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function savepointName(depth: number): string {
|
|
73
|
+
return `sp_${depth}`;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** BEGIN IMMEDIATE transaction helper (re-entrant). */
|
|
52
77
|
export function withTx<T>(db: SqliteDb, fn: () => T, opts?: { require?: boolean }): T {
|
|
53
78
|
const adapter = createDatabaseAdapter();
|
|
54
79
|
const available = adapter.isAvailable();
|
|
@@ -58,18 +83,52 @@ export function withTx<T>(db: SqliteDb, fn: () => T, opts?: { require?: boolean
|
|
|
58
83
|
return fn();
|
|
59
84
|
}
|
|
60
85
|
|
|
61
|
-
db
|
|
86
|
+
const depth = getDepth(db);
|
|
87
|
+
|
|
88
|
+
if (depth === 0) {
|
|
89
|
+
db.exec("BEGIN IMMEDIATE");
|
|
90
|
+
setDepth(db, 1);
|
|
91
|
+
try {
|
|
92
|
+
const out = fn();
|
|
93
|
+
db.exec("COMMIT");
|
|
94
|
+
return out;
|
|
95
|
+
} catch (e) {
|
|
96
|
+
try {
|
|
97
|
+
db.exec("ROLLBACK");
|
|
98
|
+
} catch {
|
|
99
|
+
// ignore
|
|
100
|
+
}
|
|
101
|
+
throw e;
|
|
102
|
+
} finally {
|
|
103
|
+
setDepth(db, 0);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Nested: use SAVEPOINT
|
|
108
|
+
const nextDepth = depth + 1;
|
|
109
|
+
const sp = savepointName(nextDepth);
|
|
110
|
+
|
|
111
|
+
db.exec(`SAVEPOINT ${sp}`);
|
|
112
|
+
setDepth(db, nextDepth);
|
|
113
|
+
|
|
62
114
|
try {
|
|
63
115
|
const out = fn();
|
|
64
|
-
db.exec(
|
|
116
|
+
db.exec(`RELEASE SAVEPOINT ${sp}`);
|
|
65
117
|
return out;
|
|
66
118
|
} catch (e) {
|
|
67
119
|
try {
|
|
68
|
-
db.exec(
|
|
120
|
+
db.exec(`ROLLBACK TO SAVEPOINT ${sp}`);
|
|
121
|
+
} catch {
|
|
122
|
+
// ignore
|
|
123
|
+
}
|
|
124
|
+
try {
|
|
125
|
+
db.exec(`RELEASE SAVEPOINT ${sp}`);
|
|
69
126
|
} catch {
|
|
70
127
|
// ignore
|
|
71
128
|
}
|
|
72
129
|
throw e;
|
|
130
|
+
} finally {
|
|
131
|
+
setDepth(db, depth);
|
|
73
132
|
}
|
|
74
133
|
}
|
|
75
134
|
|
package/src/tools/workflow.ts
CHANGED
|
@@ -1,16 +1,27 @@
|
|
|
1
|
+
// src/tools/workflow.ts
|
|
1
2
|
import { tool, type ToolDefinition } from "@opencode-ai/plugin/tool";
|
|
2
3
|
import type { AstrocodeConfig } from "../config/schema";
|
|
3
4
|
import type { SqliteDb } from "../state/db";
|
|
4
5
|
import { withTx } from "../state/db";
|
|
5
6
|
import type { StageKey } from "../state/types";
|
|
7
|
+
import type { UiEmitEvent } from "../workflow/state-machine";
|
|
6
8
|
import { buildContextSnapshot } from "../workflow/context";
|
|
7
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
decideNextAction,
|
|
11
|
+
createRunForStory,
|
|
12
|
+
startStage,
|
|
13
|
+
completeRun,
|
|
14
|
+
failRun,
|
|
15
|
+
getActiveRun,
|
|
16
|
+
EVENT_TYPES,
|
|
17
|
+
} from "../workflow/state-machine";
|
|
8
18
|
import { buildStageDirective, directiveHash } from "../workflow/directives";
|
|
9
19
|
import { injectChatPrompt } from "../ui/inject";
|
|
10
20
|
import { nowISO } from "../shared/time";
|
|
11
21
|
import { newEventId } from "../state/ids";
|
|
12
22
|
import { debug } from "../shared/log";
|
|
13
23
|
import { createToastManager } from "../ui/toasts";
|
|
24
|
+
import type { AgentConfig } from "@opencode-ai/sdk";
|
|
14
25
|
|
|
15
26
|
// Agent name mapping for case-sensitive resolution
|
|
16
27
|
export const STAGE_TO_AGENT_MAP: Record<string, string> = {
|
|
@@ -37,23 +48,17 @@ export function resolveAgentName(stageKey: StageKey, config: AstrocodeConfig, ag
|
|
|
37
48
|
// Validate that the agent actually exists in the registry
|
|
38
49
|
if (agents && !agents[candidate]) {
|
|
39
50
|
const warning = `Agent "${candidate}" not found in registry for stage "${stageKey}". Falling back to General.`;
|
|
40
|
-
if (warnings)
|
|
41
|
-
|
|
42
|
-
} else {
|
|
43
|
-
console.warn(`[Astrocode] ${warning}`);
|
|
44
|
-
}
|
|
51
|
+
if (warnings) warnings.push(warning);
|
|
52
|
+
else console.warn(`[Astrocode] ${warning}`);
|
|
45
53
|
candidate = "General";
|
|
46
54
|
}
|
|
47
55
|
|
|
48
56
|
// Final guard: ensure General exists, fallback to built-in "general" if not
|
|
49
57
|
if (agents && !agents[candidate]) {
|
|
50
58
|
const finalWarning = `Critical: General agent not found in registry. Falling back to built-in "general".`;
|
|
51
|
-
if (warnings)
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
console.warn(`[Astrocode] ${finalWarning}`);
|
|
55
|
-
}
|
|
56
|
-
return "general"; // built-in, guaranteed by OpenCode
|
|
59
|
+
if (warnings) warnings.push(finalWarning);
|
|
60
|
+
else console.warn(`[Astrocode] ${finalWarning}`);
|
|
61
|
+
return "general";
|
|
57
62
|
}
|
|
58
63
|
|
|
59
64
|
return candidate;
|
|
@@ -94,10 +99,6 @@ function stageConstraints(stage: StageKey, cfg: AstrocodeConfig): string[] {
|
|
|
94
99
|
return common;
|
|
95
100
|
}
|
|
96
101
|
|
|
97
|
-
function agentNameForStage(stage: StageKey, cfg: AstrocodeConfig): string {
|
|
98
|
-
return cfg.agents.stage_agent_names[stage];
|
|
99
|
-
}
|
|
100
|
-
|
|
101
102
|
function buildDelegationPrompt(opts: {
|
|
102
103
|
stageDirective: string;
|
|
103
104
|
run_id: string;
|
|
@@ -126,13 +127,50 @@ function buildDelegationPrompt(opts: {
|
|
|
126
127
|
`Important: do NOT do any stage work yourself in orchestrator mode.`,
|
|
127
128
|
].join("\n").trim();
|
|
128
129
|
|
|
129
|
-
// Debug log the delegation prompt to troubleshoot agent output issues
|
|
130
130
|
debug(`Delegating stage ${stage_key} to agent ${stage_agent_name}`, { prompt_length: prompt.length });
|
|
131
|
-
|
|
132
131
|
return prompt;
|
|
133
132
|
}
|
|
134
133
|
|
|
135
|
-
|
|
134
|
+
function buildUiMessage(e: UiEmitEvent): { title: string; message: string; variant: "info" | "success" | "error"; chatText: string } {
|
|
135
|
+
switch (e.kind) {
|
|
136
|
+
case "stage_started": {
|
|
137
|
+
const agent = e.agent_name ? ` (${e.agent_name})` : "";
|
|
138
|
+
const title = "Astrocode";
|
|
139
|
+
const message = `Stage started: ${e.stage_key}${agent}`;
|
|
140
|
+
const chatText = [
|
|
141
|
+
`[SYSTEM DIRECTIVE: ASTROCODE — STAGE_STARTED]`,
|
|
142
|
+
``,
|
|
143
|
+
`Run: ${e.run_id}`,
|
|
144
|
+
`Stage: ${e.stage_key}${agent}`,
|
|
145
|
+
].join("\n");
|
|
146
|
+
return { title, message, variant: "info", chatText };
|
|
147
|
+
}
|
|
148
|
+
case "run_completed": {
|
|
149
|
+
const title = "Astrocode";
|
|
150
|
+
const message = `Run completed: ${e.run_id}`;
|
|
151
|
+
const chatText = [
|
|
152
|
+
`[SYSTEM DIRECTIVE: ASTROCODE — RUN_COMPLETED]`,
|
|
153
|
+
``,
|
|
154
|
+
`Run: ${e.run_id}`,
|
|
155
|
+
`Story: ${e.story_key}`,
|
|
156
|
+
].join("\n");
|
|
157
|
+
return { title, message, variant: "success", chatText };
|
|
158
|
+
}
|
|
159
|
+
case "run_failed": {
|
|
160
|
+
const title = "Astrocode";
|
|
161
|
+
const message = `Run failed: ${e.run_id} (${e.stage_key})`;
|
|
162
|
+
const chatText = [
|
|
163
|
+
`[SYSTEM DIRECTIVE: ASTROCODE — RUN_FAILED]`,
|
|
164
|
+
``,
|
|
165
|
+
`Run: ${e.run_id}`,
|
|
166
|
+
`Story: ${e.story_key}`,
|
|
167
|
+
`Stage: ${e.stage_key}`,
|
|
168
|
+
`Error: ${e.error_text}`,
|
|
169
|
+
].join("\n");
|
|
170
|
+
return { title, message, variant: "error", chatText };
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
136
174
|
|
|
137
175
|
export function createAstroWorkflowProceedTool(opts: { ctx: any; config: AstrocodeConfig; db: SqliteDb; agents?: Record<string, AgentConfig> }): ToolDefinition {
|
|
138
176
|
const { ctx, config, db, agents } = opts;
|
|
@@ -153,6 +191,10 @@ export function createAstroWorkflowProceedTool(opts: { ctx: any; config: Astroco
|
|
|
153
191
|
const warnings: string[] = [];
|
|
154
192
|
const startedAt = nowISO();
|
|
155
193
|
|
|
194
|
+
// Collect UI events emitted inside state-machine functions, then flush AFTER tx.
|
|
195
|
+
const uiEvents: UiEmitEvent[] = [];
|
|
196
|
+
const emit = (e: UiEmitEvent) => uiEvents.push(e);
|
|
197
|
+
|
|
156
198
|
for (let i = 0; i < steps; i++) {
|
|
157
199
|
const next = decideNextAction(db, config);
|
|
158
200
|
|
|
@@ -162,72 +204,26 @@ export function createAstroWorkflowProceedTool(opts: { ctx: any; config: Astroco
|
|
|
162
204
|
}
|
|
163
205
|
|
|
164
206
|
if (next.kind === "start_run") {
|
|
207
|
+
// SINGLE tx boundary: caller owns tx, state-machine is pure.
|
|
165
208
|
const { run_id } = withTx(db, () => createRunForStory(db, config, next.story_key));
|
|
166
209
|
actions.push(`started run ${run_id} for story ${next.story_key}`);
|
|
167
210
|
|
|
168
|
-
if (config.ui.toasts.enabled && config.ui.toasts.show_run_started) {
|
|
169
|
-
await toasts.show({ title: "Astrocode", message: `Run started (${run_id})`, variant: "success" });
|
|
170
|
-
}
|
|
171
|
-
|
|
172
211
|
if (mode === "step") break;
|
|
173
212
|
continue;
|
|
174
213
|
}
|
|
175
214
|
|
|
176
215
|
if (next.kind === "complete_run") {
|
|
177
|
-
withTx(db, () => completeRun(db, next.run_id));
|
|
216
|
+
withTx(db, () => completeRun(db, next.run_id, emit));
|
|
178
217
|
actions.push(`completed run ${next.run_id}`);
|
|
179
218
|
|
|
180
|
-
if (
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
// Inject continuation directive for workflow resumption
|
|
185
|
-
if (sessionId) {
|
|
186
|
-
const continueDirective = [
|
|
187
|
-
`[SYSTEM DIRECTIVE: ASTROCODE — CONTINUE]`,
|
|
188
|
-
``,
|
|
189
|
-
`Run ${next.run_id} completed successfully.`,
|
|
190
|
-
``,
|
|
191
|
-
`The Clara Forms implementation run has finished all stages. The spec has been analyzed and decomposed into prioritized implementation stories.`,
|
|
192
|
-
``,
|
|
193
|
-
`Next actions: Review the generated stories and approve the next one to continue development.`,
|
|
194
|
-
].join("\n");
|
|
195
|
-
|
|
196
|
-
await injectChatPrompt({
|
|
197
|
-
ctx,
|
|
198
|
-
sessionId,
|
|
199
|
-
text: continueDirective,
|
|
200
|
-
agent: "Astro"
|
|
201
|
-
});
|
|
202
|
-
|
|
203
|
-
actions.push(`injected continuation directive for completed run ${next.run_id}`);
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
// Check for next approved story to start
|
|
207
|
-
const nextStory = db.prepare(
|
|
208
|
-
"SELECT story_key, title FROM stories WHERE state = 'approved' ORDER BY priority DESC, created_at ASC LIMIT 1"
|
|
209
|
-
).get() as { story_key: string; title: string } | undefined;
|
|
210
|
-
|
|
211
|
-
if (nextStory && sessionId) {
|
|
212
|
-
const nextDirective = [
|
|
213
|
-
`[SYSTEM DIRECTIVE: ASTROCODE — START_NEXT_STORY]`,
|
|
214
|
-
``,
|
|
215
|
-
`The previous run completed successfully. Start the next approved story.`,
|
|
216
|
-
``,
|
|
217
|
-
`Next Story: ${nextStory.story_key} — ${nextStory.title}`,
|
|
218
|
-
``,
|
|
219
|
-
`Action: Call astro_story_approve with story_key="${nextStory.story_key}" to start it, or select a different story.`,
|
|
220
|
-
].join("\n");
|
|
221
|
-
|
|
222
|
-
await injectChatPrompt({
|
|
223
|
-
ctx,
|
|
224
|
-
sessionId,
|
|
225
|
-
text: nextDirective,
|
|
226
|
-
agent: "Astro"
|
|
227
|
-
});
|
|
219
|
+
if (mode === "step") break;
|
|
220
|
+
continue;
|
|
221
|
+
}
|
|
228
222
|
|
|
229
|
-
|
|
230
|
-
|
|
223
|
+
if (next.kind === "failed") {
|
|
224
|
+
// Ensure DB state reflects failure in one tx; emit UI event.
|
|
225
|
+
withTx(db, () => failRun(db, next.run_id, next.stage_key, next.error_text, emit));
|
|
226
|
+
actions.push(`failed: ${next.stage_key} — ${next.error_text}`);
|
|
231
227
|
|
|
232
228
|
if (mode === "step") break;
|
|
233
229
|
continue;
|
|
@@ -236,62 +232,45 @@ export function createAstroWorkflowProceedTool(opts: { ctx: any; config: Astroco
|
|
|
236
232
|
if (next.kind === "delegate_stage") {
|
|
237
233
|
const active = getActiveRun(db);
|
|
238
234
|
if (!active) throw new Error("Invariant: delegate_stage but no active run.");
|
|
235
|
+
|
|
239
236
|
const run = db.prepare("SELECT * FROM runs WHERE run_id=?").get(active.run_id) as any;
|
|
240
237
|
const story = db.prepare("SELECT * FROM stories WHERE story_key=?").get(run.story_key) as any;
|
|
241
238
|
|
|
242
|
-
// Mark stage started + set subagent_type to the stage agent.
|
|
243
239
|
let agentName = resolveAgentName(next.stage_key, config, agents, warnings);
|
|
244
240
|
|
|
245
|
-
// Validate agent availability with fallback chain
|
|
246
|
-
const systemConfig = config as any;
|
|
247
|
-
// Check both the system config agent map (if present) OR the local agents map passed to the tool
|
|
248
241
|
const agentExists = (name: string) => {
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
// Check system config agent map
|
|
254
|
-
if (systemConfig.agent && systemConfig.agent[name]) {
|
|
255
|
-
return true;
|
|
256
|
-
}
|
|
257
|
-
// For known stage agents, assume they exist (they are system-provided subagents)
|
|
258
|
-
const knownStageAgents = ["Frame", "Plan", "Spec", "Implement", "Review", "Verify", "Close"];
|
|
259
|
-
if (knownStageAgents.includes(name)) {
|
|
260
|
-
return true;
|
|
261
|
-
}
|
|
262
|
-
return false;
|
|
242
|
+
if (agents && agents[name]) return true;
|
|
243
|
+
const knownStageAgents = ["Frame", "Plan", "Spec", "Implement", "Review", "Verify", "Close", "General", "Astro", "general"];
|
|
244
|
+
if (knownStageAgents.includes(name)) return true;
|
|
245
|
+
return false;
|
|
263
246
|
};
|
|
264
247
|
|
|
248
|
+
if (!agentExists(agentName)) {
|
|
249
|
+
const originalAgent = agentName;
|
|
250
|
+
console.warn(`[Astrocode] Agent ${agentName} not found. Falling back to orchestrator.`);
|
|
251
|
+
agentName = config.agents?.orchestrator_name || "Astro";
|
|
265
252
|
if (!agentExists(agentName)) {
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
// First fallback: orchestrator
|
|
269
|
-
agentName = config.agents?.orchestrator_name || "Astro";
|
|
253
|
+
console.warn(`[Astrocode] Orchestrator ${agentName} not available. Falling back to General.`);
|
|
254
|
+
agentName = "General";
|
|
270
255
|
if (!agentExists(agentName)) {
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
if (!agentExists(agentName)) {
|
|
275
|
-
throw new Error(`Critical: No agents available for delegation. Primary: ${originalAgent}, Orchestrator: ${config.agents?.orchestrator_name || "Astro"}, General: unavailable`);
|
|
276
|
-
}
|
|
256
|
+
throw new Error(
|
|
257
|
+
`Critical: No agents available for delegation. Primary: ${originalAgent}, Orchestrator: ${config.agents?.orchestrator_name || "Astro"}, General: unavailable`
|
|
258
|
+
);
|
|
277
259
|
}
|
|
278
260
|
}
|
|
261
|
+
}
|
|
279
262
|
|
|
263
|
+
// NOTE: startStage owns its own tx (state-machine.ts).
|
|
280
264
|
withTx(db, () => {
|
|
281
|
-
startStage(db, active.run_id, next.stage_key, { subagent_type: agentName });
|
|
282
|
-
|
|
283
|
-
// Log delegation observability
|
|
284
|
-
if (config.debug?.telemetry?.enabled) {
|
|
285
|
-
// eslint-disable-next-line no-console
|
|
286
|
-
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'}`);
|
|
287
|
-
}
|
|
265
|
+
startStage(db, active.run_id, next.stage_key, { subagent_type: agentName }, emit);
|
|
288
266
|
});
|
|
289
267
|
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
268
|
+
const context = buildContextSnapshot({
|
|
269
|
+
db,
|
|
270
|
+
config,
|
|
271
|
+
run_id: active.run_id,
|
|
272
|
+
next_action: `delegate stage ${next.stage_key}`,
|
|
273
|
+
});
|
|
295
274
|
|
|
296
275
|
const stageDirective = buildStageDirective({
|
|
297
276
|
config,
|
|
@@ -312,35 +291,35 @@ export function createAstroWorkflowProceedTool(opts: { ctx: any; config: Astroco
|
|
|
312
291
|
stage_agent_name: agentName,
|
|
313
292
|
});
|
|
314
293
|
|
|
315
|
-
// Record
|
|
294
|
+
// Record continuation (best-effort; no tx wrapper needed but safe either way)
|
|
316
295
|
const h = directiveHash(delegatePrompt);
|
|
317
296
|
const now = nowISO();
|
|
318
297
|
if (sessionId) {
|
|
298
|
+
// This assumes continuations table exists in vNext schema.
|
|
319
299
|
db.prepare(
|
|
320
300
|
"INSERT INTO continuations (session_id, run_id, directive_hash, kind, reason, created_at) VALUES (?, ?, ?, 'stage', ?, ?)"
|
|
321
301
|
).run(sessionId, active.run_id, h, `delegate ${next.stage_key}`, now);
|
|
322
302
|
}
|
|
323
303
|
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
304
|
+
// Visible injection so user can see state (awaited)
|
|
305
|
+
if (sessionId) {
|
|
306
|
+
await injectChatPrompt({ ctx, sessionId, text: delegatePrompt, agent: "Astro" });
|
|
327
307
|
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
].join("\n");
|
|
308
|
+
const continueMessage = [
|
|
309
|
+
`[SYSTEM DIRECTIVE: ASTROCODE — AWAIT_STAGE_COMPLETION]`,
|
|
310
|
+
``,
|
|
311
|
+
`Stage \`${next.stage_key}\` delegated to \`${agentName}\`.`,
|
|
312
|
+
``,
|
|
313
|
+
`When \`${agentName}\` completes, call:`,
|
|
314
|
+
`astro_stage_complete(run_id="${active.run_id}", stage_key="${next.stage_key}", output_text="[paste subagent output here]")`,
|
|
315
|
+
``,
|
|
316
|
+
`This advances the workflow.`,
|
|
317
|
+
].join("\n");
|
|
339
318
|
|
|
340
|
-
|
|
341
|
-
|
|
319
|
+
await injectChatPrompt({ ctx, sessionId, text: continueMessage, agent: "Astro" });
|
|
320
|
+
}
|
|
342
321
|
|
|
343
|
-
|
|
322
|
+
actions.push(`delegated stage ${next.stage_key} via ${agentName}`);
|
|
344
323
|
|
|
345
324
|
// Stop here; subagent needs to run.
|
|
346
325
|
break;
|
|
@@ -348,18 +327,25 @@ export function createAstroWorkflowProceedTool(opts: { ctx: any; config: Astroco
|
|
|
348
327
|
|
|
349
328
|
if (next.kind === "await_stage_completion") {
|
|
350
329
|
actions.push(`await stage completion: ${next.stage_key}`);
|
|
351
|
-
|
|
330
|
+
|
|
352
331
|
if (sessionId) {
|
|
353
|
-
const context = buildContextSnapshot({
|
|
332
|
+
const context = buildContextSnapshot({
|
|
333
|
+
db,
|
|
334
|
+
config,
|
|
335
|
+
run_id: next.run_id,
|
|
336
|
+
next_action: `complete stage ${next.stage_key}`,
|
|
337
|
+
});
|
|
338
|
+
|
|
354
339
|
const prompt = [
|
|
355
340
|
`[SYSTEM DIRECTIVE: ASTROCODE — AWAIT_STAGE_OUTPUT]`,
|
|
356
341
|
``,
|
|
357
|
-
`Run
|
|
342
|
+
`Run \`${next.run_id}\` is waiting for stage \`${next.stage_key}\` output.`,
|
|
358
343
|
`If you have the subagent output, call astro_stage_complete with output_text=the FULL output.`,
|
|
359
344
|
``,
|
|
360
345
|
`Context snapshot:`,
|
|
361
346
|
context,
|
|
362
347
|
].join("\n").trim();
|
|
348
|
+
|
|
363
349
|
const h = directiveHash(prompt);
|
|
364
350
|
const now = nowISO();
|
|
365
351
|
db.prepare(
|
|
@@ -368,19 +354,40 @@ export function createAstroWorkflowProceedTool(opts: { ctx: any; config: Astroco
|
|
|
368
354
|
|
|
369
355
|
await injectChatPrompt({ ctx, sessionId, text: prompt, agent: "Astro" });
|
|
370
356
|
}
|
|
371
|
-
break;
|
|
372
|
-
}
|
|
373
357
|
|
|
374
|
-
if (next.kind === "failed") {
|
|
375
|
-
actions.push(`failed: ${next.stage_key} — ${next.error_text}`);
|
|
376
358
|
break;
|
|
377
359
|
}
|
|
378
360
|
|
|
379
|
-
// safety
|
|
380
361
|
actions.push(`unhandled next action: ${(next as any).kind}`);
|
|
381
362
|
break;
|
|
382
363
|
}
|
|
383
364
|
|
|
365
|
+
// Flush UI events (toast + prompt) AFTER state transitions
|
|
366
|
+
if (uiEvents.length > 0) {
|
|
367
|
+
for (const e of uiEvents) {
|
|
368
|
+
const msg = buildUiMessage(e);
|
|
369
|
+
|
|
370
|
+
if (config.ui.toasts.enabled) {
|
|
371
|
+
await toasts.show({
|
|
372
|
+
title: msg.title,
|
|
373
|
+
message: msg.message,
|
|
374
|
+
variant: msg.variant,
|
|
375
|
+
});
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
if ((ctx as any)?.sessionID) {
|
|
379
|
+
await injectChatPrompt({
|
|
380
|
+
ctx,
|
|
381
|
+
sessionId: (ctx as any).sessionID,
|
|
382
|
+
text: msg.chatText,
|
|
383
|
+
agent: "Astro",
|
|
384
|
+
});
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
actions.push(`ui: flushed ${uiEvents.length} event(s)`);
|
|
389
|
+
}
|
|
390
|
+
|
|
384
391
|
// Housekeeping event
|
|
385
392
|
db.prepare(
|
|
386
393
|
"INSERT INTO events (event_id, run_id, stage_key, type, body_json, created_at) VALUES (?, NULL, NULL, ?, ?, ?)"
|