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.
@@ -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 (config.ui.toasts.enabled && config.ui.toasts.show_run_completed) {
150
- await toasts.show({ title: "Astrocode", message: `Run completed (${next.run_id})`, variant: "success" });
151
- }
152
- // Inject continuation directive for workflow resumption
153
- if (sessionId) {
154
- const continueDirective = [
155
- `[SYSTEM DIRECTIVE: ASTROCODE CONTINUE]`,
156
- ``,
157
- `Run ${next.run_id} completed successfully.`,
158
- ``,
159
- `The Clara Forms implementation run has finished all stages. The spec has been analyzed and decomposed into prioritized implementation stories.`,
160
- ``,
161
- `Next actions: Review the generated stories and approve the next one to continue development.`,
162
- ].join("\n");
163
- await injectChatPrompt({
164
- ctx,
165
- sessionId,
166
- text: continueDirective,
167
- agent: "Astro"
168
- });
169
- actions.push(`injected continuation directive for completed run ${next.run_id}`);
170
- }
171
- // Check for next approved story to start
172
- const nextStory = db.prepare("SELECT story_key, title FROM stories WHERE state = 'approved' ORDER BY priority DESC, created_at ASC LIMIT 1").get();
173
- if (nextStory && sessionId) {
174
- const nextDirective = [
175
- `[SYSTEM DIRECTIVE: ASTROCODE — START_NEXT_STORY]`,
176
- ``,
177
- `The previous run completed successfully. Start the next approved story.`,
178
- ``,
179
- `Next Story: ${nextStory.story_key} — ${nextStory.title}`,
180
- ``,
181
- `Action: Call astro_story_approve with story_key="${nextStory.story_key}" to start it, or select a different story.`,
182
- ].join("\n");
183
- 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
- // Check local agents map first (populated from src/index.ts)
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
- // For known stage agents, assume they exist (they are system-provided subagents)
216
- const knownStageAgents = ["Frame", "Plan", "Spec", "Implement", "Review", "Verify", "Close"];
217
- if (knownStageAgents.includes(name)) {
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 in config. Falling back to orchestrator.`);
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
- // Log delegation observability
239
- if (config.debug?.telemetry?.enabled) {
240
- // eslint-disable-next-line no-console
241
- console.log(`[Astrocode:delegate] run_id=${active.run_id} stage=${next.stage_key} agent=${agentName} fallback=${agentName !== resolveAgentName(next.stage_key, config, agents) ? 'yes' : 'no'}`);
242
- }
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 in continuations as a stage directive (dedupe by hash)
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 ${next.stage_key} delegated to ${agentName}.`,
262
+ `Stage \`${next.stage_key}\` delegated to \`${agentName}\`.`,
279
263
  ``,
280
- `When ${agentName} completes, call:`,
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({ db, config, run_id: next.run_id, next_action: `complete stage ${next.stage_key}` });
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 ${next.run_id} is waiting for stage ${next.stage_key} output.`,
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);
@@ -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: string;
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
- let isInjecting = false;
2
- const injectionQueue = [];
3
- async function processQueue() {
4
- if (isInjecting || injectionQueue.length === 0)
5
- return;
6
- isInjecting = true;
7
- const opts = injectionQueue.shift();
8
- if (!opts) {
9
- isInjecting = false;
10
- return;
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
- const { ctx, sessionId, text, agent = "Astro" } = opts;
14
- const prefixedText = `[${agent}]\n\n${text}`;
15
- // Basic validation
16
- if (!sessionId) {
17
- console.warn("[Astrocode] Skipping injection: No sessionId provided");
18
- return;
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
- isInjecting = false;
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
- injectionQueue.push(opts);
46
- processQueue();
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
- * Old behavior: mutate an approved story into a planning/decomposition instruction.
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 (config-driven):
35
+ * Deterministic trigger:
22
36
  * - config.workflow.genesis_planning:
23
37
  * - "off" => never attach directive
24
- * - "first_story_only"=> attach only when story_key === "S-0001"
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;