astrocode-workflow 0.3.1 → 0.3.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/shared/metrics.d.ts +66 -0
- package/dist/shared/metrics.js +112 -0
- package/dist/src/agents/commands.d.ts +9 -0
- package/dist/src/agents/commands.js +121 -0
- package/dist/src/agents/prompts.d.ts +3 -0
- package/dist/src/agents/prompts.js +232 -0
- package/dist/src/agents/registry.d.ts +6 -0
- package/dist/src/agents/registry.js +242 -0
- package/dist/src/agents/types.d.ts +14 -0
- package/dist/src/agents/types.js +8 -0
- package/dist/src/config/config-handler.d.ts +4 -0
- package/dist/src/config/config-handler.js +46 -0
- package/dist/src/config/defaults.d.ts +3 -0
- package/dist/src/config/defaults.js +3 -0
- package/dist/src/config/loader.d.ts +11 -0
- package/dist/src/config/loader.js +82 -0
- package/dist/src/config/schema.d.ts +194 -0
- package/dist/src/config/schema.js +223 -0
- package/dist/src/hooks/continuation-enforcer.d.ts +34 -0
- package/dist/src/hooks/continuation-enforcer.js +190 -0
- package/dist/src/hooks/inject-provider.d.ts +22 -0
- package/dist/src/hooks/inject-provider.js +120 -0
- package/dist/src/hooks/tool-output-truncator.d.ts +25 -0
- package/dist/src/hooks/tool-output-truncator.js +57 -0
- package/dist/src/index.d.ts +3 -0
- package/dist/src/index.js +308 -0
- package/dist/src/shared/deep-merge.d.ts +8 -0
- package/dist/src/shared/deep-merge.js +25 -0
- package/dist/src/shared/hash.d.ts +1 -0
- package/dist/src/shared/hash.js +4 -0
- package/dist/src/shared/log.d.ts +7 -0
- package/dist/src/shared/log.js +24 -0
- package/dist/src/shared/metrics.d.ts +66 -0
- package/dist/src/shared/metrics.js +112 -0
- package/dist/src/shared/model-tuning.d.ts +9 -0
- package/dist/src/shared/model-tuning.js +28 -0
- package/dist/src/shared/paths.d.ts +19 -0
- package/dist/src/shared/paths.js +64 -0
- package/dist/src/shared/text.d.ts +4 -0
- package/dist/src/shared/text.js +19 -0
- package/dist/src/shared/time.d.ts +1 -0
- package/dist/src/shared/time.js +3 -0
- package/dist/src/state/adapters/index.d.ts +41 -0
- package/dist/src/state/adapters/index.js +115 -0
- package/dist/src/state/db.d.ts +16 -0
- package/dist/src/state/db.js +225 -0
- package/dist/src/state/ids.d.ts +8 -0
- package/dist/src/state/ids.js +25 -0
- package/dist/src/state/repo-lock.d.ts +3 -0
- package/dist/src/state/repo-lock.js +29 -0
- package/dist/src/state/schema.d.ts +2 -0
- package/dist/src/state/schema.js +251 -0
- package/dist/src/state/types.d.ts +71 -0
- package/dist/src/state/types.js +1 -0
- package/dist/src/tools/artifacts.d.ts +18 -0
- package/dist/src/tools/artifacts.js +71 -0
- package/dist/src/tools/health.d.ts +8 -0
- package/dist/src/tools/health.js +119 -0
- package/dist/src/tools/index.d.ts +20 -0
- package/dist/src/tools/index.js +94 -0
- package/dist/src/tools/init.d.ts +17 -0
- package/dist/src/tools/init.js +96 -0
- package/dist/src/tools/injects.d.ts +53 -0
- package/dist/src/tools/injects.js +325 -0
- package/dist/src/tools/metrics.d.ts +7 -0
- package/dist/src/tools/metrics.js +61 -0
- package/dist/src/tools/repair.d.ts +8 -0
- package/dist/src/tools/repair.js +25 -0
- package/dist/src/tools/reset.d.ts +8 -0
- package/dist/src/tools/reset.js +92 -0
- package/dist/src/tools/run.d.ts +13 -0
- package/dist/src/tools/run.js +54 -0
- package/dist/src/tools/spec.d.ts +12 -0
- package/dist/src/tools/spec.js +44 -0
- package/dist/src/tools/stage.d.ts +23 -0
- package/dist/src/tools/stage.js +371 -0
- package/dist/src/tools/status.d.ts +8 -0
- package/dist/src/tools/status.js +125 -0
- package/dist/src/tools/story.d.ts +23 -0
- package/dist/src/tools/story.js +85 -0
- package/dist/src/tools/workflow.d.ts +13 -0
- package/dist/src/tools/workflow.js +355 -0
- package/dist/src/ui/inject.d.ts +12 -0
- package/dist/src/ui/inject.js +107 -0
- package/dist/src/ui/toasts.d.ts +13 -0
- package/dist/src/ui/toasts.js +39 -0
- package/dist/src/workflow/artifacts.d.ts +24 -0
- package/dist/src/workflow/artifacts.js +45 -0
- package/dist/src/workflow/baton.d.ts +72 -0
- package/dist/src/workflow/baton.js +166 -0
- package/dist/src/workflow/context.d.ts +20 -0
- package/dist/src/workflow/context.js +113 -0
- package/dist/src/workflow/directives.d.ts +39 -0
- package/dist/src/workflow/directives.js +137 -0
- package/dist/src/workflow/repair.d.ts +8 -0
- package/dist/src/workflow/repair.js +99 -0
- package/dist/src/workflow/state-machine.d.ts +86 -0
- package/dist/src/workflow/state-machine.js +216 -0
- package/dist/src/workflow/story-helpers.d.ts +9 -0
- package/dist/src/workflow/story-helpers.js +13 -0
- package/dist/state/db.d.ts +1 -0
- package/dist/state/db.js +9 -0
- package/dist/state/repo-lock.d.ts +3 -0
- package/dist/state/repo-lock.js +29 -0
- package/dist/test/integration/db-transactions.test.d.ts +1 -0
- package/dist/test/integration/db-transactions.test.js +126 -0
- package/dist/test/integration/injection-metrics.test.d.ts +1 -0
- package/dist/test/integration/injection-metrics.test.js +129 -0
- package/dist/tools/health.d.ts +8 -0
- package/dist/tools/health.js +119 -0
- package/dist/tools/index.js +9 -0
- package/dist/tools/metrics.d.ts +7 -0
- package/dist/tools/metrics.js +61 -0
- package/dist/tools/reset.d.ts +8 -0
- package/dist/tools/reset.js +92 -0
- package/dist/tools/workflow.js +178 -168
- package/dist/ui/inject.js +21 -9
- package/package.json +6 -3
- package/src/index.ts +8 -0
- package/src/shared/metrics.ts +148 -0
- package/src/state/db.ts +10 -1
- package/src/state/repo-lock.ts +158 -0
- package/src/tools/health.ts +128 -0
- package/src/tools/index.ts +12 -3
- package/src/tools/init.ts +26 -14
- package/src/tools/metrics.ts +71 -0
- package/src/tools/repair.ts +21 -8
- package/src/tools/reset.ts +100 -0
- package/src/tools/stage.ts +12 -0
- package/src/tools/status.ts +17 -3
- package/src/tools/story.ts +41 -15
- package/src/tools/workflow.ts +15 -1
- package/src/ui/inject.ts +21 -9
package/dist/tools/workflow.js
CHANGED
|
@@ -9,6 +9,7 @@ import { nowISO } from "../shared/time";
|
|
|
9
9
|
import { newEventId } from "../state/ids";
|
|
10
10
|
import { debug } from "../shared/log";
|
|
11
11
|
import { createToastManager } from "../ui/toasts";
|
|
12
|
+
import { acquireRepoLock } from "../state/repo-lock";
|
|
12
13
|
// Agent name mapping for case-sensitive resolution
|
|
13
14
|
export const STAGE_TO_AGENT_MAP = {
|
|
14
15
|
frame: "Frame",
|
|
@@ -155,191 +156,200 @@ export function createAstroWorkflowProceedTool(opts) {
|
|
|
155
156
|
max_steps: tool.schema.number().int().positive().default(config.workflow.default_max_steps),
|
|
156
157
|
},
|
|
157
158
|
execute: async ({ mode, max_steps }) => {
|
|
158
|
-
|
|
159
|
-
const
|
|
160
|
-
const
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
const { run_id } = withTx(db, () => createRunForStory(db, config, next.story_key));
|
|
175
|
-
actions.push(`started run ${run_id} for story ${next.story_key}`);
|
|
176
|
-
if (mode === "step")
|
|
177
|
-
break;
|
|
178
|
-
continue;
|
|
179
|
-
}
|
|
180
|
-
if (next.kind === "complete_run") {
|
|
181
|
-
withTx(db, () => completeRun(db, next.run_id, emit));
|
|
182
|
-
actions.push(`completed run ${next.run_id}`);
|
|
183
|
-
if (mode === "step")
|
|
159
|
+
// Acquire repo lock to ensure no concurrent workflow operations
|
|
160
|
+
const lockPath = `${ctx.directory}/.astro/astro.lock`;
|
|
161
|
+
const repoLock = acquireRepoLock(lockPath);
|
|
162
|
+
try {
|
|
163
|
+
const sessionId = ctx.sessionID;
|
|
164
|
+
const steps = Math.min(max_steps, config.workflow.loop_max_steps_hard_cap);
|
|
165
|
+
const actions = [];
|
|
166
|
+
const warnings = [];
|
|
167
|
+
const startedAt = nowISO();
|
|
168
|
+
// Collect UI events emitted inside state-machine functions, then flush AFTER tx.
|
|
169
|
+
const uiEvents = [];
|
|
170
|
+
const emit = (e) => uiEvents.push(e);
|
|
171
|
+
for (let i = 0; i < steps; i++) {
|
|
172
|
+
const next = decideNextAction(db, config);
|
|
173
|
+
if (next.kind === "idle") {
|
|
174
|
+
actions.push("idle: no approved stories");
|
|
184
175
|
break;
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
176
|
+
}
|
|
177
|
+
if (next.kind === "start_run") {
|
|
178
|
+
// SINGLE tx boundary: caller owns tx, state-machine is pure.
|
|
179
|
+
const { run_id } = withTx(db, () => createRunForStory(db, config, next.story_key));
|
|
180
|
+
actions.push(`started run ${run_id} for story ${next.story_key}`);
|
|
181
|
+
if (mode === "step")
|
|
182
|
+
break;
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
if (next.kind === "complete_run") {
|
|
186
|
+
withTx(db, () => completeRun(db, next.run_id, emit));
|
|
187
|
+
actions.push(`completed run ${next.run_id}`);
|
|
188
|
+
if (mode === "step")
|
|
189
|
+
break;
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
192
|
+
if (next.kind === "failed") {
|
|
193
|
+
// Ensure DB state reflects failure in one tx; emit UI event.
|
|
194
|
+
withTx(db, () => failRun(db, next.run_id, next.stage_key, next.error_text, emit));
|
|
195
|
+
actions.push(`failed: ${next.stage_key} — ${next.error_text}`);
|
|
196
|
+
if (mode === "step")
|
|
197
|
+
break;
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
if (next.kind === "delegate_stage") {
|
|
201
|
+
const active = getActiveRun(db);
|
|
202
|
+
if (!active)
|
|
203
|
+
throw new Error("Invariant: delegate_stage but no active run.");
|
|
204
|
+
const run = db.prepare("SELECT * FROM runs WHERE run_id=?").get(active.run_id);
|
|
205
|
+
const story = db.prepare("SELECT * FROM stories WHERE story_key=?").get(run.story_key);
|
|
206
|
+
let agentName = resolveAgentName(next.stage_key, config, agents, warnings);
|
|
207
|
+
const agentExists = (name) => {
|
|
208
|
+
if (agents && agents[name])
|
|
209
|
+
return true;
|
|
210
|
+
const knownStageAgents = ["Frame", "Plan", "Spec", "Implement", "Review", "Verify", "Close", "General", "Astro", "general"];
|
|
211
|
+
if (knownStageAgents.includes(name))
|
|
212
|
+
return true;
|
|
213
|
+
return false;
|
|
214
|
+
};
|
|
214
215
|
if (!agentExists(agentName)) {
|
|
215
|
-
|
|
216
|
-
agentName
|
|
216
|
+
const originalAgent = agentName;
|
|
217
|
+
console.warn(`[Astrocode] Agent ${agentName} not found. Falling back to orchestrator.`);
|
|
218
|
+
agentName = config.agents?.orchestrator_name || "Astro";
|
|
217
219
|
if (!agentExists(agentName)) {
|
|
218
|
-
|
|
220
|
+
console.warn(`[Astrocode] Orchestrator ${agentName} not available. Falling back to General.`);
|
|
221
|
+
agentName = "General";
|
|
222
|
+
if (!agentExists(agentName)) {
|
|
223
|
+
throw new Error(`Critical: No agents available for delegation. Primary: ${originalAgent}, Orchestrator: ${config.agents?.orchestrator_name || "Astro"}, General: unavailable`);
|
|
224
|
+
}
|
|
219
225
|
}
|
|
220
226
|
}
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
});
|
|
226
|
-
const context = buildContextSnapshot({
|
|
227
|
-
db,
|
|
228
|
-
config,
|
|
229
|
-
run_id: active.run_id,
|
|
230
|
-
next_action: `delegate stage ${next.stage_key}`,
|
|
231
|
-
});
|
|
232
|
-
const stageDirective = buildStageDirective({
|
|
233
|
-
config,
|
|
234
|
-
stage_key: next.stage_key,
|
|
235
|
-
run_id: active.run_id,
|
|
236
|
-
story_key: run.story_key,
|
|
237
|
-
story_title: story?.title ?? "(missing)",
|
|
238
|
-
stage_agent_name: agentName,
|
|
239
|
-
stage_goal: stageGoal(next.stage_key, config),
|
|
240
|
-
stage_constraints: stageConstraints(next.stage_key, config),
|
|
241
|
-
context_snapshot_md: context,
|
|
242
|
-
}).body;
|
|
243
|
-
const delegatePrompt = buildDelegationPrompt({
|
|
244
|
-
stageDirective,
|
|
245
|
-
run_id: active.run_id,
|
|
246
|
-
stage_key: next.stage_key,
|
|
247
|
-
stage_agent_name: agentName,
|
|
248
|
-
});
|
|
249
|
-
// Record continuation (best-effort; no tx wrapper needed but safe either way)
|
|
250
|
-
const h = directiveHash(delegatePrompt);
|
|
251
|
-
const now = nowISO();
|
|
252
|
-
if (sessionId) {
|
|
253
|
-
// This assumes continuations table exists in vNext schema.
|
|
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);
|
|
255
|
-
}
|
|
256
|
-
// Visible injection so user can see state (awaited)
|
|
257
|
-
if (sessionId) {
|
|
258
|
-
await injectChatPrompt({ ctx, sessionId, text: delegatePrompt, agent: "Astro" });
|
|
259
|
-
const continueMessage = [
|
|
260
|
-
`[SYSTEM DIRECTIVE: ASTROCODE — AWAIT_STAGE_COMPLETION]`,
|
|
261
|
-
``,
|
|
262
|
-
`Stage \`${next.stage_key}\` delegated to \`${agentName}\`.`,
|
|
263
|
-
``,
|
|
264
|
-
`When \`${agentName}\` completes, call:`,
|
|
265
|
-
`astro_stage_complete(run_id="${active.run_id}", stage_key="${next.stage_key}", output_text="[paste subagent output here]")`,
|
|
266
|
-
``,
|
|
267
|
-
`This advances the workflow.`,
|
|
268
|
-
].join("\n");
|
|
269
|
-
await injectChatPrompt({ ctx, sessionId, text: continueMessage, agent: "Astro" });
|
|
270
|
-
}
|
|
271
|
-
actions.push(`delegated stage ${next.stage_key} via ${agentName}`);
|
|
272
|
-
// Stop here; subagent needs to run.
|
|
273
|
-
break;
|
|
274
|
-
}
|
|
275
|
-
if (next.kind === "await_stage_completion") {
|
|
276
|
-
actions.push(`await stage completion: ${next.stage_key}`);
|
|
277
|
-
if (sessionId) {
|
|
227
|
+
// NOTE: startStage owns its own tx (state-machine.ts).
|
|
228
|
+
withTx(db, () => {
|
|
229
|
+
startStage(db, active.run_id, next.stage_key, { subagent_type: agentName }, emit);
|
|
230
|
+
});
|
|
278
231
|
const context = buildContextSnapshot({
|
|
279
232
|
db,
|
|
280
233
|
config,
|
|
281
|
-
run_id:
|
|
282
|
-
next_action: `
|
|
234
|
+
run_id: active.run_id,
|
|
235
|
+
next_action: `delegate stage ${next.stage_key}`,
|
|
236
|
+
});
|
|
237
|
+
const stageDirective = buildStageDirective({
|
|
238
|
+
config,
|
|
239
|
+
stage_key: next.stage_key,
|
|
240
|
+
run_id: active.run_id,
|
|
241
|
+
story_key: run.story_key,
|
|
242
|
+
story_title: story?.title ?? "(missing)",
|
|
243
|
+
stage_agent_name: agentName,
|
|
244
|
+
stage_goal: stageGoal(next.stage_key, config),
|
|
245
|
+
stage_constraints: stageConstraints(next.stage_key, config),
|
|
246
|
+
context_snapshot_md: context,
|
|
247
|
+
}).body;
|
|
248
|
+
const delegatePrompt = buildDelegationPrompt({
|
|
249
|
+
stageDirective,
|
|
250
|
+
run_id: active.run_id,
|
|
251
|
+
stage_key: next.stage_key,
|
|
252
|
+
stage_agent_name: agentName,
|
|
283
253
|
});
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
``,
|
|
287
|
-
`Run \`${next.run_id}\` is waiting for stage \`${next.stage_key}\` output.`,
|
|
288
|
-
`If you have the subagent output, call astro_stage_complete with output_text=the FULL output.`,
|
|
289
|
-
``,
|
|
290
|
-
`Context snapshot:`,
|
|
291
|
-
context,
|
|
292
|
-
].join("\n").trim();
|
|
293
|
-
const h = directiveHash(prompt);
|
|
254
|
+
// Record continuation (best-effort; no tx wrapper needed but safe either way)
|
|
255
|
+
const h = directiveHash(delegatePrompt);
|
|
294
256
|
const now = nowISO();
|
|
295
|
-
|
|
296
|
-
|
|
257
|
+
if (sessionId) {
|
|
258
|
+
// This assumes continuations table exists in vNext schema.
|
|
259
|
+
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);
|
|
260
|
+
}
|
|
261
|
+
// Visible injection so user can see state (awaited)
|
|
262
|
+
if (sessionId) {
|
|
263
|
+
await injectChatPrompt({ ctx, sessionId, text: delegatePrompt, agent: "Astro" });
|
|
264
|
+
const continueMessage = [
|
|
265
|
+
`[SYSTEM DIRECTIVE: ASTROCODE — AWAIT_STAGE_COMPLETION]`,
|
|
266
|
+
``,
|
|
267
|
+
`Stage \`${next.stage_key}\` delegated to \`${agentName}\`.`,
|
|
268
|
+
``,
|
|
269
|
+
`When \`${agentName}\` completes, call:`,
|
|
270
|
+
`astro_stage_complete(run_id="${active.run_id}", stage_key="${next.stage_key}", output_text="[paste subagent output here]")`,
|
|
271
|
+
``,
|
|
272
|
+
`This advances the workflow.`,
|
|
273
|
+
].join("\n");
|
|
274
|
+
await injectChatPrompt({ ctx, sessionId, text: continueMessage, agent: "Astro" });
|
|
275
|
+
}
|
|
276
|
+
actions.push(`delegated stage ${next.stage_key} via ${agentName}`);
|
|
277
|
+
// Stop here; subagent needs to run.
|
|
278
|
+
break;
|
|
279
|
+
}
|
|
280
|
+
if (next.kind === "await_stage_completion") {
|
|
281
|
+
actions.push(`await stage completion: ${next.stage_key}`);
|
|
282
|
+
if (sessionId) {
|
|
283
|
+
const context = buildContextSnapshot({
|
|
284
|
+
db,
|
|
285
|
+
config,
|
|
286
|
+
run_id: next.run_id,
|
|
287
|
+
next_action: `complete stage ${next.stage_key}`,
|
|
288
|
+
});
|
|
289
|
+
const prompt = [
|
|
290
|
+
`[SYSTEM DIRECTIVE: ASTROCODE — AWAIT_STAGE_OUTPUT]`,
|
|
291
|
+
``,
|
|
292
|
+
`Run \`${next.run_id}\` is waiting for stage \`${next.stage_key}\` output.`,
|
|
293
|
+
`If you have the subagent output, call astro_stage_complete with output_text=the FULL output.`,
|
|
294
|
+
``,
|
|
295
|
+
`Context snapshot:`,
|
|
296
|
+
context,
|
|
297
|
+
].join("\n").trim();
|
|
298
|
+
const h = directiveHash(prompt);
|
|
299
|
+
const now = nowISO();
|
|
300
|
+
db.prepare("INSERT INTO continuations (session_id, run_id, directive_hash, kind, reason, created_at) VALUES (?, ?, ?, 'continue', ?, ?)").run(sessionId, next.run_id, h, `await ${next.stage_key}`, now);
|
|
301
|
+
await injectChatPrompt({ ctx, sessionId, text: prompt, agent: "Astro" });
|
|
302
|
+
}
|
|
303
|
+
break;
|
|
297
304
|
}
|
|
305
|
+
actions.push(`unhandled next action: ${next.kind}`);
|
|
298
306
|
break;
|
|
299
307
|
}
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
agent: "Astro",
|
|
320
|
-
});
|
|
308
|
+
// Flush UI events (toast + prompt) AFTER state transitions
|
|
309
|
+
if (uiEvents.length > 0) {
|
|
310
|
+
for (const e of uiEvents) {
|
|
311
|
+
const msg = buildUiMessage(e);
|
|
312
|
+
if (config.ui.toasts.enabled) {
|
|
313
|
+
await toasts.show({
|
|
314
|
+
title: msg.title,
|
|
315
|
+
message: msg.message,
|
|
316
|
+
variant: msg.variant,
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
if (ctx?.sessionID) {
|
|
320
|
+
await injectChatPrompt({
|
|
321
|
+
ctx,
|
|
322
|
+
sessionId: ctx.sessionID,
|
|
323
|
+
text: msg.chatText,
|
|
324
|
+
agent: "Astro",
|
|
325
|
+
});
|
|
326
|
+
}
|
|
321
327
|
}
|
|
328
|
+
actions.push(`ui: flushed ${uiEvents.length} event(s)`);
|
|
329
|
+
}
|
|
330
|
+
// Housekeeping event
|
|
331
|
+
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());
|
|
332
|
+
const active = getActiveRun(db);
|
|
333
|
+
const lines = [];
|
|
334
|
+
lines.push(`# astro_workflow_proceed`);
|
|
335
|
+
lines.push(`- mode: ${mode}`);
|
|
336
|
+
lines.push(`- steps requested: ${max_steps} (cap=${steps})`);
|
|
337
|
+
if (active)
|
|
338
|
+
lines.push(`- active run: \`${active.run_id}\` (stage=${active.current_stage_key ?? "?"})`);
|
|
339
|
+
lines.push(``, `## Actions`);
|
|
340
|
+
for (const a of actions)
|
|
341
|
+
lines.push(`- ${a}`);
|
|
342
|
+
if (warnings.length > 0) {
|
|
343
|
+
lines.push(``, `## Warnings`);
|
|
344
|
+
for (const w of warnings)
|
|
345
|
+
lines.push(`⚠️ ${w}`);
|
|
322
346
|
}
|
|
323
|
-
|
|
347
|
+
return lines.join("\n").trim();
|
|
324
348
|
}
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
const lines = [];
|
|
329
|
-
lines.push(`# astro_workflow_proceed`);
|
|
330
|
-
lines.push(`- mode: ${mode}`);
|
|
331
|
-
lines.push(`- steps requested: ${max_steps} (cap=${steps})`);
|
|
332
|
-
if (active)
|
|
333
|
-
lines.push(`- active run: \`${active.run_id}\` (stage=${active.current_stage_key ?? "?"})`);
|
|
334
|
-
lines.push(``, `## Actions`);
|
|
335
|
-
for (const a of actions)
|
|
336
|
-
lines.push(`- ${a}`);
|
|
337
|
-
if (warnings.length > 0) {
|
|
338
|
-
lines.push(``, `## Warnings`);
|
|
339
|
-
for (const w of warnings)
|
|
340
|
-
lines.push(`⚠️ ${w}`);
|
|
349
|
+
finally {
|
|
350
|
+
// Always release the lock
|
|
351
|
+
repoLock.release();
|
|
341
352
|
}
|
|
342
|
-
return lines.join("\n").trim();
|
|
343
353
|
},
|
|
344
354
|
});
|
|
345
355
|
}
|
package/dist/ui/inject.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
// src/ui/inject.ts
|
|
2
|
+
import { recordInjection, recordError } from "../shared/metrics";
|
|
1
3
|
const MAX_ATTEMPTS = 4;
|
|
2
4
|
const RETRY_DELAYS_MS = [250, 500, 1000, 2000]; // attempt 1..4
|
|
3
5
|
// Per-session queues so one stuck session doesn't block others
|
|
@@ -17,15 +19,24 @@ function getPromptInvoker(ctx) {
|
|
|
17
19
|
async function tryInjectOnce(item) {
|
|
18
20
|
const { ctx, sessionId, text, agent = "Astro" } = item;
|
|
19
21
|
const prefixedText = `[${agent}]\n\n${text}`;
|
|
20
|
-
const {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
22
|
+
const injectionRecorder = recordInjection({ sessionId, attempts: item.attempts + 1, agent });
|
|
23
|
+
const injectionStart = injectionRecorder.start();
|
|
24
|
+
try {
|
|
25
|
+
const { session, prompt } = getPromptInvoker(ctx);
|
|
26
|
+
// IMPORTANT: force correct `this` binding
|
|
27
|
+
await prompt.call(session, {
|
|
28
|
+
path: { id: sessionId },
|
|
29
|
+
body: {
|
|
30
|
+
parts: [{ type: "text", text: prefixedText }],
|
|
31
|
+
agent,
|
|
32
|
+
},
|
|
33
|
+
});
|
|
34
|
+
injectionRecorder.end(injectionStart, true);
|
|
35
|
+
}
|
|
36
|
+
catch (error) {
|
|
37
|
+
injectionRecorder.end(injectionStart, false);
|
|
38
|
+
throw error;
|
|
39
|
+
}
|
|
29
40
|
}
|
|
30
41
|
async function runSessionQueue(sessionId) {
|
|
31
42
|
if (running.has(sessionId))
|
|
@@ -48,6 +59,7 @@ async function runSessionQueue(sessionId) {
|
|
|
48
59
|
const delay = RETRY_DELAYS_MS[Math.min(item.attempts - 1, RETRY_DELAYS_MS.length - 1)] ?? 2000;
|
|
49
60
|
if (item.attempts >= MAX_ATTEMPTS) {
|
|
50
61
|
console.warn(`[Astrocode] Injection failed permanently after ${item.attempts} attempts: ${msg}`);
|
|
62
|
+
recordError("injection_failure", `Injection failed after ${item.attempts} attempts: ${msg}`);
|
|
51
63
|
item.reject(err);
|
|
52
64
|
continue;
|
|
53
65
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "astrocode-workflow",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -13,7 +13,9 @@
|
|
|
13
13
|
"scripts": {
|
|
14
14
|
"build": "tsc -p tsconfig.json",
|
|
15
15
|
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
16
|
-
"clean": "rm -rf dist"
|
|
16
|
+
"clean": "rm -rf dist",
|
|
17
|
+
"test": "vitest",
|
|
18
|
+
"test:run": "vitest run"
|
|
17
19
|
},
|
|
18
20
|
"dependencies": {
|
|
19
21
|
"@opencode-ai/plugin": "^1.1.19",
|
|
@@ -28,6 +30,7 @@
|
|
|
28
30
|
"devDependencies": {
|
|
29
31
|
"@types/better-sqlite3": "^7.6.12",
|
|
30
32
|
"@types/node": "^20.12.12",
|
|
31
|
-
"typescript": "^5.6.3"
|
|
33
|
+
"typescript": "^5.6.3",
|
|
34
|
+
"vitest": "^1.6.0"
|
|
32
35
|
}
|
|
33
36
|
}
|
package/src/index.ts
CHANGED
|
@@ -13,6 +13,7 @@ import { createToastManager, type ToastOptions } from "./ui/toasts";
|
|
|
13
13
|
import { createAstroAgents } from "./agents/registry";
|
|
14
14
|
import type { AgentConfig } from "@opencode-ai/sdk";
|
|
15
15
|
import { info, warn } from "./shared/log";
|
|
16
|
+
import { acquireRepoLock } from "./state/repo-lock";
|
|
16
17
|
|
|
17
18
|
// Type definitions for plugin components
|
|
18
19
|
type ConfigHandler = (config: Record<string, any>) => Promise<void>;
|
|
@@ -58,6 +59,10 @@ const Astrocode: Plugin = async (ctx) => {
|
|
|
58
59
|
}
|
|
59
60
|
const repoRoot = ctx.directory;
|
|
60
61
|
|
|
62
|
+
// Acquire exclusive repo lock to prevent multiple processes from corrupting the database
|
|
63
|
+
const lockPath = `${repoRoot}/.astro/astro.lock`;
|
|
64
|
+
const repoLock = acquireRepoLock(lockPath);
|
|
65
|
+
|
|
61
66
|
// Always load config first - this provides defaults even in limited mode
|
|
62
67
|
let pluginConfig: AstrocodeConfig;
|
|
63
68
|
try {
|
|
@@ -325,6 +330,9 @@ const Astrocode: Plugin = async (ctx) => {
|
|
|
325
330
|
|
|
326
331
|
// Best-effort cleanup
|
|
327
332
|
close: async () => {
|
|
333
|
+
// Release repo lock first (important for process termination)
|
|
334
|
+
repoLock.release();
|
|
335
|
+
|
|
328
336
|
if (db && typeof db.close === "function") {
|
|
329
337
|
try {
|
|
330
338
|
db.close();
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
// src/shared/metrics.ts
|
|
2
|
+
|
|
3
|
+
export interface TransactionMetrics {
|
|
4
|
+
startTime: number;
|
|
5
|
+
duration: number;
|
|
6
|
+
success: boolean;
|
|
7
|
+
nestedDepth: number;
|
|
8
|
+
operation?: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface InjectionMetrics {
|
|
12
|
+
sessionId: string;
|
|
13
|
+
attempts: number;
|
|
14
|
+
duration: number;
|
|
15
|
+
success: boolean;
|
|
16
|
+
agent?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface SystemMetrics {
|
|
20
|
+
transactions: TransactionMetrics[];
|
|
21
|
+
injections: InjectionMetrics[];
|
|
22
|
+
errors: Array<{ type: string; message: string; timestamp: number }>;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
class MetricsCollector {
|
|
26
|
+
private transactions: TransactionMetrics[] = [];
|
|
27
|
+
private injections: InjectionMetrics[] = [];
|
|
28
|
+
private errors: Array<{ type: string; message: string; timestamp: number }> = [];
|
|
29
|
+
private maxEntries = 1000; // Keep last 1000 entries per type
|
|
30
|
+
|
|
31
|
+
recordTransaction(metrics: TransactionMetrics) {
|
|
32
|
+
this.transactions.push(metrics);
|
|
33
|
+
if (this.transactions.length > this.maxEntries) {
|
|
34
|
+
this.transactions.shift();
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
recordInjection(metrics: InjectionMetrics) {
|
|
39
|
+
this.injections.push(metrics);
|
|
40
|
+
if (this.injections.length > this.maxEntries) {
|
|
41
|
+
this.injections.shift();
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
recordError(type: string, message: string) {
|
|
46
|
+
this.errors.push({ type, message, timestamp: Date.now() });
|
|
47
|
+
if (this.errors.length > this.maxEntries) {
|
|
48
|
+
this.errors.shift();
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
getMetrics(): SystemMetrics {
|
|
53
|
+
return {
|
|
54
|
+
transactions: [...this.transactions],
|
|
55
|
+
injections: [...this.injections],
|
|
56
|
+
errors: [...this.errors],
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
getTransactionStats() {
|
|
61
|
+
const txs = this.transactions;
|
|
62
|
+
if (txs.length === 0) return null;
|
|
63
|
+
|
|
64
|
+
const successful = txs.filter(t => t.success);
|
|
65
|
+
const failed = txs.filter(t => !t.success);
|
|
66
|
+
|
|
67
|
+
return {
|
|
68
|
+
total: txs.length,
|
|
69
|
+
successful: successful.length,
|
|
70
|
+
failed: failed.length,
|
|
71
|
+
successRate: successful.length / txs.length,
|
|
72
|
+
avgDuration: txs.reduce((sum, t) => sum + t.duration, 0) / txs.length,
|
|
73
|
+
avgNestedDepth: txs.reduce((sum, t) => sum + t.nestedDepth, 0) / txs.length,
|
|
74
|
+
minDuration: Math.min(...txs.map(t => t.duration)),
|
|
75
|
+
maxDuration: Math.max(...txs.map(t => t.duration)),
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
getInjectionStats() {
|
|
80
|
+
const injections = this.injections;
|
|
81
|
+
if (injections.length === 0) return null;
|
|
82
|
+
|
|
83
|
+
const successful = injections.filter(i => i.success);
|
|
84
|
+
const failed = injections.filter(i => !i.success);
|
|
85
|
+
|
|
86
|
+
return {
|
|
87
|
+
total: injections.length,
|
|
88
|
+
successful: successful.length,
|
|
89
|
+
failed: failed.length,
|
|
90
|
+
successRate: successful.length / injections.length,
|
|
91
|
+
avgAttempts: injections.reduce((sum, i) => sum + i.attempts, 0) / injections.length,
|
|
92
|
+
avgDuration: injections.reduce((sum, i) => sum + i.duration, 0) / injections.length,
|
|
93
|
+
totalRetries: injections.reduce((sum, i) => sum + Math.max(0, i.attempts - 1), 0),
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
clear() {
|
|
98
|
+
this.transactions = [];
|
|
99
|
+
this.injections = [];
|
|
100
|
+
this.errors = [];
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Global singleton
|
|
105
|
+
export const metrics = new MetricsCollector();
|
|
106
|
+
|
|
107
|
+
// Convenience functions
|
|
108
|
+
export function recordTransaction(metricsData: Omit<TransactionMetrics, 'startTime' | 'duration' | 'success'>) {
|
|
109
|
+
return {
|
|
110
|
+
start() {
|
|
111
|
+
return {
|
|
112
|
+
...metricsData,
|
|
113
|
+
startTime: Date.now(),
|
|
114
|
+
};
|
|
115
|
+
},
|
|
116
|
+
end(startData: ReturnType<ReturnType<typeof recordTransaction>['start']>, success: boolean) {
|
|
117
|
+
const duration = Date.now() - startData.startTime;
|
|
118
|
+
metrics.recordTransaction({
|
|
119
|
+
...startData,
|
|
120
|
+
duration,
|
|
121
|
+
success,
|
|
122
|
+
});
|
|
123
|
+
},
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export function recordInjection(metricsData: Omit<InjectionMetrics, 'duration' | 'success'>) {
|
|
128
|
+
return {
|
|
129
|
+
start() {
|
|
130
|
+
return {
|
|
131
|
+
...metricsData,
|
|
132
|
+
startTime: Date.now(),
|
|
133
|
+
};
|
|
134
|
+
},
|
|
135
|
+
end(startData: any, success: boolean) {
|
|
136
|
+
const duration = Date.now() - startData.startTime;
|
|
137
|
+
metrics.recordInjection({
|
|
138
|
+
...startData,
|
|
139
|
+
duration,
|
|
140
|
+
success,
|
|
141
|
+
});
|
|
142
|
+
},
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export function recordError(type: string, message: string) {
|
|
147
|
+
metrics.recordError(type, message);
|
|
148
|
+
}
|