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.
- package/README.md +243 -11
- package/dist/agents/prompts.d.ts +1 -0
- package/dist/agents/prompts.js +159 -0
- package/dist/agents/registry.js +11 -1
- package/dist/config/loader.js +34 -0
- package/dist/config/schema.d.ts +7 -1
- package/dist/config/schema.js +2 -0
- package/dist/hooks/continuation-enforcer.d.ts +9 -1
- package/dist/hooks/continuation-enforcer.js +2 -1
- package/dist/hooks/inject-provider.d.ts +9 -1
- package/dist/hooks/inject-provider.js +2 -1
- package/dist/hooks/tool-output-truncator.d.ts +9 -1
- package/dist/hooks/tool-output-truncator.js +2 -1
- package/dist/index.js +228 -45
- package/dist/state/adapters/index.d.ts +4 -2
- package/dist/state/adapters/index.js +23 -27
- package/dist/state/db.d.ts +6 -8
- package/dist/state/db.js +106 -45
- package/dist/tools/index.d.ts +13 -3
- package/dist/tools/index.js +14 -31
- package/dist/tools/init.d.ts +10 -1
- package/dist/tools/init.js +73 -18
- package/dist/tools/injects.js +90 -26
- package/dist/tools/spec.d.ts +0 -1
- package/dist/tools/spec.js +4 -1
- package/dist/tools/status.d.ts +1 -1
- package/dist/tools/status.js +70 -52
- package/dist/tools/workflow.js +2 -2
- package/dist/ui/inject.d.ts +16 -2
- package/dist/ui/inject.js +104 -33
- package/dist/workflow/directives.d.ts +2 -0
- package/dist/workflow/directives.js +34 -19
- package/dist/workflow/state-machine.d.ts +46 -3
- package/dist/workflow/state-machine.js +249 -92
- package/package.json +1 -1
- package/src/agents/prompts.ts +160 -0
- package/src/agents/registry.ts +16 -1
- package/src/config/loader.ts +39 -4
- package/src/config/schema.ts +3 -0
- package/src/hooks/continuation-enforcer.ts +9 -2
- package/src/hooks/inject-provider.ts +9 -2
- package/src/hooks/tool-output-truncator.ts +9 -2
- package/src/index.ts +260 -56
- package/src/state/adapters/index.ts +21 -26
- package/src/state/db.ts +114 -58
- package/src/tools/index.ts +29 -31
- package/src/tools/init.ts +91 -22
- package/src/tools/injects.ts +147 -53
- package/src/tools/spec.ts +6 -2
- package/src/tools/status.ts +71 -55
- package/src/tools/workflow.ts +3 -3
- package/src/ui/inject.ts +115 -41
- package/src/workflow/directives.ts +103 -75
- package/src/workflow/state-machine.ts +327 -109
package/src/ui/inject.ts
CHANGED
|
@@ -1,62 +1,136 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
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
|
-
|
|
10
|
-
|
|
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
|
-
|
|
13
|
-
const
|
|
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 (!
|
|
16
|
-
|
|
49
|
+
if (!sessionId) {
|
|
50
|
+
console.warn("[Astrocode] Injection skipped: missing sessionId");
|
|
17
51
|
return;
|
|
18
52
|
}
|
|
19
53
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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
|
-
|
|
25
|
-
|
|
26
|
-
console.warn("[Astrocode] Skipping injection: No sessionId provided");
|
|
27
|
-
return;
|
|
28
|
-
}
|
|
60
|
+
const maxAttempts = 3;
|
|
61
|
+
let attempt = 0;
|
|
29
62
|
|
|
30
|
-
|
|
31
|
-
|
|
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
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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
|
-
|
|
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 =
|
|
32
|
-
|
|
33
|
-
[
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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
|
|
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
|
-
|
|
82
|
-
|
|
83
|
-
].join("\n")
|
|
84
|
-
|
|
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
|
|
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
|
-
|
|
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 {
|
|
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
|
|
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 =
|
|
136
|
-
|
|
137
|
-
[
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
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 {
|