astrocode-workflow 0.1.58 → 0.2.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.
Files changed (54) hide show
  1. package/README.md +243 -11
  2. package/dist/agents/prompts.d.ts +1 -0
  3. package/dist/agents/prompts.js +159 -0
  4. package/dist/agents/registry.js +11 -1
  5. package/dist/config/loader.js +34 -0
  6. package/dist/config/schema.d.ts +7 -1
  7. package/dist/config/schema.js +2 -0
  8. package/dist/hooks/continuation-enforcer.d.ts +9 -1
  9. package/dist/hooks/continuation-enforcer.js +2 -1
  10. package/dist/hooks/inject-provider.d.ts +9 -1
  11. package/dist/hooks/inject-provider.js +2 -1
  12. package/dist/hooks/tool-output-truncator.d.ts +9 -1
  13. package/dist/hooks/tool-output-truncator.js +2 -1
  14. package/dist/index.js +228 -45
  15. package/dist/state/adapters/index.d.ts +4 -2
  16. package/dist/state/adapters/index.js +23 -27
  17. package/dist/state/db.d.ts +6 -8
  18. package/dist/state/db.js +106 -45
  19. package/dist/tools/index.d.ts +13 -3
  20. package/dist/tools/index.js +14 -31
  21. package/dist/tools/init.d.ts +10 -1
  22. package/dist/tools/init.js +73 -18
  23. package/dist/tools/injects.js +90 -26
  24. package/dist/tools/spec.d.ts +0 -1
  25. package/dist/tools/spec.js +4 -1
  26. package/dist/tools/status.d.ts +1 -1
  27. package/dist/tools/status.js +70 -52
  28. package/dist/tools/workflow.js +2 -2
  29. package/dist/ui/inject.d.ts +16 -2
  30. package/dist/ui/inject.js +104 -33
  31. package/dist/workflow/directives.d.ts +2 -0
  32. package/dist/workflow/directives.js +34 -19
  33. package/dist/workflow/state-machine.d.ts +46 -3
  34. package/dist/workflow/state-machine.js +249 -92
  35. package/package.json +1 -1
  36. package/src/agents/prompts.ts +160 -0
  37. package/src/agents/registry.ts +16 -1
  38. package/src/config/loader.ts +39 -4
  39. package/src/config/schema.ts +3 -0
  40. package/src/hooks/continuation-enforcer.ts +9 -2
  41. package/src/hooks/inject-provider.ts +9 -2
  42. package/src/hooks/tool-output-truncator.ts +9 -2
  43. package/src/index.ts +260 -56
  44. package/src/state/adapters/index.ts +21 -26
  45. package/src/state/db.ts +114 -58
  46. package/src/tools/index.ts +29 -31
  47. package/src/tools/init.ts +91 -22
  48. package/src/tools/injects.ts +147 -53
  49. package/src/tools/spec.ts +6 -2
  50. package/src/tools/status.ts +71 -55
  51. package/src/tools/workflow.ts +3 -3
  52. package/src/ui/inject.ts +115 -41
  53. package/src/workflow/directives.ts +103 -75
  54. package/src/workflow/state-machine.ts +327 -109
