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/dist/ui/inject.d.ts
CHANGED
|
@@ -1,6 +1,20 @@
|
|
|
1
|
-
|
|
1
|
+
type QueueItem = {
|
|
2
2
|
ctx: any;
|
|
3
3
|
sessionId: string;
|
|
4
4
|
text: string;
|
|
5
5
|
agent?: string;
|
|
6
|
-
}
|
|
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
|
-
|
|
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
|
-
|
|
4
|
-
|
|
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
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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
|
-
|
|
34
|
-
|
|
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
|
-
|
|
38
|
-
|
|
39
|
-
if (injectionQueue.length > 0) {
|
|
40
|
-
setImmediate(processQueue);
|
|
41
|
-
}
|
|
87
|
+
workerRunning = false;
|
|
88
|
+
resolveDrainWaitersIfIdle();
|
|
42
89
|
}
|
|
43
90
|
}
|
|
44
|
-
|
|
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
|
-
|
|
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
|
|
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")
|
|
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
|
|
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"))
|
|
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
|
|
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"))
|
|
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
|
|
78
|
-
const
|
|
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 =
|
|
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
|
-
`
|
|
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
|
-
`
|
|
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")
|
|
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
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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;
|