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
package/src/ui/inject.ts CHANGED
@@ -1,62 +1,136 @@
1
- let isInjecting = false;
2
- const injectionQueue: Array<{
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.
11
+
12
+ type QueueItem = {
3
13
  ctx: any;
4
14
  sessionId: string;
5
15
  text: string;
6
16
  agent?: string;
7
- }> = [];
17
+ };
18
+
19
+ const injectionQueue: QueueItem[] = [];
20
+
21
+ let workerRunning = false;
22
+
23
+ // Used to let callers await "queue drained"
24
+ let drainWaiters: Array<() => void> = [];
25
+
26
+ function sleep(ms: number) {
27
+ return new Promise<void>((resolve) => setTimeout(resolve, ms));
28
+ }
29
+
30
+ function resolveDrainWaitersIfIdle() {
31
+ if (workerRunning) return;
32
+ if (injectionQueue.length !== 0) return;
33
+
34
+ const waiters = drainWaiters;
35
+ drainWaiters = [];
36
+ for (const w of waiters) w();
37
+ }
8
38
 
9
- async function processQueue() {
10
- if (isInjecting || injectionQueue.length === 0) return;
39
+ function getPromptApi(ctx: any) {
40
+ const fn = ctx?.client?.session?.prompt;
41
+ return typeof fn === "function" ? fn.bind(ctx.client.session) : null;
42
+ }
11
43
 
12
- isInjecting = true;
13
- const opts = injectionQueue.shift();
44
+ async function sendWithRetries(opts: QueueItem): Promise<void> {
45
+ const { ctx, sessionId, text } = opts;
46
+ const agent = opts.agent ?? "Astro";
47
+ const prefixedText = `[${agent}]\n\n${text}`;
14
48
 
15
- if (!opts) {
16
- isInjecting = false;
49
+ if (!sessionId) {
50
+ console.warn("[Astrocode] Injection skipped: missing sessionId");
17
51
  return;
18
52
  }
19
53
 
20
- try {
21
- const { ctx, sessionId, text, agent = "Astro" } = opts;
22
- const prefixedText = `[${agent}]\n\n${text}`;
54
+ const prompt = getPromptApi(ctx);
55
+ if (!prompt) {
56
+ console.warn("[Astrocode] Injection skipped: ctx.client.session.prompt unavailable");
57
+ return;
58
+ }
23
59
 
24
- // Basic validation
25
- if (!sessionId) {
26
- console.warn("[Astrocode] Skipping injection: No sessionId provided");
27
- return;
28
- }
60
+ const maxAttempts = 3;
61
+ let attempt = 0;
29
62
 
30
- if (!ctx?.client?.session?.prompt) {
31
- console.warn("[Astrocode] Skipping injection: API not available (ctx.client.session.prompt)");
63
+ while (attempt < maxAttempts) {
64
+ attempt += 1;
65
+ try {
66
+ await prompt({
67
+ path: { id: sessionId },
68
+ body: {
69
+ parts: [{ type: "text", text: prefixedText }],
70
+ agent,
71
+ },
72
+ });
32
73
  return;
74
+ } catch (err) {
75
+ const msg = err instanceof Error ? err.message : String(err);
76
+ const isLast = attempt >= maxAttempts;
77
+
78
+ if (isLast) {
79
+ console.warn(`[Astrocode] Injection failed (final): ${msg}`);
80
+ return;
81
+ }
82
+
83
+ // Exponential backoff + jitter
84
+ const base = 150 * Math.pow(2, attempt - 1); // 150, 300, 600
85
+ const jitter = Math.floor(Math.random() * 120);
86
+ await sleep(base + jitter);
33
87
  }
88
+ }
89
+ }
34
90
 
35
- await ctx.client.session.prompt({
36
- path: { id: sessionId },
37
- body: {
38
- parts: [{ type: "text", text: prefixedText }],
39
- // Pass agent context for systems that support it
40
- agent: agent,
41
- },
42
- });
43
- } catch (error) {
44
- console.warn(`[Astrocode] Injection failed: ${error}`);
45
- } finally {
46
- isInjecting = false;
47
- // Process next item immediately
48
- if (injectionQueue.length > 0) {
49
- setImmediate(processQueue);
91
+ async function runWorkerLoop(): Promise<void> {
92
+ if (workerRunning) return;
93
+ workerRunning = true;
94
+
95
+ try {
96
+ // Drain sequentially to preserve ordering
97
+ while (injectionQueue.length > 0) {
98
+ const item = injectionQueue.shift();
99
+ if (!item) continue;
100
+ await sendWithRetries(item);
50
101
  }
102
+ } finally {
103
+ workerRunning = false;
104
+ resolveDrainWaitersIfIdle();
51
105
  }
52
106
  }
53
107
 
54
- export async function injectChatPrompt(opts: {
55
- ctx: any;
56
- sessionId: string;
57
- text: string;
58
- agent?: string;
59
- }) {
108
+ /**
109
+ * Enqueue an injection and ensure the worker is running.
110
+ * Does NOT wait for delivery — use `flushChatPrompts()` to wait.
111
+ */
112
+ export function enqueueChatPrompt(opts: QueueItem) {
60
113
  injectionQueue.push(opts);
61
- processQueue();
114
+ // Kick worker
115
+ void runWorkerLoop();
116
+ }
117
+
118
+ /**
119
+ * Wait until all queued injections have been processed (sent or exhausted retries).
120
+ */
121
+ export function flushChatPrompts(): Promise<void> {
122
+ if (!workerRunning && injectionQueue.length === 0) return Promise.resolve();
123
+ return new Promise<void>((resolve) => {
124
+ drainWaiters.push(resolve);
125
+ // Ensure worker is running (in case someone enqueued without kick)
126
+ void runWorkerLoop();
127
+ });
128
+ }
129
+
130
+ /**
131
+ * Deterministic helper: enqueue + flush (recommended for stage boundaries).
132
+ */
133
+ export async function injectChatPrompt(opts: QueueItem): Promise<void> {
134
+ enqueueChatPrompt(opts);
135
+ await flushChatPrompts();
62
136
  }
@@ -1,8 +1,8 @@
1
+ // src/workflow/directives.ts
1
2
  import type { AstrocodeConfig } from "../config/schema";
2
3
  import { sha256Hex } from "../shared/hash";
3
4
  import { clampChars, normalizeNewlines } from "../shared/text";
4
5
  import type { StageKey } from "../state/types";
5
- import { addStalenessIndicator } from "./context";
6
6
 
7
7
  export type DirectiveKind = "continue" | "stage" | "blocked" | "repair";
8
8
 
@@ -13,12 +13,25 @@ export type BuiltDirective = {
13
13
  hash: string;
14
14
  };
15
15
 
16
+ function getInjectMaxChars(config?: AstrocodeConfig): number {
17
+ // Deterministic fallback for older configs.
18
+ const v = (config as any)?.context_compaction?.inject_max_chars;
19
+ return typeof v === "number" && Number.isFinite(v) && v > 0 ? v : 12000;
20
+ }
21
+
16
22
  export function directiveHash(body: string): string {
17
23
  // Stable hash to dedupe: normalize newlines + trim
18
24
  const norm = normalizeNewlines(body).trim();
19
25
  return sha256Hex(norm);
20
26
  }
21
27
 
28
+ function finalizeBody(body: string, maxChars: number): string {
29
+ // Normalize first, clamp second, trim last => hash/body match exactly.
30
+ const norm = normalizeNewlines(body);
31
+ const clamped = clampChars(norm, maxChars);
32
+ return clamped.trim();
33
+ }
34
+
22
35
  export function buildContinueDirective(opts: {
23
36
  config: AstrocodeConfig;
24
37
  run_id: string;
@@ -27,28 +40,27 @@ export function buildContinueDirective(opts: {
27
40
  context_snapshot_md: string;
28
41
  }): BuiltDirective {
29
42
  const { config, run_id, stage_key, next_action, context_snapshot_md } = opts;
43
+ const maxChars = getInjectMaxChars(config);
30
44
 
31
- const body = clampChars(
32
- normalizeNewlines(
33
- [
34
- `[SYSTEM DIRECTIVE: ASTROCODE — CONTINUE]`,
35
- ``,
36
- `This directive is injected by the Astro agent to continue the workflow.`,
37
- ``,
38
- `Run: \`${run_id}\`${stage_key ? ` Stage: \`${stage_key}\`` : ""}`,
39
- ``,
40
- `Next action: ${next_action}`,
41
- ``,
42
- `Rules:`,
43
- `- Do not stop early. Keep going until the run is completed, failed, or blocked.`,
44
- `- Prefer tools over prose.`,
45
- `- If blocked, ask exactly ONE question and stop.`,
46
- ``,
47
- `Context snapshot:`,
48
- context_snapshot_md.trim(),
49
- ].join("\n")
50
- ),
51
- config.context_compaction.inject_max_chars
45
+ const body = finalizeBody(
46
+ [
47
+ `[SYSTEM DIRECTIVE: ASTROCODE — CONTINUE]`,
48
+ ``,
49
+ `This directive is injected by the Astro agent to continue the workflow.`,
50
+ ``,
51
+ `Run: \`${run_id}\`${stage_key ? ` Stage: \`${stage_key}\`` : ""}`,
52
+ ``,
53
+ `Next action: ${next_action}`,
54
+ ``,
55
+ `Rules:`,
56
+ `- Do not stop early. Keep going until the run is completed, failed, or blocked.`,
57
+ `- Prefer tools over prose.`,
58
+ `- If blocked, ask exactly ONE question and stop.`,
59
+ ``,
60
+ `Context snapshot:`,
61
+ (context_snapshot_md ?? "").trim(),
62
+ ].join("\n"),
63
+ maxChars
52
64
  );
53
65
 
54
66
  return {
@@ -60,13 +72,16 @@ export function buildContinueDirective(opts: {
60
72
  }
61
73
 
62
74
  export function buildBlockedDirective(opts: {
75
+ config?: AstrocodeConfig;
63
76
  run_id: string;
64
77
  stage_key: string;
65
78
  question: string;
66
79
  context_snapshot_md: string;
67
80
  }): BuiltDirective {
68
- const { run_id, stage_key, question, context_snapshot_md } = opts;
69
- const body = normalizeNewlines(
81
+ const { config, run_id, stage_key, question, context_snapshot_md } = opts;
82
+ const maxChars = getInjectMaxChars(config);
83
+
84
+ const body = finalizeBody(
70
85
  [
71
86
  `[SYSTEM DIRECTIVE: ASTROCODE — BLOCKED]`,
72
87
  ``,
@@ -78,10 +93,11 @@ export function buildBlockedDirective(opts: {
78
93
  ``,
79
94
  `Question: ${question}`,
80
95
  ``,
81
- `Context snapshot:`,
82
- context_snapshot_md.trim(),
83
- ].join("\n")
84
- ).trim();
96
+ `Context snapshot:`,
97
+ (context_snapshot_md ?? "").trim(),
98
+ ].join("\n"),
99
+ maxChars
100
+ );
85
101
 
86
102
  return {
87
103
  kind: "blocked",
@@ -91,8 +107,10 @@ export function buildBlockedDirective(opts: {
91
107
  };
92
108
  }
93
109
 
94
- export function buildRepairDirective(opts: { report_md: string }): BuiltDirective {
95
- const body = normalizeNewlines(
110
+ export function buildRepairDirective(opts: { config?: AstrocodeConfig; report_md: string }): BuiltDirective {
111
+ const maxChars = getInjectMaxChars(opts.config);
112
+
113
+ const body = finalizeBody(
96
114
  [
97
115
  `[SYSTEM DIRECTIVE: ASTROCODE — REPAIR]`,
98
116
  ``,
@@ -101,9 +119,10 @@ export function buildRepairDirective(opts: { report_md: string }): BuiltDirectiv
101
119
  `Astrocode performed a repair pass. Summarize in <= 10 lines and continue.`,
102
120
  ``,
103
121
  `Repair report:`,
104
- opts.report_md.trim(),
105
- ].join("\n")
106
- ).trim();
122
+ (opts.report_md ?? "").trim(),
123
+ ].join("\n"),
124
+ maxChars
125
+ );
107
126
 
108
127
  return {
109
128
  kind: "repair",
@@ -124,53 +143,62 @@ export function buildStageDirective(opts: {
124
143
  stage_constraints: string[];
125
144
  context_snapshot_md: string;
126
145
  }): BuiltDirective {
127
- const { config, stage_key, run_id, story_key, story_title, stage_agent_name, stage_goal, stage_constraints, context_snapshot_md } = opts;
146
+ const {
147
+ config,
148
+ stage_key,
149
+ run_id,
150
+ story_key,
151
+ story_title,
152
+ stage_agent_name,
153
+ stage_goal,
154
+ stage_constraints,
155
+ context_snapshot_md,
156
+ } = opts;
128
157
 
129
- const stageKeyUpper = stage_key.toUpperCase();
158
+ const maxChars = getInjectMaxChars(config);
159
+ const stageKeyUpper = String(stage_key).toUpperCase();
130
160
 
131
- const constraintsBlock = stage_constraints.length
161
+ const constraintsBlock = Array.isArray(stage_constraints) && stage_constraints.length
132
162
  ? ["", "Constraints:", ...stage_constraints.map((c) => `- ${c}`)].join("\n")
133
163
  : "";
134
164
 
135
- const body = clampChars(
136
- normalizeNewlines(
137
- [
138
- `[SYSTEM DIRECTIVE: ASTROCODE — STAGE_${stageKeyUpper}]`,
139
- ``,
140
- `This directive is injected by the Astro agent to delegate the stage task.`,
141
- ``,
142
- `You are: \`${stage_agent_name}\``,
143
- `Run: \`${run_id}\``,
144
- `Story: \`${story_key}\` — ${story_title}`,
145
- ``,
146
- `Stage goal: ${stage_goal}`,
147
- constraintsBlock,
148
- ``,
149
- `Output contract (strict):`,
150
- `1) Baton markdown (short, structured)`,
151
- `2) ASTRO JSON between markers:`,
152
- ` ${"<!-- ASTRO_JSON_BEGIN -->"}`,
153
- ` {`,
154
- ` "schema_version": 1,`,
155
- ` "stage_key": "${stage_key}",`,
156
- ` "status": "ok",`,
157
- ` ...`,
158
- ` }`,
159
- ` ${"<!-- ASTRO_JSON_END -->"}`,
160
- ``,
161
- `ASTRO JSON requirements:`,
162
- `- stage_key must be "${stage_key}"`,
163
- `- status must be "ok" | "blocked" | "failed"`,
164
- `- include summary + next_actions`,
165
- `- include files/evidence paths when relevant`,
166
- ``,
167
- `If blocked: ask exactly ONE question and stop.`,
168
- ``,
169
- `Context snapshot:`,
170
- context_snapshot_md.trim(),
171
- ].join("\n")
172
- ),
173
- config.context_compaction.inject_max_chars
165
+ const body = finalizeBody(
166
+ [
167
+ `[SYSTEM DIRECTIVE: ASTROCODE — STAGE_${stageKeyUpper}]`,
168
+ ``,
169
+ `This directive is injected by the Astro agent to delegate the stage task.`,
170
+ ``,
171
+ `You are: \`${stage_agent_name}\``,
172
+ `Run: \`${run_id}\``,
173
+ `Story: \`${story_key}\` — ${story_title}`,
174
+ ``,
175
+ `Stage goal: ${stage_goal}`,
176
+ constraintsBlock,
177
+ ``,
178
+ `Output contract (strict):`,
179
+ `1) Baton markdown (short, structured)`,
180
+ `2) ASTRO JSON between markers:`,
181
+ ` <!-- ASTRO_JSON_BEGIN -->`,
182
+ ` {`,
183
+ ` "schema_version": 1,`,
184
+ ` "stage_key": "${stage_key}",`,
185
+ ` "status": "ok",`,
186
+ ` "...": "..."`,
187
+ ` }`,
188
+ ` <!-- ASTRO_JSON_END -->`,
189
+ ``,
190
+ `ASTRO JSON requirements:`,
191
+ `- stage_key must be "${stage_key}"`,
192
+ `- status must be "ok" | "blocked" | "failed"`,
193
+ `- include summary + next_actions`,
194
+ `- include files/evidence paths when relevant`,
195
+ ``,
196
+ `If blocked: ask exactly ONE question and stop.`,
197
+ ``,
198
+ `Context snapshot:`,
199
+ (context_snapshot_md ?? "").trim(),
200
+ ].join("\n"),
201
+ maxChars
174
202
  );
175
203
 
176
204
  return {