kodelyth-ecc 2.25.0 → 2.29.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.
@@ -0,0 +1,175 @@
1
+ // scripts/super-agent/orchestrator.js
2
+ //
3
+ // The super-agent driver — the piece Agent B found missing. It is an Arena-style
4
+ // state machine: a PURE nextAction(run) decides what to do next, and the driving
5
+ // LLM performs that action (decompose with blueprint, dispatch via the swarm,
6
+ // evaluate with the GAN loop, integrate branches, verify) and reports back
7
+ // through a submit* function, which advances the run and persists it.
8
+ //
9
+ // plan ──decompose──▶ execute ──(all steps done)──▶ integrate ──▶ verify ──▶ done
10
+ // │
11
+ // └─ dispatch ready steps (deps satisfied), budget-gated
12
+ //
13
+ // nextAction never mutates, so the whole control plane is tested without spawning
14
+ // a single agent. Mutations + phase transitions live in the submit* recorders.
15
+ 'use strict';
16
+
17
+ const dag = require('./dag.js');
18
+ const state = require('./state.js');
19
+ const { PHASE } = state;
20
+
21
+ // Rough per-step token estimate for the budget gate. Deliberately conservative;
22
+ // real spend is accrued from what the driver reports.
23
+ const DEFAULT_STEP_TOKENS = 4000;
24
+
25
+ function estimateTokens(steps) {
26
+ return (steps.length || 1) * DEFAULT_STEP_TOKENS;
27
+ }
28
+
29
+ function stepBrief(s) {
30
+ return {
31
+ id: s.id,
32
+ description: s.description || '',
33
+ deps: s.deps || [],
34
+ contextBrief: s.contextBrief || s.context || '',
35
+ exitCriteria: s.exitCriteria || s.exit || '',
36
+ };
37
+ }
38
+
39
+ function branchesOf(run) {
40
+ return run.steps
41
+ .filter((s) => s.status === dag.STATUS.DONE && s.branch)
42
+ .map((s) => ({ step: s.id, branch: s.branch }));
43
+ }
44
+
45
+ // ── The state machine (pure) ─────────────────────────────────────────────────
46
+ function nextAction(run) {
47
+ if (!run || run.status !== 'running') {
48
+ return { action: 'report', reason: (run && (run.stopReason || run.status)) || 'no run', run: run && state.summarize(run) };
49
+ }
50
+
51
+ switch (run.phase) {
52
+ case PHASE.PLAN:
53
+ // No plan yet → ask the driver to decompose the goal into a step DAG.
54
+ return { action: 'decompose', goal: run.goal, scope: run.scope };
55
+
56
+ case PHASE.EXECUTE: {
57
+ if (dag.isStalled(run.steps)) {
58
+ return { action: 'halt', reason: 'plan stalled — a dependency failed, so no remaining step can run' };
59
+ }
60
+ const ready = dag.readySteps(run.steps);
61
+ if (ready.length === 0) {
62
+ // Not stalled and nothing ready → steps are in progress; wait for results.
63
+ return { action: 'await', inProgress: run.steps.filter((s) => s.status === dag.STATUS.IN_PROGRESS).map((s) => s.id) };
64
+ }
65
+ const afford = state.canAfford(run, estimateTokens(ready));
66
+ if (!afford.ok) return { action: 'halt', reason: `budget: ${afford.reason}` };
67
+ return { action: 'dispatch', parallel: ready.length > 1, steps: ready.map(stepBrief) };
68
+ }
69
+
70
+ case PHASE.INTEGRATE:
71
+ return { action: 'integrate', branches: branchesOf(run), scope: run.scope };
72
+
73
+ case PHASE.VERIFY:
74
+ return { action: 'verify', scope: run.scope };
75
+
76
+ case PHASE.DONE:
77
+ return { action: 'report', reason: 'done', run: state.summarize(run) };
78
+
79
+ default:
80
+ return { action: 'report', reason: `unknown phase: ${run.phase}`, run: state.summarize(run) };
81
+ }
82
+ }
83
+
84
+ // ── Recorders (mutate + advance + persist) ───────────────────────────────────
85
+
86
+ // The driver decomposed the goal (via blueprint). Install the plan.
87
+ function submitPlan(run, rawSteps, { persist = true } = {}) {
88
+ requirePhase(run, PHASE.PLAN);
89
+ const steps = dag.normalizeSteps(rawSteps);
90
+ const v = dag.validate(steps);
91
+ if (!v.ok) throw new Error(`super-agent: invalid plan — ${v.errors.join('; ')}`);
92
+ if (steps.length === 0) throw new Error('super-agent: plan has no steps');
93
+ run.steps = steps;
94
+ run.phase = PHASE.EXECUTE;
95
+ state.record(run, { event: 'planned', steps: steps.length });
96
+ return persist ? state.save(run) : run;
97
+ }
98
+
99
+ // Mark a step as about to run (so nextAction reports it as in-progress, not ready).
100
+ function markDispatched(run, stepIds, { persist = true } = {}) {
101
+ const want = new Set((Array.isArray(stepIds) ? stepIds : [stepIds]).map(String));
102
+ for (const s of run.steps) {
103
+ if (want.has(s.id) && s.status === dag.STATUS.PENDING) s.status = dag.STATUS.IN_PROGRESS;
104
+ }
105
+ state.record(run, { event: 'dispatched', steps: [...want] });
106
+ return persist ? state.save(run) : run;
107
+ }
108
+
109
+ // The driver finished a step. Record result + budget, then advance the phase if
110
+ // the plan is now complete or stalled.
111
+ function submitStepResult(run, stepId, { status = dag.STATUS.DONE, branch = null, result = null, tokensSpent = 0 } = {}, { persist = true } = {}) {
112
+ const s = run.steps.find((x) => x.id === String(stepId));
113
+ if (!s) throw new Error(`super-agent: unknown step ${stepId}`);
114
+ s.status = status;
115
+ if (branch) s.branch = branch;
116
+ if (result != null) s.result = result;
117
+ state.accrue(run, { tokensSpent, steps: 1 });
118
+ state.record(run, { event: 'step', step: s.id, status, tokensSpent });
119
+
120
+ if (dag.isComplete(run.steps)) {
121
+ run.phase = PHASE.INTEGRATE;
122
+ } else if (dag.isStalled(run.steps)) {
123
+ state.stop(run, 'plan stalled after a step failed');
124
+ }
125
+ return persist ? state.save(run) : run;
126
+ }
127
+
128
+ // The driver merged the worker branches.
129
+ function submitIntegration(run, { merged = true, conflicts = [], tokensSpent = 0 } = {}, { persist = true } = {}) {
130
+ requirePhase(run, PHASE.INTEGRATE);
131
+ state.accrue(run, { tokensSpent });
132
+ if (Array.isArray(conflicts) && conflicts.length > 0) {
133
+ state.record(run, { event: 'integration-conflicts', conflicts });
134
+ state.stop(run, `integration conflicts in: ${conflicts.join(', ')}`);
135
+ } else if (!merged) {
136
+ state.stop(run, 'integration reported not merged');
137
+ } else {
138
+ run.phase = PHASE.VERIFY;
139
+ state.record(run, { event: 'integrated' });
140
+ }
141
+ return persist ? state.save(run) : run;
142
+ }
143
+
144
+ // The driver ran the verification gate (tests/build).
145
+ function submitVerification(run, { passed = false, detail = '', tokensSpent = 0 } = {}, { persist = true } = {}) {
146
+ requirePhase(run, PHASE.VERIFY);
147
+ state.accrue(run, { tokensSpent });
148
+ if (passed) {
149
+ run.phase = PHASE.DONE;
150
+ run.status = 'done';
151
+ state.record(run, { event: 'verified', detail });
152
+ } else {
153
+ run.status = 'failed';
154
+ run.stopReason = `verification failed: ${detail || 'tests/build did not pass'}`;
155
+ state.record(run, { event: 'verification-failed', detail });
156
+ }
157
+ return persist ? state.save(run) : run;
158
+ }
159
+
160
+ function requirePhase(run, expected) {
161
+ if (run.phase !== expected) {
162
+ throw new Error(`super-agent: expected phase "${expected}" but run is in "${run.phase}"`);
163
+ }
164
+ }
165
+
166
+ module.exports = {
167
+ nextAction,
168
+ submitPlan,
169
+ markDispatched,
170
+ submitStepResult,
171
+ submitIntegration,
172
+ submitVerification,
173
+ estimateTokens,
174
+ DEFAULT_STEP_TOKENS,
175
+ };
@@ -0,0 +1,136 @@
1
+ // scripts/super-agent/state.js
2
+ //
3
+ // Run lifecycle, persistence (so a run resumes after a crash or a new session),
4
+ // and the budget governor for the super-agent orchestrator. Mirrors the pattern
5
+ // of scripts/arena/state.js: the control plane is tokenless and durable, so the
6
+ // expensive part (actually running agents) can stop and pick up again.
7
+ 'use strict';
8
+
9
+ const fs = require('fs');
10
+ const os = require('os');
11
+ const path = require('path');
12
+ const crypto = require('crypto');
13
+ const safeFs = require('../lib/safe-fs.js');
14
+ const dag = require('./dag.js');
15
+
16
+ const RUN_DIR = process.env.KODELYTH_SUPER_AGENT_DIR
17
+ || path.join(os.homedir(), '.kodelythecc', 'super-agent');
18
+
19
+ const PHASE = Object.freeze({
20
+ PLAN: 'plan',
21
+ EXECUTE: 'execute',
22
+ INTEGRATE: 'integrate',
23
+ VERIFY: 'verify',
24
+ DONE: 'done',
25
+ });
26
+
27
+ function runPath(id) {
28
+ return path.join(RUN_DIR, `${id}.json`);
29
+ }
30
+
31
+ function createRun({ goal, limits = {}, scope = '.' } = {}) {
32
+ if (!goal || !String(goal).trim()) throw new Error('super-agent: a run needs a goal');
33
+ return {
34
+ id: crypto.randomBytes(6).toString('hex'),
35
+ goal: String(goal).trim(),
36
+ scope,
37
+ createdAt: new Date().toISOString(),
38
+ status: 'running', // running | done | failed | stopped
39
+ phase: PHASE.PLAN,
40
+ steps: [],
41
+ budget: {
42
+ // 0 / null means "no cap". Caps are the governor Agent B flagged as missing
43
+ // — a single per-session token-budget could not bound a whole run.
44
+ tokenCap: limits.tokenCap || 0,
45
+ stepCap: limits.stepCap || 0,
46
+ tokensSpent: 0,
47
+ stepsRun: 0,
48
+ },
49
+ history: [],
50
+ stopReason: null,
51
+ };
52
+ }
53
+
54
+ function record(run, event) {
55
+ run.history.push({ at: new Date().toISOString(), ...event });
56
+ }
57
+
58
+ // Budget governor. Returns { ok, reason? }. Checks both the token cap and the
59
+ // step cap; an estimate of 0 still fails if we are already over.
60
+ function canAfford(run, estTokens = 0) {
61
+ const b = run.budget;
62
+ if (b.stepCap && b.stepsRun >= b.stepCap) {
63
+ return { ok: false, reason: `step cap reached (${b.stepsRun}/${b.stepCap})` };
64
+ }
65
+ if (b.tokenCap && b.tokensSpent + estTokens > b.tokenCap) {
66
+ return { ok: false, reason: `token cap would be exceeded (${b.tokensSpent}+${estTokens} > ${b.tokenCap})` };
67
+ }
68
+ return { ok: true };
69
+ }
70
+
71
+ function accrue(run, { tokensSpent = 0, steps = 0 } = {}) {
72
+ run.budget.tokensSpent += Math.max(0, tokensSpent) || 0;
73
+ run.budget.stepsRun += Math.max(0, steps) || 0;
74
+ }
75
+
76
+ function stop(run, reason) {
77
+ run.status = 'stopped';
78
+ run.stopReason = reason;
79
+ record(run, { event: 'stopped', reason });
80
+ }
81
+
82
+ function save(run) {
83
+ fs.mkdirSync(RUN_DIR, { recursive: true });
84
+ // Atomic: a crash mid-write must not corrupt a resumable run.
85
+ safeFs.replaceFileAtomic(runPath(run.id), JSON.stringify(run, null, 2) + '\n', 0o600);
86
+ return run;
87
+ }
88
+
89
+ function load(id) {
90
+ const raw = fs.readFileSync(runPath(id), 'utf8');
91
+ return JSON.parse(raw);
92
+ }
93
+
94
+ function exists(id) {
95
+ return fs.existsSync(runPath(id));
96
+ }
97
+
98
+ function list() {
99
+ if (!fs.existsSync(RUN_DIR)) return [];
100
+ return fs.readdirSync(RUN_DIR)
101
+ .filter((f) => f.endsWith('.json'))
102
+ .map((f) => {
103
+ try { return JSON.parse(fs.readFileSync(path.join(RUN_DIR, f), 'utf8')); }
104
+ catch { return null; }
105
+ })
106
+ .filter(Boolean)
107
+ .sort((a, b) => String(b.createdAt).localeCompare(String(a.createdAt)));
108
+ }
109
+
110
+ function summarize(run) {
111
+ return {
112
+ id: run.id,
113
+ goal: run.goal,
114
+ status: run.status,
115
+ phase: run.phase,
116
+ progress: dag.progress(run.steps),
117
+ budget: run.budget,
118
+ stopReason: run.stopReason,
119
+ };
120
+ }
121
+
122
+ module.exports = {
123
+ PHASE,
124
+ RUN_DIR,
125
+ runPath,
126
+ createRun,
127
+ record,
128
+ canAfford,
129
+ accrue,
130
+ stop,
131
+ save,
132
+ load,
133
+ exists,
134
+ list,
135
+ summarize,
136
+ };