astrocode-workflow 0.2.0 → 0.4.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/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/dist/state/db.d.ts
CHANGED
|
@@ -4,7 +4,7 @@ export declare function openSqlite(dbPath: string, opts?: {
|
|
|
4
4
|
busyTimeoutMs?: number;
|
|
5
5
|
}): SqliteDb;
|
|
6
6
|
export declare function configurePragmas(db: SqliteDb, pragmas: Record<string, any>): void;
|
|
7
|
-
/** BEGIN IMMEDIATE transaction helper. */
|
|
7
|
+
/** BEGIN IMMEDIATE transaction helper (re-entrant). */
|
|
8
8
|
export declare function withTx<T>(db: SqliteDb, fn: () => T, opts?: {
|
|
9
9
|
require?: boolean;
|
|
10
10
|
}): T;
|
package/dist/state/db.js
CHANGED
|
@@ -46,7 +46,30 @@ export function configurePragmas(db, pragmas) {
|
|
|
46
46
|
if (pragmas.temp_store)
|
|
47
47
|
db.pragma(`temp_store = ${pragmas.temp_store}`);
|
|
48
48
|
}
|
|
49
|
-
/**
|
|
49
|
+
/**
|
|
50
|
+
* Re-entrant transaction helper.
|
|
51
|
+
*
|
|
52
|
+
* SQLite rejects BEGIN inside BEGIN. We use:
|
|
53
|
+
* - depth=0: BEGIN IMMEDIATE ... COMMIT/ROLLBACK
|
|
54
|
+
* - depth>0: SAVEPOINT sp_n ... RELEASE / ROLLBACK TO + RELEASE
|
|
55
|
+
*
|
|
56
|
+
* This allows callers to safely nest withTx across layers (tools -> workflow -> state machine)
|
|
57
|
+
* without "cannot start a transaction within a transaction".
|
|
58
|
+
*/
|
|
59
|
+
const TX_DEPTH = new WeakMap();
|
|
60
|
+
function getDepth(db) {
|
|
61
|
+
return TX_DEPTH.get(db) ?? 0;
|
|
62
|
+
}
|
|
63
|
+
function setDepth(db, depth) {
|
|
64
|
+
if (depth <= 0)
|
|
65
|
+
TX_DEPTH.delete(db);
|
|
66
|
+
else
|
|
67
|
+
TX_DEPTH.set(db, depth);
|
|
68
|
+
}
|
|
69
|
+
function savepointName(depth) {
|
|
70
|
+
return `sp_${depth}`;
|
|
71
|
+
}
|
|
72
|
+
/** BEGIN IMMEDIATE transaction helper (re-entrant). */
|
|
50
73
|
export function withTx(db, fn, opts) {
|
|
51
74
|
const adapter = createDatabaseAdapter();
|
|
52
75
|
const available = adapter.isAvailable();
|
|
@@ -55,21 +78,56 @@ export function withTx(db, fn, opts) {
|
|
|
55
78
|
throw new Error("Database adapter unavailable; transaction required.");
|
|
56
79
|
return fn();
|
|
57
80
|
}
|
|
58
|
-
db
|
|
81
|
+
const depth = getDepth(db);
|
|
82
|
+
if (depth === 0) {
|
|
83
|
+
db.exec("BEGIN IMMEDIATE");
|
|
84
|
+
setDepth(db, 1);
|
|
85
|
+
try {
|
|
86
|
+
const out = fn();
|
|
87
|
+
db.exec("COMMIT");
|
|
88
|
+
return out;
|
|
89
|
+
}
|
|
90
|
+
catch (e) {
|
|
91
|
+
try {
|
|
92
|
+
db.exec("ROLLBACK");
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
// ignore
|
|
96
|
+
}
|
|
97
|
+
throw e;
|
|
98
|
+
}
|
|
99
|
+
finally {
|
|
100
|
+
setDepth(db, 0);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
// Nested: use SAVEPOINT
|
|
104
|
+
const nextDepth = depth + 1;
|
|
105
|
+
const sp = savepointName(nextDepth);
|
|
106
|
+
db.exec(`SAVEPOINT ${sp}`);
|
|
107
|
+
setDepth(db, nextDepth);
|
|
59
108
|
try {
|
|
60
109
|
const out = fn();
|
|
61
|
-
db.exec(
|
|
110
|
+
db.exec(`RELEASE SAVEPOINT ${sp}`);
|
|
62
111
|
return out;
|
|
63
112
|
}
|
|
64
113
|
catch (e) {
|
|
65
114
|
try {
|
|
66
|
-
db.exec(
|
|
115
|
+
db.exec(`ROLLBACK TO SAVEPOINT ${sp}`);
|
|
116
|
+
}
|
|
117
|
+
catch {
|
|
118
|
+
// ignore
|
|
119
|
+
}
|
|
120
|
+
try {
|
|
121
|
+
db.exec(`RELEASE SAVEPOINT ${sp}`);
|
|
67
122
|
}
|
|
68
123
|
catch {
|
|
69
124
|
// ignore
|
|
70
125
|
}
|
|
71
126
|
throw e;
|
|
72
127
|
}
|
|
128
|
+
finally {
|
|
129
|
+
setDepth(db, depth);
|
|
130
|
+
}
|
|
73
131
|
}
|
|
74
132
|
export function getSchemaVersion(db) {
|
|
75
133
|
try {
|
package/dist/tools/workflow.d.ts
CHANGED
|
@@ -2,9 +2,9 @@ import { type ToolDefinition } from "@opencode-ai/plugin/tool";
|
|
|
2
2
|
import type { AstrocodeConfig } from "../config/schema";
|
|
3
3
|
import type { SqliteDb } from "../state/db";
|
|
4
4
|
import type { StageKey } from "../state/types";
|
|
5
|
+
import type { AgentConfig } from "@opencode-ai/sdk";
|
|
5
6
|
export declare const STAGE_TO_AGENT_MAP: Record<string, string>;
|
|
6
7
|
export declare function resolveAgentName(stageKey: StageKey, config: AstrocodeConfig, agents?: any, warnings?: string[]): string;
|
|
7
|
-
import { AgentConfig } from "@opencode-ai/sdk";
|
|
8
8
|
export declare function createAstroWorkflowProceedTool(opts: {
|
|
9
9
|
ctx: any;
|
|
10
10
|
config: AstrocodeConfig;
|
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,20 +1,12 @@
|
|
|
1
|
-
|
|
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
|
+
*/
|
|
7
|
+
export declare function injectChatPrompt(opts: {
|
|
2
8
|
ctx: any;
|
|
3
|
-
sessionId
|
|
9
|
+
sessionId?: string;
|
|
4
10
|
text: string;
|
|
5
11
|
agent?: string;
|
|
6
|
-
}
|
|
7
|
-
/**
|
|
8
|
-
* Enqueue an injection and ensure the worker is running.
|
|
9
|
-
* Does NOT wait for delivery — use `flushChatPrompts()` to wait.
|
|
10
|
-
*/
|
|
11
|
-
export declare function enqueueChatPrompt(opts: QueueItem): void;
|
|
12
|
-
/**
|
|
13
|
-
* Wait until all queued injections have been processed (sent or exhausted retries).
|
|
14
|
-
*/
|
|
15
|
-
export declare function flushChatPrompts(): Promise<void>;
|
|
16
|
-
/**
|
|
17
|
-
* Deterministic helper: enqueue + flush (recommended for stage boundaries).
|
|
18
|
-
*/
|
|
19
|
-
export declare function injectChatPrompt(opts: QueueItem): Promise<void>;
|
|
20
|
-
export {};
|
|
12
|
+
}): Promise<void>;
|