@@ -1,6 +1,20 @@
1
- export declare function injectChatPrompt(opts: {
1
+ type QueueItem = {
2
2
  ctx: any;
3
3
  sessionId: string;
4
4
  text: string;
5
5
  agent?: string;
6
- }): Promise<void>;
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 {};
package/dist/ui/inject.js CHANGED
@@ -1,47 +1,118 @@
1
- let isInjecting = false;
1
+ // src/ui/inject.ts
2
+ //
3
+ // Deterministic chat injection:
4
+ // - Always enqueue
5
+ // - Process sequentially (per-process single worker)
6
+ // - Retries with backoff
7
+ // - flush() lets callers wait until injections are actually sent
8
+ //
9
+ // IMPORTANT: Callers who need reliability must `await injectChatPrompt(...)`
10
+ // or `await flushChatPrompts()` after enqueueing.
2
11
  const injectionQueue = [];
3
- async function processQueue() {
4
- if (isInjecting || injectionQueue.length === 0)
12
+ let workerRunning = false;
13
+ // Used to let callers await "queue drained"
14
+ let drainWaiters = [];
15
+ function sleep(ms) {
16
+ return new Promise((resolve) => setTimeout(resolve, ms));
17
+ }
18
+ function resolveDrainWaitersIfIdle() {
19
+ if (workerRunning)
5
20
  return;
6
- isInjecting = true;
7
- const opts = injectionQueue.shift();
8
- if (!opts) {
9
- isInjecting = false;
21
+ if (injectionQueue.length !== 0)
22
+ return;
23
+ const waiters = drainWaiters;
24
+ drainWaiters = [];
25
+ for (const w of waiters)
26
+ w();
27
+ }
28
+ function getPromptApi(ctx) {
29
+ const fn = ctx?.client?.session?.prompt;
30
+ return typeof fn === "function" ? fn.bind(ctx.client.session) : null;
31
+ }
32
+ async function sendWithRetries(opts) {
33
+ const { ctx, sessionId, text } = opts;
34
+ const agent = opts.agent ?? "Astro";
35
+ const prefixedText = `[${agent}]\n\n${text}`;
36
+ if (!sessionId) {
37
+ console.warn("[Astrocode] Injection skipped: missing sessionId");
10
38
  return;
11
39
  }
12
- 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");
40
+ const prompt = getPromptApi(ctx);
41
+ if (!prompt) {
42
+ console.warn("[Astrocode] Injection skipped: ctx.client.session.prompt unavailable");
43
+ return;
44
+ }
45
+ const maxAttempts = 3;
46
+ let attempt = 0;
47
+ while (attempt < maxAttempts) {
48
+ attempt += 1;
49
+ try {
50
+ await prompt({
51
+ path: { id: sessionId },
52
+ body: {
53
+ parts: [{ type: "text", text: prefixedText }],
54
+ agent,
55
+ },
56
+ });
18
57
  return;
19
58
  }
20
- if (!ctx?.client?.session?.prompt) {
21
- console.warn("[Astrocode] Skipping injection: API not available (ctx.client.session.prompt)");
22
- return;
59
+ catch (err) {
60
+ const msg = err instanceof Error ? err.message : String(err);
61
+ const isLast = attempt >= maxAttempts;
62
+ if (isLast) {
63
+ console.warn(`[Astrocode] Injection failed (final): ${msg}`);
64
+ return;
65
+ }
66
+ // Exponential backoff + jitter
67
+ const base = 150 * Math.pow(2, attempt - 1); // 150, 300, 600
68
+ const jitter = Math.floor(Math.random() * 120);
69
+ await sleep(base + jitter);
23
70
  }
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
71
  }
33
- catch (error) {
34
- console.warn(`[Astrocode] Injection failed: ${error}`);
72
+ }
73
+ async function runWorkerLoop() {
74
+ if (workerRunning)
75
+ return;
76
+ workerRunning = true;
77
+ try {
78
+ // Drain sequentially to preserve ordering
79
+ while (injectionQueue.length > 0) {
80
+ const item = injectionQueue.shift();
81
+ if (!item)
82
+ continue;
83
+ await sendWithRetries(item);
84
+ }
35
85
  }
36
86
  finally {
37
- isInjecting = false;
38
- // Process next item immediately
39
- if (injectionQueue.length > 0) {
40
- setImmediate(processQueue);
41
- }
87
+ workerRunning = false;
88
+ resolveDrainWaitersIfIdle();
42
89
  }
43
90
  }
44
- export async function injectChatPrompt(opts) {
91
+ /**
92
+ * Enqueue an injection and ensure the worker is running.
93
+ * Does NOT wait for delivery — use `flushChatPrompts()` to wait.
94
+ */
95
+ export function enqueueChatPrompt(opts) {
45
96
  injectionQueue.push(opts);
46
- processQueue();
97
+ // Kick worker
98
+ void runWorkerLoop();
99
+ }
100
+ /**
101
+ * Wait until all queued injections have been processed (sent or exhausted retries).
102
+ */
103
+ export function flushChatPrompts() {
104
+ if (!workerRunning && injectionQueue.length === 0)
105
+ return Promise.resolve();
106
+ return new Promise((resolve) => {
107
+ drainWaiters.push(resolve);
108
+ // Ensure worker is running (in case someone enqueued without kick)
109
+ void runWorkerLoop();
110
+ });
111
+ }
112
+ /**
113
+ * Deterministic helper: enqueue + flush (recommended for stage boundaries).
114
+ */
115
+ export async function injectChatPrompt(opts) {
116
+ enqueueChatPrompt(opts);
117
+ await flushChatPrompts();
47
118
  }
@@ -16,12 +16,14 @@ export declare function buildContinueDirective(opts: {
16
16
  context_snapshot_md: string;
17
17
  }): BuiltDirective;
18
18
  export declare function buildBlockedDirective(opts: {
19
+ config?: AstrocodeConfig;
19
20
  run_id: string;
20
21
  stage_key: string;
21
22
  question: string;
22
23
  context_snapshot_md: string;
23
24
  }): BuiltDirective;
24
25
  export declare function buildRepairDirective(opts: {
26
+ config?: AstrocodeConfig;
25
27
  report_md: string;
26
28
  }): BuiltDirective;
27
29
  export declare function buildStageDirective(opts: {
@@ -1,13 +1,25 @@
1
1
  import { sha256Hex } from "../shared/hash";
2
2
  import { clampChars, normalizeNewlines } from "../shared/text";
3
+ function getInjectMaxChars(config) {
4
+ // Deterministic fallback for older configs.
5
+ const v = config?.context_compaction?.inject_max_chars;
6
+ return typeof v === "number" && Number.isFinite(v) && v > 0 ? v : 12000;
7
+ }
3
8
  export function directiveHash(body) {
4
9
  // Stable hash to dedupe: normalize newlines + trim
5
10
  const norm = normalizeNewlines(body).trim();
6
11
  return sha256Hex(norm);
7
12
  }
13
+ function finalizeBody(body, maxChars) {
14
+ // Normalize first, clamp second, trim last => hash/body match exactly.
15
+ const norm = normalizeNewlines(body);
16
+ const clamped = clampChars(norm, maxChars);
17
+ return clamped.trim();
18
+ }
8
19
  export function buildContinueDirective(opts) {
9
20
  const { config, run_id, stage_key, next_action, context_snapshot_md } = opts;
10
- const body = clampChars(normalizeNewlines([
21
+ const maxChars = getInjectMaxChars(config);
22
+ const body = finalizeBody([
11
23
  `[SYSTEM DIRECTIVE: ASTROCODE — CONTINUE]`,
12
24
  ``,
13
25
  `This directive is injected by the Astro agent to continue the workflow.`,
@@ -22,8 +34,8 @@ export function buildContinueDirective(opts) {
22
34
  `- If blocked, ask exactly ONE question and stop.`,
23
35
  ``,
24
36
  `Context snapshot:`,
25
- context_snapshot_md.trim(),
26
- ].join("\n")), config.context_compaction.inject_max_chars);
37
+ (context_snapshot_md ?? "").trim(),
38
+ ].join("\n"), maxChars);
27
39
  return {
28
40
  kind: "continue",
29
41
  title: "ASTROCODE — CONTINUE",
@@ -32,8 +44,9 @@ export function buildContinueDirective(opts) {
32
44
  };
33
45
  }
34
46
  export function buildBlockedDirective(opts) {
35
- const { run_id, stage_key, question, context_snapshot_md } = opts;
36
- const body = normalizeNewlines([
47
+ const { config, run_id, stage_key, question, context_snapshot_md } = opts;
48
+ const maxChars = getInjectMaxChars(config);
49
+ const body = finalizeBody([
37
50
  `[SYSTEM DIRECTIVE: ASTROCODE — BLOCKED]`,
38
51
  ``,
39
52
  `This directive is injected by the Astro agent indicating the workflow is blocked.`,
@@ -45,8 +58,8 @@ export function buildBlockedDirective(opts) {
45
58
  `Question: ${question}`,
46
59
  ``,
47
60
  `Context snapshot:`,
48
- context_snapshot_md.trim(),
49
- ].join("\n")).trim();
61
+ (context_snapshot_md ?? "").trim(),
62
+ ].join("\n"), maxChars);
50
63
  return {
51
64
  kind: "blocked",
52
65
  title: "ASTROCODE — BLOCKED",
@@ -55,7 +68,8 @@ export function buildBlockedDirective(opts) {
55
68
  };
56
69
  }
57
70
  export function buildRepairDirective(opts) {
58
- const body = normalizeNewlines([
71
+ const maxChars = getInjectMaxChars(opts.config);
72
+ const body = finalizeBody([
59
73
  `[SYSTEM DIRECTIVE: ASTROCODE — REPAIR]`,
60
74
  ``,
61
75
  `This directive is injected by the Astro agent after performing a repair pass.`,
@@ -63,8 +77,8 @@ export function buildRepairDirective(opts) {
63
77
  `Astrocode performed a repair pass. Summarize in <= 10 lines and continue.`,
64
78
  ``,
65
79
  `Repair report:`,
66
- opts.report_md.trim(),
67
- ].join("\n")).trim();
80
+ (opts.report_md ?? "").trim(),
81
+ ].join("\n"), maxChars);
68
82
  return {
69
83
  kind: "repair",
70
84
  title: "ASTROCODE — REPAIR",
@@ -73,12 +87,13 @@ export function buildRepairDirective(opts) {
73
87
  };
74
88
  }
75
89
  export function buildStageDirective(opts) {
76
- const { config, stage_key, run_id, story_key, story_title, stage_agent_name, stage_goal, stage_constraints, context_snapshot_md } = opts;
77
- const stageKeyUpper = stage_key.toUpperCase();
78
- const constraintsBlock = stage_constraints.length
90
+ const { config, stage_key, run_id, story_key, story_title, stage_agent_name, stage_goal, stage_constraints, context_snapshot_md, } = opts;
91
+ const maxChars = getInjectMaxChars(config);
92
+ const stageKeyUpper = String(stage_key).toUpperCase();
93
+ const constraintsBlock = Array.isArray(stage_constraints) && stage_constraints.length
79
94
  ? ["", "Constraints:", ...stage_constraints.map((c) => `- ${c}`)].join("\n")
80
95
  : "";
81
- const body = clampChars(normalizeNewlines([
96
+ const body = finalizeBody([
82
97
  `[SYSTEM DIRECTIVE: ASTROCODE — STAGE_${stageKeyUpper}]`,
83
98
  ``,
84
99
  `This directive is injected by the Astro agent to delegate the stage task.`,
@@ -93,14 +108,14 @@ export function buildStageDirective(opts) {
93
108
  `Output contract (strict):`,
94
109
  `1) Baton markdown (short, structured)`,
95
110
  `2) ASTRO JSON between markers:`,
96
- ` ${"<!-- ASTRO_JSON_BEGIN -->"}`,
111
+ ` <!-- ASTRO_JSON_BEGIN -->`,
97
112
  ` {`,
98
113
  ` "schema_version": 1,`,
99
114
  ` "stage_key": "${stage_key}",`,
100
115
  ` "status": "ok",`,
101
- ` ...`,
116
+ ` "...": "..."`,
102
117
  ` }`,
103
- ` ${"<!-- ASTRO_JSON_END -->"}`,
118
+ ` <!-- ASTRO_JSON_END -->`,
104
119
  ``,
105
120
  `ASTRO JSON requirements:`,
106
121
  `- stage_key must be "${stage_key}"`,
@@ -111,8 +126,8 @@ export function buildStageDirective(opts) {
111
126
  `If blocked: ask exactly ONE question and stop.`,
112
127
  ``,
113
128
  `Context snapshot:`,
114
- context_snapshot_md.trim(),
115
- ].join("\n")), config.context_compaction.inject_max_chars);
129
+ (context_snapshot_md ?? "").trim(),
130
+ ].join("\n"), maxChars);
116
131
  return {
117
132
  kind: "stage",
118
133
  title: `ASTROCODE — STAGE_${stageKeyUpper}`,
@@ -1,6 +1,39 @@
1
1
  import type { AstrocodeConfig } from "../config/schema";
2
2
  import type { SqliteDb } from "../state/db";
3
3
  import type { RunRow, StageKey, StageRunRow, StoryRow } from "../state/types";
4
+ import type { ToastOptions } from "../ui/toasts";
5
+ export declare const EVENT_TYPES: {
6
+ readonly RUN_STARTED: "run.started";
7
+ readonly RUN_COMPLETED: "run.completed";
8
+ readonly RUN_FAILED: "run.failed";
9
+ readonly RUN_ABORTED: "run.aborted";
10
+ readonly RUN_GENESIS_PLANNING_ATTACHED: "run.genesis_planning_attached";
11
+ readonly STAGE_STARTED: "stage.started";
12
+ readonly WORKFLOW_PROCEED: "workflow.proceed";
13
+ };
14
+ /**
15
+ * UI HOOKS
16
+ * --------
17
+ * This workflow module is DB-first. UI emission is optional and happens AFTER the DB tx commits.
18
+ *
19
+ * Contract:
20
+ * - If you pass ui.ctx + ui.sessionId, we will inject a visible chat message deterministically.
21
+ * - If you pass ui.toast, we will also toast (throttling is handled by the toast manager).
22
+ */
23
+ export type WorkflowUi = {
24
+ ctx: any;
25
+ sessionId: string;
26
+ agentName?: string;
27
+ toast?: (t: ToastOptions) => Promise<void>;
28
+ };
29
+ /**
30
+ * PLANNING-FIRST REDESIGN
31
+ * ----------------------
32
+ * - Never mutate story title/body.
33
+ * - Planning-first becomes a run-scoped inject stored in `injects` (scope = run:<run_id>).
34
+ * - Trigger is deterministic via config.workflow.genesis_planning:
35
+ * - "off" | "first_story_only" | "always"
36
+ */
4
37
  export type NextAction = {
5
38
  kind: "idle";
6
39
  reason: "no_approved_stories";
@@ -34,10 +67,20 @@ export declare function decideNextAction(db: SqliteDb, config: AstrocodeConfig):
34
67
  export declare function createRunForStory(db: SqliteDb, config: AstrocodeConfig, storyKey: string): {
35
68
  run_id: string;
36
69
  };
70
+ /**
71
+ * STAGE MOVEMENT (START) — now async so UI injection is deterministic.
72
+ */
37
73
  export declare function startStage(db: SqliteDb, runId: string, stageKey: StageKey, meta?: {
38
74
  subagent_type?: string;
39
75
  subagent_session_id?: string;
40
- }): void;
41
- export declare function completeRun(db: SqliteDb, runId: string): void;
42
- export declare function failRun(db: SqliteDb, runId: string, stageKey: StageKey, errorText: string): void;
76
+ ui?: WorkflowUi;
77
+ }): Promise<void>;
78
+ /**
79
+ * STAGE CLOSED (RUN COMPLETED) — now async so UI injection is deterministic.
80
+ */
81
+ export declare function completeRun(db: SqliteDb, runId: string, ui?: WorkflowUi): Promise<void>;
82
+ /**
83
+ * STAGE CLOSED (RUN FAILED) — now async so UI injection is deterministic.
84
+ */
85
+ export declare function failRun(db: SqliteDb, runId: string, stageKey: StageKey, errorText: string, ui?: WorkflowUi): Promise<void>;
43
86
  export declare function abortRun(db: SqliteDb, runId: string, reason: string): void;