@volter/supercode-orchestrator 0.5.73 → 0.5.75
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/agents.mjs +34 -24
- package/bin/orchestrator.mjs +2 -0
- package/board/agent-declarations.mjs +5 -5
- package/board/ci.mjs +98 -102
- package/board/cli.mjs +44 -57
- package/board/dispatch.mjs +314 -190
- package/board/engine.mjs +50 -28
- package/board/git-document.mjs +68 -20
- package/board/machine-fleet.mjs +27 -14
- package/board/runtime-door.mjs +17 -14
- package/board/store.mjs +1 -1
- package/board/tools.mjs +2 -2
- package/board/workflow.mjs +13 -12
- package/board/workspace.mjs +22 -15
- package/board/ztrack.mjs +22 -12
- package/hermes-cli.mjs +4 -12
- package/loop.mjs +7 -7
- package/package.json +2 -2
- package/bin/machine-fleet.mjs +0 -38
- package/bin/runtime-door.mjs +0 -23
package/agents.mjs
CHANGED
|
@@ -24,8 +24,8 @@ function store(root){
|
|
|
24
24
|
return db;
|
|
25
25
|
}
|
|
26
26
|
const digest=value=>createHash('sha256').update(value).digest('hex');
|
|
27
|
-
const minted=(key,machine)=>{
|
|
28
|
-
const reply=runtimeDoor(machine,'harness.v1.agents.mint',{key});
|
|
27
|
+
const minted=async(key,machine)=>{
|
|
28
|
+
const reply=await runtimeDoor(machine,'harness.v1.agents.mint',{key});
|
|
29
29
|
if(reply.code===0)return JSON.parse(reply.stdout).id;
|
|
30
30
|
// A host without the machine daemon (a container running only the orchestrator) mints the same
|
|
31
31
|
// globally unique shape here; the intent row keyed by this key is what keeps the id stable.
|
|
@@ -65,14 +65,18 @@ export function finishAgentAssignment(root,org,assignment,reason){
|
|
|
65
65
|
}catch(error){try{db.exec('ROLLBACK');}catch{}throw error;}finally{db.close();}
|
|
66
66
|
}
|
|
67
67
|
/** Mint and retain intent before any network call. Offline declarations keep their immutable id for sync. */
|
|
68
|
-
export function declareAgentIntent({root,org,server,profiles,profile,key,machine=null,refresh=true}) {
|
|
69
|
-
const intended=resolveAgentProfile(profiles,profile),stable=`${server}/${org}/${key}
|
|
68
|
+
export async function declareAgentIntent({root,org,server,profiles,profile,key,machine=null,refresh=true}) {
|
|
69
|
+
const intended=resolveAgentProfile(profiles,profile),stable=`${server}/${org}/${key}`;
|
|
70
|
+
// the id is minted (a door call, awaited) before the store is opened: no write is held across it
|
|
71
|
+
const known=(db)=>{const head=db.prepare('SELECT * FROM agent_declaration_heads WHERE org=? AND key=?').get(org,stable);
|
|
72
|
+
return head?db.prepare('SELECT * FROM agent_intents WHERE org=? AND id=?').get(org,head.agent_id):db.prepare('SELECT * FROM agent_intents WHERE org=? AND key=?').get(org,stable);};
|
|
73
|
+
let peek=store(root),mint=null;try{if(!known(peek))mint=await minted(stable,machine);}finally{peek.close();}
|
|
74
|
+
const db=store(root);
|
|
70
75
|
try {
|
|
71
|
-
|
|
72
|
-
let row=head?db.prepare('SELECT * FROM agent_intents WHERE org=? AND id=?').get(org,head.agent_id):db.prepare('SELECT * FROM agent_intents WHERE org=? AND key=?').get(org,stable);
|
|
76
|
+
let row=known(db);
|
|
73
77
|
const fresh=!row;
|
|
74
78
|
if(!row){
|
|
75
|
-
const id=minted(stable,machine);
|
|
79
|
+
const id=mint??await minted(stable,machine);
|
|
76
80
|
db.prepare('INSERT INTO agent_intents(org,id,key,profile_ref,profile_json,declared,created_at) VALUES (?,?,?,?,?,0,?) ON CONFLICT(org,key) DO NOTHING').run(org,id,stable,profile,JSON.stringify(intended),Date.now());
|
|
77
81
|
row=db.prepare('SELECT * FROM agent_intents WHERE org=? AND key=?').get(org,stable);
|
|
78
82
|
}
|
|
@@ -81,7 +85,11 @@ export function declareAgentIntent({root,org,server,profiles,profile,key,machine
|
|
|
81
85
|
return {id:row.id,key:row.key,baseKey:stable,declared:!!row.declared,intended:refresh?intended:JSON.parse(row.profile_json),fresh,org};
|
|
82
86
|
}finally{db.close();}
|
|
83
87
|
}
|
|
84
|
-
function nextAgentGeneration(root,intent,machine){
|
|
88
|
+
async function nextAgentGeneration(root,intent,machine){
|
|
89
|
+
// the next generation's id is minted before the write opens (a door call, awaited, never inside the transaction);
|
|
90
|
+
// a head moved meanwhile answers with its agent, and the mint (keyed) is simply not used
|
|
91
|
+
let peek=store(root),next;try{const head=peek.prepare('SELECT generation FROM agent_declaration_heads WHERE org=? AND key=?').get(intent.org,intent.baseKey);next=head?{generation:head.generation+1}:null;}finally{peek.close();}
|
|
92
|
+
if(next){next.key=`${intent.baseKey}/generation/${next.generation}`;next.id=await minted(next.key,machine);}
|
|
85
93
|
const db=store(root);try{
|
|
86
94
|
db.exec('BEGIN IMMEDIATE');
|
|
87
95
|
const head=db.prepare('SELECT * FROM agent_declaration_heads WHERE org=? AND key=?').get(intent.org,intent.baseKey);
|
|
@@ -89,7 +97,9 @@ function nextAgentGeneration(root,intent,machine){
|
|
|
89
97
|
const row=db.prepare('SELECT * FROM agent_intents WHERE org=? AND id=?').get(intent.org,head.agent_id);db.exec('COMMIT');
|
|
90
98
|
return {...intent,id:row.id,key:row.key,declared:!!row.declared};
|
|
91
99
|
}
|
|
92
|
-
const generation=head.generation+1,key=`${intent.baseKey}/generation/${generation}
|
|
100
|
+
const generation=head.generation+1,key=`${intent.baseKey}/generation/${generation}`;
|
|
101
|
+
if(next?.generation!==generation){db.exec('ROLLBACK');return await nextAgentGeneration(root,intent,machine);}
|
|
102
|
+
const id=next.id;
|
|
93
103
|
db.prepare('INSERT INTO agent_intents(org,id,key,profile_ref,profile_json,created_at) VALUES (?,?,?,?,?,?)').run(intent.org,id,key,intent.intended.profile,JSON.stringify(intent.intended),Date.now());
|
|
94
104
|
db.prepare('UPDATE agent_declaration_heads SET agent_id=?,generation=? WHERE org=? AND key=?').run(id,generation,intent.org,intent.baseKey);
|
|
95
105
|
db.exec('COMMIT');return {...intent,id,key,declared:false};
|
|
@@ -106,15 +116,15 @@ function recordDeclaration(root,org,id,declaration){
|
|
|
106
116
|
/** The identity a run starts as, never asking Teams: the daemon-minted id and retained intent. Its declaration is handed
|
|
107
117
|
* to this machine's daemon (handOffAgents), which declares it to Teams under the same id. Nothing is granted from this
|
|
108
118
|
* local record; `identity.synced` says whether the declaration has been handed on. */
|
|
109
|
-
function localAgent(options,{key,name,declaration,assignment,parent=null,starter}){
|
|
119
|
+
async function localAgent(options,{key,name,declaration,assignment,parent=null,starter}){
|
|
110
120
|
const {root,org,server,profiles,profile,machine=null}=options;
|
|
111
121
|
const rootProfile=profiles.default?'default':profile;
|
|
112
|
-
const rootIntent=declareAgentIntent({root,org,server,profiles,profile:rootProfile,key:'root',machine,refresh:false});
|
|
122
|
+
const rootIntent=await declareAgentIntent({root,org,server,profiles,profile:rootProfile,key:'root',machine,refresh:false});
|
|
113
123
|
recordDeclaration(root,org,rootIntent.id,{kind:'root',profile:rootProfile,name:basename(root),parent:null});
|
|
114
|
-
let intent=key==='root'?rootIntent:declareAgentIntent({root,org,server,profiles,profile,key,machine});
|
|
124
|
+
let intent=key==='root'?rootIntent:await declareAgentIntent({root,org,server,profiles,profile,key,machine});
|
|
115
125
|
// An agent ended here is never started again: its end is on its runs here (handOffAgents), and the key's next generation
|
|
116
126
|
// is minted in its place, with fresh memory.
|
|
117
|
-
if(key!=='root'&&endedHere(root,org,intent.id))intent=nextAgentGeneration(root,intent,machine);
|
|
127
|
+
if(key!=='root'&&endedHere(root,org,intent.id))intent=await nextAgentGeneration(root,intent,machine);
|
|
118
128
|
const above=parent??(key==='root'?null:{id:rootIntent.id,path:basename(root)});
|
|
119
129
|
if(key!=='root')recordDeclaration(root,org,intent.id,{...declaration,name,parent:above?.id??null});
|
|
120
130
|
const identity={id:intent.id,kind:'agent',org,name,path:above?`${above.path}/${name}`:name,parent:above?.id??null,
|
|
@@ -123,7 +133,7 @@ function localAgent(options,{key,name,declaration,assignment,parent=null,starter
|
|
|
123
133
|
const assigned=assignAgent(root,org,intent.id,assignment);
|
|
124
134
|
return {identity,intended:intent.intended,assignment:assigned,assignments:[assignment],org,synced:false};
|
|
125
135
|
}
|
|
126
|
-
export function localCardAgent(options){
|
|
136
|
+
export async function localCardAgent(options){
|
|
127
137
|
const {root,org,server,profiles,card,role,profile}=options;
|
|
128
138
|
// A card role's assignment the manager moved to another agent (`workflow reassign --agent`) runs as that agent: the
|
|
129
139
|
// active assignment names it, not the key's own head, and an agent whose end was synchronized here is not used.
|
|
@@ -138,7 +148,7 @@ export function localCardAgent(options){
|
|
|
138
148
|
declaration:{kind:'card',card,role,profile,sponsor:options.sponsor??null,starterSession:options.starterSession??null},
|
|
139
149
|
starter:options.starterSession?{kind:'session',id:options.starterSession}:{kind:'automation',id:`board:${basename(options.root)}`}});
|
|
140
150
|
}
|
|
141
|
-
export function localProfileAgent(options){
|
|
151
|
+
export async function localProfileAgent(options){
|
|
142
152
|
const {root,profiles,profile,agent:declared=null,declarations={}}=options,rootProfile=profiles.default?'default':profile;
|
|
143
153
|
const starter={kind:'automation',id:`orchestrator:${basename(root)}`};
|
|
144
154
|
if(declared&&declared!==profile){
|
|
@@ -148,7 +158,7 @@ export function localProfileAgent(options){
|
|
|
148
158
|
}
|
|
149
159
|
if(profile===rootProfile)return localAgent(options,{key:'root',name:basename(root),assignment:{kind:'profile',target:profile,role:'profile'},declaration:{kind:'root',profile},starter});
|
|
150
160
|
resolveAgentProfile(profiles,profile);
|
|
151
|
-
const parentProfile=profiles[profile].parent,parent=parentProfile&&parentProfile!==rootProfile?localProfileAgent({...options,profile:parentProfile}).identity:null;
|
|
161
|
+
const parentProfile=profiles[profile].parent,parent=parentProfile&&parentProfile!==rootProfile?(await localProfileAgent({...options,profile:parentProfile})).identity:null;
|
|
152
162
|
return localAgent(options,{key:`profile/${profile}`,name:profile,parent,assignment:{kind:'profile',target:profile,role:'profile'},declaration:{kind:'profile',profile},starter});
|
|
153
163
|
}
|
|
154
164
|
/** A local identity for a retained run: its id, intent and assignments, without a Teams record. */
|
|
@@ -189,13 +199,13 @@ function adoptLocalOrg(root,scope){
|
|
|
189
199
|
}
|
|
190
200
|
/** An explicit fork's child, declared under its parent and started by the fork's source session; the source must be a
|
|
191
201
|
* session this home ran for the parent. Local: the child's declaration is handed on like any other (handOffAgents). */
|
|
192
|
-
export function localForkAgent(options){
|
|
202
|
+
export async function localForkAgent(options){
|
|
193
203
|
const {root,org,profiles,parent,session,name,key}=options;
|
|
194
204
|
const owner=agentRunForSession(root,[session]);
|
|
195
205
|
if(owner?.agent_id!==parent)throw new Error('fork source is not a session of the declared parent');
|
|
196
206
|
const parentIntent=agentIntent(root,org,parent);
|
|
197
207
|
if(!parentIntent)throw new Error('fork parent has no retained profile intent here');
|
|
198
|
-
let intent=declareAgentIntent({...options,profile:parentIntent.profile_ref,key:`fork/${parent}/${key}`,refresh:false});
|
|
208
|
+
let intent=await declareAgentIntent({...options,profile:parentIntent.profile_ref,key:`fork/${parent}/${key}`,refresh:false});
|
|
199
209
|
if(endedHere(root,org,intent.id))throw new Error('this fork identity ended; declare a new fork key');
|
|
200
210
|
// The snapshot copies configuration only. Memory stays the new child's own empty state.
|
|
201
211
|
const intended=structuredClone(intent.fresh?parentIntent.profile:intent.intended);
|
|
@@ -227,9 +237,9 @@ function contextForOrg(org){
|
|
|
227
237
|
* (the context named, for its org alone). Never the process's own environment: the work was recorded for an org, and
|
|
228
238
|
* that org's context is the one that delivers it.
|
|
229
239
|
*/
|
|
230
|
-
export function handOffAgents(root,{context=null}={}){
|
|
240
|
+
export async function handOffAgents(root,{context=null}={}){
|
|
231
241
|
const waiting=[];
|
|
232
|
-
if(context){const scope=agentScope(context);if(!scope.offline)handOffOrg(root,scope,waiting);return waiting;}
|
|
242
|
+
if(context){const scope=agentScope(context);if(!scope.offline)await handOffOrg(root,scope,waiting);return waiting;}
|
|
233
243
|
let orgs;
|
|
234
244
|
const db=store(root);
|
|
235
245
|
try{orgs=db.prepare("SELECT org FROM agent_intents WHERE declared=0 AND declaration_json IS NOT NULL UNION SELECT org FROM agent_end_outbox").all().map(row=>row.org).filter(org=>org&&org!=='local');}
|
|
@@ -240,13 +250,13 @@ export function handOffAgents(root,{context=null}={}){
|
|
|
240
250
|
const named=contextForOrg(org);
|
|
241
251
|
if(!named){waiting.push({agent:'*',error:`no Teams context on this machine for org ${org} (or several, none selected); its agent work waits`});continue;}
|
|
242
252
|
const scope=agentScope(named);
|
|
243
|
-
if(!scope.offline&&scope.org===org)handOffOrg(root,scope,waiting);
|
|
253
|
+
if(!scope.offline&&scope.org===org)await handOffOrg(root,scope,waiting);
|
|
244
254
|
}catch(error){waiting.push({agent:'*',error:`org ${org}: ${error.message}; its agent work waits`});}
|
|
245
255
|
}
|
|
246
256
|
return waiting;
|
|
247
257
|
}
|
|
248
258
|
|
|
249
|
-
function handOffOrg(root,scope,waiting){
|
|
259
|
+
async function handOffOrg(root,scope,waiting){
|
|
250
260
|
adoptLocalOrg(root,scope);
|
|
251
261
|
let db=store(root),rows,ends;
|
|
252
262
|
try{
|
|
@@ -258,7 +268,7 @@ function handOffOrg(root,scope,waiting){
|
|
|
258
268
|
const body={org:scope.org,id:row.id,name:declaration.name??declaration.profile,parent:declaration.parent??null,profile:declaration.profile,
|
|
259
269
|
...(declaration.kind==='root'?{root:true}:{}),...(declaration.sponsor?{sponsor:declaration.sponsor}:{}),
|
|
260
270
|
...(declaration.starterSession?{starter_session:declaration.starterSession}:{}),key:digest(row.key)};
|
|
261
|
-
const reply=runtimeDoor(null,'harness.v1.agents.declare',body,{timeout:15_000});
|
|
271
|
+
const reply=await runtimeDoor(null,'harness.v1.agents.declare',body,{timeout:15_000});
|
|
262
272
|
if(reply.code!==0){waiting.push({agent:row.id,error:`not handed to this machine's daemon: ${reply.stderr.trim().split('\n').at(-1)}`});break;}
|
|
263
273
|
db=store(root);try{db.prepare('UPDATE agent_intents SET declared=1 WHERE org=? AND id=?').run(scope.org,row.id);}finally{db.close();}
|
|
264
274
|
}
|
|
@@ -266,7 +276,7 @@ function handOffOrg(root,scope,waiting){
|
|
|
266
276
|
db=store(root);let active;
|
|
267
277
|
try{active=db.prepare("SELECT 1 FROM agent_assignments WHERE org=? AND agent_id=? AND state='active'").get(scope.org,row.agent_id);if(active)db.prepare('DELETE FROM agent_end_outbox WHERE org=? AND agent_id=?').run(scope.org,row.agent_id);}finally{db.close();}
|
|
268
278
|
if(active)continue;
|
|
269
|
-
const reply=runtimeDoor(null,'harness.v1.agents.end',{org:scope.org,agent:row.agent_id,reason:row.reason,key:digest(`${scope.org}/${row.agent_id}/end/${row.created_at}`)},{timeout:15_000});
|
|
279
|
+
const reply=await runtimeDoor(null,'harness.v1.agents.end',{org:scope.org,agent:row.agent_id,reason:row.reason,key:digest(`${scope.org}/${row.agent_id}/end/${row.created_at}`)},{timeout:15_000});
|
|
270
280
|
if(reply.code!==0){waiting.push({agent:row.agent_id,error:`end not handed to this machine's daemon: ${reply.stderr.trim().split('\n').at(-1)}`});break;}
|
|
271
281
|
db=store(root);
|
|
272
282
|
try{
|
package/bin/orchestrator.mjs
CHANGED
|
@@ -18,6 +18,8 @@ if (process.argv[2] === 'workflow') {
|
|
|
18
18
|
// a store's own error (SQLite's, with its errcode) is no message written for a person: its stack says which statement
|
|
19
19
|
// met it, as a follower's end in the connector's log (board events) otherwise could not
|
|
20
20
|
catch(error) {process.stderr.write(`supercode workflow: ${error.message??error}\n${error?.errcode!=null&&error.stack?`${error.stack}\n`:''}`);code=1;}
|
|
21
|
+
// the board documents' git work this command queued (a ztrack board's commit and push) is finished before it exits
|
|
22
|
+
try{const{settleDocuments}=await import('../board/git-document.mjs');await settleDocuments();}catch{/* nothing queued */}
|
|
21
23
|
await Promise.all([process.stdout,process.stderr].map(stream=>new Promise(done=>stream.write('',done))));
|
|
22
24
|
process.exit(code);
|
|
23
25
|
} else if (process.argv[2] === 'agent-channel') {
|
|
@@ -5,7 +5,7 @@ import { agentScope, localCardAgent, finishAgentAssignment, activeAgentAssignmen
|
|
|
5
5
|
import { event, json, tx, view } from './store.mjs';
|
|
6
6
|
import { workflowFor } from './workflow.mjs';
|
|
7
7
|
|
|
8
|
-
export function declareCardAgentEffect(board,effect) {
|
|
8
|
+
export async function declareCardAgentEffect(board,effect) {
|
|
9
9
|
const task=view(board,db=>db.prepare('SELECT t.*,c.subtask_of,c.archived,c.sponsor FROM tasks t JOIN supercode_cards c ON c.task_id=t.id WHERE t.id=?').get(effect.id));
|
|
10
10
|
if(!task||task.subtask_of)return true;
|
|
11
11
|
const entry=workflowFor(board.root),profile=task.assignee??effect.profile??entry.params.default_assignee;
|
|
@@ -19,10 +19,10 @@ export function declareCardAgentEffect(board,effect) {
|
|
|
19
19
|
const context=effect.context,scope=agentScope(context);
|
|
20
20
|
const options={root:board.root,org:scope.org,server:scope.server,profiles:entry.profiles,
|
|
21
21
|
profile,card:effect.id,role:effect.role,sponsor:task.sponsor??null,context,starterSession:effect.starter_session};
|
|
22
|
-
const agent=localCardAgent(options);
|
|
22
|
+
const agent=await localCardAgent(options);
|
|
23
23
|
if(task.archived||['done','cancelled','archived'].includes(task.status))finishAgentAssignment(board.root,scope.org,agent.assignment,`card ${effect.id} ${task.status}`);
|
|
24
24
|
// A daemon not running keeps the declaration here; every delivery pass hands it on again.
|
|
25
|
-
const waiting=handOffAgents(board.root,{context});
|
|
25
|
+
const waiting=await handOffAgents(board.root,{context});
|
|
26
26
|
tx(board,db=>event(db,effect.id,'agent_declared',{agent:agent.identity.id,org:scope.org,role:effect.role,profile,assignment:agent.assignment,handed_to_machine:!waiting.length}));
|
|
27
27
|
return true;
|
|
28
28
|
}catch(error) {
|
|
@@ -33,12 +33,12 @@ export function declareCardAgentEffect(board,effect) {
|
|
|
33
33
|
|
|
34
34
|
/** The card agents of `ids` whose cards are done, cancelled or archived end their assignments, and an agent left with
|
|
35
35
|
* none has its end handed to this machine's daemon: run by the verb that closed the card, so no dispatcher is needed. */
|
|
36
|
-
export function endFinishedAgents(board,ids){
|
|
36
|
+
export async function endFinishedAgents(board,ids){
|
|
37
37
|
const wanted=new Set(ids.map(String)),assignments=activeAgentAssignments(board.root).filter(item=>item.kind==='card'&&wanted.has(String(item.target)));
|
|
38
38
|
if(!assignments.length)return [];
|
|
39
39
|
for(const assignment of assignments){
|
|
40
40
|
const task=view(board,db=>db.prepare('SELECT t.status,c.archived FROM tasks t JOIN supercode_cards c ON c.task_id=t.id WHERE t.id=?').get(assignment.target));
|
|
41
41
|
if(task&&(task.archived||['done','cancelled','archived'].includes(task.status)))finishAgentAssignment(board.root,assignment.org,assignment.id,`card ${assignment.target} ${task.archived?'archived':task.status}`);
|
|
42
42
|
}
|
|
43
|
-
return handOffAgents(board.root);
|
|
43
|
+
return await handOffAgents(board.root);
|
|
44
44
|
}
|