astrocode-workflow 0.2.0 → 0.2.2
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/index.js +6 -0
- package/dist/state/db.d.ts +1 -1
- package/dist/state/db.js +62 -4
- package/dist/state/repo-lock.d.ts +3 -0
- package/dist/state/repo-lock.js +29 -0
- package/dist/tools/workflow.d.ts +1 -1
- package/dist/tools/workflow.js +224 -209
- 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/index.ts +8 -0
- package/src/state/db.ts +63 -4
- package/src/state/repo-lock.ts +26 -0
- package/src/tools/workflow.ts +159 -142
- 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
|
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// src/state/repo-lock.ts
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
|
|
5
|
+
export function acquireRepoLock(lockPath: string): { release: () => void } {
|
|
6
|
+
fs.mkdirSync(path.dirname(lockPath), { recursive: true });
|
|
7
|
+
|
|
8
|
+
let fd: number;
|
|
9
|
+
try {
|
|
10
|
+
fd = fs.openSync(lockPath, "wx"); // exclusive create
|
|
11
|
+
} catch (e: any) {
|
|
12
|
+
const msg = e?.code === "EEXIST"
|
|
13
|
+
? `Astrocode lock is already held (${lockPath}). Another opencode process is running in this repo.`
|
|
14
|
+
: `Failed to acquire lock (${lockPath}): ${e?.message ?? String(e)}`;
|
|
15
|
+
throw new Error(msg);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
fs.writeFileSync(fd, `${(process as any).pid}\n`, "utf8");
|
|
19
|
+
|
|
20
|
+
return {
|
|
21
|
+
release: () => {
|
|
22
|
+
try { fs.closeSync(fd); } catch {}
|
|
23
|
+
try { fs.unlinkSync(lockPath); } catch {}
|
|
24
|
+
},
|
|
25
|
+
};
|
|
26
|
+
}
|
package/src/tools/workflow.ts
CHANGED
|
@@ -1,16 +1,28 @@
|
|
|
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";
|
|
25
|
+
import { acquireRepoLock } from "../state/repo-lock";
|
|
14
26
|
|
|
15
27
|
// Agent name mapping for case-sensitive resolution
|
|
16
28
|
export const STAGE_TO_AGENT_MAP: Record<string, string> = {
|
|
@@ -37,23 +49,17 @@ export function resolveAgentName(stageKey: StageKey, config: AstrocodeConfig, ag
|
|
|
37
49
|
// Validate that the agent actually exists in the registry
|
|
38
50
|
if (agents && !agents[candidate]) {
|
|
39
51
|
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
|
-
}
|
|
52
|
+
if (warnings) warnings.push(warning);
|
|
53
|
+
else console.warn(`[Astrocode] ${warning}`);
|
|
45
54
|
candidate = "General";
|
|
46
55
|
}
|
|
47
56
|
|
|
48
57
|
// Final guard: ensure General exists, fallback to built-in "general" if not
|
|
49
58
|
if (agents && !agents[candidate]) {
|
|
50
59
|
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
|
|
60
|
+
if (warnings) warnings.push(finalWarning);
|
|
61
|
+
else console.warn(`[Astrocode] ${finalWarning}`);
|
|
62
|
+
return "general";
|
|
57
63
|
}
|
|
58
64
|
|
|
59
65
|
return candidate;
|
|
@@ -94,10 +100,6 @@ function stageConstraints(stage: StageKey, cfg: AstrocodeConfig): string[] {
|
|
|
94
100
|
return common;
|
|
95
101
|
}
|
|
96
102
|
|
|
97
|
-
function agentNameForStage(stage: StageKey, cfg: AstrocodeConfig): string {
|
|
98
|
-
return cfg.agents.stage_agent_names[stage];
|
|
99
|
-
}
|
|
100
|
-
|
|
101
103
|
function buildDelegationPrompt(opts: {
|
|
102
104
|
stageDirective: string;
|
|
103
105
|
run_id: string;
|
|
@@ -126,13 +128,50 @@ function buildDelegationPrompt(opts: {
|
|
|
126
128
|
`Important: do NOT do any stage work yourself in orchestrator mode.`,
|
|
127
129
|
].join("\n").trim();
|
|
128
130
|
|
|
129
|
-
// Debug log the delegation prompt to troubleshoot agent output issues
|
|
130
131
|
debug(`Delegating stage ${stage_key} to agent ${stage_agent_name}`, { prompt_length: prompt.length });
|
|
131
|
-
|
|
132
132
|
return prompt;
|
|
133
133
|
}
|
|
134
134
|
|
|
135
|
-
|
|
135
|
+
function buildUiMessage(e: UiEmitEvent): { title: string; message: string; variant: "info" | "success" | "error"; chatText: string } {
|
|
136
|
+
switch (e.kind) {
|
|
137
|
+
case "stage_started": {
|
|
138
|
+
const agent = e.agent_name ? ` (${e.agent_name})` : "";
|
|
139
|
+
const title = "Astrocode";
|
|
140
|
+
const message = `Stage started: ${e.stage_key}${agent}`;
|
|
141
|
+
const chatText = [
|
|
142
|
+
`[SYSTEM DIRECTIVE: ASTROCODE — STAGE_STARTED]`,
|
|
143
|
+
``,
|
|
144
|
+
`Run: ${e.run_id}`,
|
|
145
|
+
`Stage: ${e.stage_key}${agent}`,
|
|
146
|
+
].join("\n");
|
|
147
|
+
return { title, message, variant: "info", chatText };
|
|
148
|
+
}
|
|
149
|
+
case "run_completed": {
|
|
150
|
+
const title = "Astrocode";
|
|
151
|
+
const message = `Run completed: ${e.run_id}`;
|
|
152
|
+
const chatText = [
|
|
153
|
+
`[SYSTEM DIRECTIVE: ASTROCODE — RUN_COMPLETED]`,
|
|
154
|
+
``,
|
|
155
|
+
`Run: ${e.run_id}`,
|
|
156
|
+
`Story: ${e.story_key}`,
|
|
157
|
+
].join("\n");
|
|
158
|
+
return { title, message, variant: "success", chatText };
|
|
159
|
+
}
|
|
160
|
+
case "run_failed": {
|
|
161
|
+
const title = "Astrocode";
|
|
162
|
+
const message = `Run failed: ${e.run_id} (${e.stage_key})`;
|
|
163
|
+
const chatText = [
|
|
164
|
+
`[SYSTEM DIRECTIVE: ASTROCODE — RUN_FAILED]`,
|
|
165
|
+
``,
|
|
166
|
+
`Run: ${e.run_id}`,
|
|
167
|
+
`Story: ${e.story_key}`,
|
|
168
|
+
`Stage: ${e.stage_key}`,
|
|
169
|
+
`Error: ${e.error_text}`,
|
|
170
|
+
].join("\n");
|
|
171
|
+
return { title, message, variant: "error", chatText };
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
136
175
|
|
|
137
176
|
export function createAstroWorkflowProceedTool(opts: { ctx: any; config: AstrocodeConfig; db: SqliteDb; agents?: Record<string, AgentConfig> }): ToolDefinition {
|
|
138
177
|
const { ctx, config, db, agents } = opts;
|
|
@@ -146,13 +185,22 @@ export function createAstroWorkflowProceedTool(opts: { ctx: any; config: Astroco
|
|
|
146
185
|
max_steps: tool.schema.number().int().positive().default(config.workflow.default_max_steps),
|
|
147
186
|
},
|
|
148
187
|
execute: async ({ mode, max_steps }) => {
|
|
149
|
-
|
|
150
|
-
const
|
|
188
|
+
// Acquire repo lock to ensure no concurrent workflow operations
|
|
189
|
+
const lockPath = `${(ctx as any).directory}/.astro/astro.lock`;
|
|
190
|
+
const repoLock = acquireRepoLock(lockPath);
|
|
191
|
+
|
|
192
|
+
try {
|
|
193
|
+
const sessionId = (ctx as any).sessionID as string | undefined;
|
|
194
|
+
const steps = Math.min(max_steps, config.workflow.loop_max_steps_hard_cap);
|
|
151
195
|
|
|
152
196
|
const actions: string[] = [];
|
|
153
197
|
const warnings: string[] = [];
|
|
154
198
|
const startedAt = nowISO();
|
|
155
199
|
|
|
200
|
+
// Collect UI events emitted inside state-machine functions, then flush AFTER tx.
|
|
201
|
+
const uiEvents: UiEmitEvent[] = [];
|
|
202
|
+
const emit = (e: UiEmitEvent) => uiEvents.push(e);
|
|
203
|
+
|
|
156
204
|
for (let i = 0; i < steps; i++) {
|
|
157
205
|
const next = decideNextAction(db, config);
|
|
158
206
|
|
|
@@ -162,72 +210,26 @@ export function createAstroWorkflowProceedTool(opts: { ctx: any; config: Astroco
|
|
|
162
210
|
}
|
|
163
211
|
|
|
164
212
|
if (next.kind === "start_run") {
|
|
213
|
+
// SINGLE tx boundary: caller owns tx, state-machine is pure.
|
|
165
214
|
const { run_id } = withTx(db, () => createRunForStory(db, config, next.story_key));
|
|
166
215
|
actions.push(`started run ${run_id} for story ${next.story_key}`);
|
|
167
216
|
|
|
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
217
|
if (mode === "step") break;
|
|
173
218
|
continue;
|
|
174
219
|
}
|
|
175
220
|
|
|
176
221
|
if (next.kind === "complete_run") {
|
|
177
|
-
withTx(db, () => completeRun(db, next.run_id));
|
|
222
|
+
withTx(db, () => completeRun(db, next.run_id, emit));
|
|
178
223
|
actions.push(`completed run ${next.run_id}`);
|
|
179
224
|
|
|
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
|
-
});
|
|
225
|
+
if (mode === "step") break;
|
|
226
|
+
continue;
|
|
227
|
+
}
|
|
228
228
|
|
|
229
|
-
|
|
230
|
-
|
|
229
|
+
if (next.kind === "failed") {
|
|
230
|
+
// Ensure DB state reflects failure in one tx; emit UI event.
|
|
231
|
+
withTx(db, () => failRun(db, next.run_id, next.stage_key, next.error_text, emit));
|
|
232
|
+
actions.push(`failed: ${next.stage_key} — ${next.error_text}`);
|
|
231
233
|
|
|
232
234
|
if (mode === "step") break;
|
|
233
235
|
continue;
|
|
@@ -236,62 +238,45 @@ export function createAstroWorkflowProceedTool(opts: { ctx: any; config: Astroco
|
|
|
236
238
|
if (next.kind === "delegate_stage") {
|
|
237
239
|
const active = getActiveRun(db);
|
|
238
240
|
if (!active) throw new Error("Invariant: delegate_stage but no active run.");
|
|
241
|
+
|
|
239
242
|
const run = db.prepare("SELECT * FROM runs WHERE run_id=?").get(active.run_id) as any;
|
|
240
243
|
const story = db.prepare("SELECT * FROM stories WHERE story_key=?").get(run.story_key) as any;
|
|
241
244
|
|
|
242
|
-
// Mark stage started + set subagent_type to the stage agent.
|
|
243
245
|
let agentName = resolveAgentName(next.stage_key, config, agents, warnings);
|
|
244
246
|
|
|
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
247
|
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;
|
|
248
|
+
if (agents && agents[name]) return true;
|
|
249
|
+
const knownStageAgents = ["Frame", "Plan", "Spec", "Implement", "Review", "Verify", "Close", "General", "Astro", "general"];
|
|
250
|
+
if (knownStageAgents.includes(name)) return true;
|
|
251
|
+
return false;
|
|
263
252
|
};
|
|
264
253
|
|
|
254
|
+
if (!agentExists(agentName)) {
|
|
255
|
+
const originalAgent = agentName;
|
|
256
|
+
console.warn(`[Astrocode] Agent ${agentName} not found. Falling back to orchestrator.`);
|
|
257
|
+
agentName = config.agents?.orchestrator_name || "Astro";
|
|
265
258
|
if (!agentExists(agentName)) {
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
// First fallback: orchestrator
|
|
269
|
-
agentName = config.agents?.orchestrator_name || "Astro";
|
|
259
|
+
console.warn(`[Astrocode] Orchestrator ${agentName} not available. Falling back to General.`);
|
|
260
|
+
agentName = "General";
|
|
270
261
|
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
|
-
}
|
|
262
|
+
throw new Error(
|
|
263
|
+
`Critical: No agents available for delegation. Primary: ${originalAgent}, Orchestrator: ${config.agents?.orchestrator_name || "Astro"}, General: unavailable`
|
|
264
|
+
);
|
|
277
265
|
}
|
|
278
266
|
}
|
|
267
|
+
}
|
|
279
268
|
|
|
269
|
+
// NOTE: startStage owns its own tx (state-machine.ts).
|
|
280
270
|
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
|
-
}
|
|
271
|
+
startStage(db, active.run_id, next.stage_key, { subagent_type: agentName }, emit);
|
|
288
272
|
});
|
|
289
273
|
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
274
|
+
const context = buildContextSnapshot({
|
|
275
|
+
db,
|
|
276
|
+
config,
|
|
277
|
+
run_id: active.run_id,
|
|
278
|
+
next_action: `delegate stage ${next.stage_key}`,
|
|
279
|
+
});
|
|
295
280
|
|
|
296
281
|
const stageDirective = buildStageDirective({
|
|
297
282
|
config,
|
|
@@ -312,35 +297,35 @@ export function createAstroWorkflowProceedTool(opts: { ctx: any; config: Astroco
|
|
|
312
297
|
stage_agent_name: agentName,
|
|
313
298
|
});
|
|
314
299
|
|
|
315
|
-
// Record
|
|
300
|
+
// Record continuation (best-effort; no tx wrapper needed but safe either way)
|
|
316
301
|
const h = directiveHash(delegatePrompt);
|
|
317
302
|
const now = nowISO();
|
|
318
303
|
if (sessionId) {
|
|
304
|
+
// This assumes continuations table exists in vNext schema.
|
|
319
305
|
db.prepare(
|
|
320
306
|
"INSERT INTO continuations (session_id, run_id, directive_hash, kind, reason, created_at) VALUES (?, ?, ?, 'stage', ?, ?)"
|
|
321
307
|
).run(sessionId, active.run_id, h, `delegate ${next.stage_key}`, now);
|
|
322
308
|
}
|
|
323
309
|
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
310
|
+
// Visible injection so user can see state (awaited)
|
|
311
|
+
if (sessionId) {
|
|
312
|
+
await injectChatPrompt({ ctx, sessionId, text: delegatePrompt, agent: "Astro" });
|
|
327
313
|
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
].join("\n");
|
|
314
|
+
const continueMessage = [
|
|
315
|
+
`[SYSTEM DIRECTIVE: ASTROCODE — AWAIT_STAGE_COMPLETION]`,
|
|
316
|
+
``,
|
|
317
|
+
`Stage \`${next.stage_key}\` delegated to \`${agentName}\`.`,
|
|
318
|
+
``,
|
|
319
|
+
`When \`${agentName}\` completes, call:`,
|
|
320
|
+
`astro_stage_complete(run_id="${active.run_id}", stage_key="${next.stage_key}", output_text="[paste subagent output here]")`,
|
|
321
|
+
``,
|
|
322
|
+
`This advances the workflow.`,
|
|
323
|
+
].join("\n");
|
|
339
324
|
|
|
340
|
-
|
|
341
|
-
|
|
325
|
+
await injectChatPrompt({ ctx, sessionId, text: continueMessage, agent: "Astro" });
|
|
326
|
+
}
|
|
342
327
|
|
|
343
|
-
|
|
328
|
+
actions.push(`delegated stage ${next.stage_key} via ${agentName}`);
|
|
344
329
|
|
|
345
330
|
// Stop here; subagent needs to run.
|
|
346
331
|
break;
|
|
@@ -348,18 +333,25 @@ export function createAstroWorkflowProceedTool(opts: { ctx: any; config: Astroco
|
|
|
348
333
|
|
|
349
334
|
if (next.kind === "await_stage_completion") {
|
|
350
335
|
actions.push(`await stage completion: ${next.stage_key}`);
|
|
351
|
-
|
|
336
|
+
|
|
352
337
|
if (sessionId) {
|
|
353
|
-
const context = buildContextSnapshot({
|
|
338
|
+
const context = buildContextSnapshot({
|
|
339
|
+
db,
|
|
340
|
+
config,
|
|
341
|
+
run_id: next.run_id,
|
|
342
|
+
next_action: `complete stage ${next.stage_key}`,
|
|
343
|
+
});
|
|
344
|
+
|
|
354
345
|
const prompt = [
|
|
355
346
|
`[SYSTEM DIRECTIVE: ASTROCODE — AWAIT_STAGE_OUTPUT]`,
|
|
356
347
|
``,
|
|
357
|
-
`Run
|
|
348
|
+
`Run \`${next.run_id}\` is waiting for stage \`${next.stage_key}\` output.`,
|
|
358
349
|
`If you have the subagent output, call astro_stage_complete with output_text=the FULL output.`,
|
|
359
350
|
``,
|
|
360
351
|
`Context snapshot:`,
|
|
361
352
|
context,
|
|
362
353
|
].join("\n").trim();
|
|
354
|
+
|
|
363
355
|
const h = directiveHash(prompt);
|
|
364
356
|
const now = nowISO();
|
|
365
357
|
db.prepare(
|
|
@@ -368,19 +360,40 @@ export function createAstroWorkflowProceedTool(opts: { ctx: any; config: Astroco
|
|
|
368
360
|
|
|
369
361
|
await injectChatPrompt({ ctx, sessionId, text: prompt, agent: "Astro" });
|
|
370
362
|
}
|
|
371
|
-
break;
|
|
372
|
-
}
|
|
373
363
|
|
|
374
|
-
if (next.kind === "failed") {
|
|
375
|
-
actions.push(`failed: ${next.stage_key} — ${next.error_text}`);
|
|
376
364
|
break;
|
|
377
365
|
}
|
|
378
366
|
|
|
379
|
-
// safety
|
|
380
367
|
actions.push(`unhandled next action: ${(next as any).kind}`);
|
|
381
368
|
break;
|
|
382
369
|
}
|
|
383
370
|
|
|
371
|
+
// Flush UI events (toast + prompt) AFTER state transitions
|
|
372
|
+
if (uiEvents.length > 0) {
|
|
373
|
+
for (const e of uiEvents) {
|
|
374
|
+
const msg = buildUiMessage(e);
|
|
375
|
+
|
|
376
|
+
if (config.ui.toasts.enabled) {
|
|
377
|
+
await toasts.show({
|
|
378
|
+
title: msg.title,
|
|
379
|
+
message: msg.message,
|
|
380
|
+
variant: msg.variant,
|
|
381
|
+
});
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
if ((ctx as any)?.sessionID) {
|
|
385
|
+
await injectChatPrompt({
|
|
386
|
+
ctx,
|
|
387
|
+
sessionId: (ctx as any).sessionID,
|
|
388
|
+
text: msg.chatText,
|
|
389
|
+
agent: "Astro",
|
|
390
|
+
});
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
actions.push(`ui: flushed ${uiEvents.length} event(s)`);
|
|
395
|
+
}
|
|
396
|
+
|
|
384
397
|
// Housekeeping event
|
|
385
398
|
db.prepare(
|
|
386
399
|
"INSERT INTO events (event_id, run_id, stage_key, type, body_json, created_at) VALUES (?, NULL, NULL, ?, ?, ?)"
|
|
@@ -401,6 +414,10 @@ export function createAstroWorkflowProceedTool(opts: { ctx: any; config: Astroco
|
|
|
401
414
|
}
|
|
402
415
|
|
|
403
416
|
return lines.join("\n").trim();
|
|
417
|
+
} finally {
|
|
418
|
+
// Always release the lock
|
|
419
|
+
repoLock.release();
|
|
420
|
+
}
|
|
404
421
|
},
|
|
405
422
|
});
|
|
406
423
|
}
|