@volter/supercode-orchestrator 0.5.36 → 0.5.37
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 +32 -10
- package/bin/agent-door.mjs +4 -1
- package/board/dispatch.mjs +12 -30
- package/index.d.ts +2 -2
- package/package.json +1 -1
package/agents.mjs
CHANGED
|
@@ -8,6 +8,8 @@ import { createHash, randomBytes } from 'node:crypto';
|
|
|
8
8
|
import { runtimeDoor } from './board/runtime-door.mjs';
|
|
9
9
|
import { resolveAgentProfile } from './agent-profiles.mjs';
|
|
10
10
|
import { compareAgentProfile } from '@volter/teams/agent-profile';
|
|
11
|
+
import { WorkspaceContexts } from '@volter/teams/context';
|
|
12
|
+
import { supercodeHome } from '@volter/teams/home';
|
|
11
13
|
const entry=fileURLToPath(new URL('./bin/agent-door.mjs',import.meta.url));
|
|
12
14
|
// The caller each door names to Teams (X-Supercode-Caller): this host's own command words, then the door's operation.
|
|
13
15
|
const HOST_CALLER=(process.env.SUPERCODE_CALLER_NAME??`orchestrator ${process.argv.slice(2).filter(arg=>!arg.startsWith('-')).slice(0,2).filter(arg=>/^[a-z][a-z0-9-]{0,30}$/.test(arg)).join(' ')}`).slice(0,80);
|
|
@@ -22,11 +24,9 @@ export function teamsUnavailable(error){
|
|
|
22
24
|
if(error?.status!=null)return error.status>=500||error.status===401;
|
|
23
25
|
return !['forbidden','conflict','not_found','invalid_request','revision_mismatch','idempotency_conflict'].includes(error?.code);
|
|
24
26
|
}
|
|
25
|
-
/** The Org agent identity belongs to: the selected Teams context
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
catch(error){if(teamsUnavailable(error))return {server:'local',org:'local',principal:null,offline:true,reason:error.message};throw error;}
|
|
29
|
-
}
|
|
27
|
+
/** The Org agent identity belongs to: the selected Teams context as this home records it (read locally, no request),
|
|
28
|
+
* or this home's local store when none is selected. A context named but unreadable here is this home's error, and throws. */
|
|
29
|
+
export function agentScope(context){return agentDoor('scope',[],{context});}
|
|
30
30
|
function store(root){
|
|
31
31
|
mkdirSync(root,{recursive:true,mode:0o700});const db=new DatabaseSync(join(root,'agent-state.sqlite'));
|
|
32
32
|
db.exec(`PRAGMA busy_timeout=5000; PRAGMA journal_mode=WAL;
|
|
@@ -260,27 +260,42 @@ export function ensureForkAgent(options) {
|
|
|
260
260
|
export function agentRunForKey(root,org,agent,key) {
|
|
261
261
|
const db=store(root);try{return db.prepare('SELECT * FROM agent_runs WHERE org=? AND agent_id=? AND run_id=? ORDER BY rowid DESC LIMIT 1').get(org,agent,key)??null;}finally{db.close();}
|
|
262
262
|
}
|
|
263
|
+
const declarationHead=(root,org,key)=>{const db=store(root);try{return db.prepare('SELECT agent_id FROM agent_declaration_heads WHERE org=? AND key=?').get(org,key)?.agent_id??null;}finally{db.close();}};
|
|
264
|
+
const endedHere=(root,org,id)=>{const db=store(root);try{return !!db.prepare('SELECT 1 FROM agent_runs WHERE org=? AND agent_id=? AND end_reason IS NOT NULL LIMIT 1').get(org,id);}finally{db.close();}};
|
|
263
265
|
function recordDeclaration(root,org,id,declaration){
|
|
264
266
|
const db=store(root);try{db.prepare('UPDATE agent_intents SET declaration_json=? WHERE org=? AND id=? AND declared=0').run(JSON.stringify(declaration),org,id);}finally{db.close();}
|
|
265
267
|
}
|
|
266
|
-
/**
|
|
267
|
-
*
|
|
268
|
+
/** The identity a run starts as without asking Teams: the daemon-minted id and retained intent. An identity not yet
|
|
269
|
+
* declared is declared later under the same id (synchronizeLocalAgents). Nothing is granted from this local record;
|
|
270
|
+
* `identity.synced` says whether Teams has the declaration, and the record's own `synced: false` that Teams was not read. */
|
|
268
271
|
function localAgent(options,{key,name,declaration,assignment,parent=null,starter}){
|
|
269
272
|
const {root,org,server,profiles,profile,machine=null}=options;
|
|
270
273
|
const rootProfile=profiles.default?'default':profile;
|
|
271
274
|
const rootIntent=declareAgentIntent({root,org,server,profiles,profile:rootProfile,key:'root',machine,refresh:false});
|
|
272
275
|
recordDeclaration(root,org,rootIntent.id,{kind:'root',profile:rootProfile});
|
|
273
|
-
|
|
276
|
+
let intent=key==='root'?rootIntent:declareAgentIntent({root,org,server,profiles,profile,key,machine});
|
|
277
|
+
// An agent Teams ended is never started again: its end, once synchronized, is on its runs here (synchronizeAgentEnds),
|
|
278
|
+
// and the key's next generation is minted in its place, with fresh memory.
|
|
279
|
+
if(key!=='root'&&endedHere(root,org,intent.id))intent=nextAgentGeneration(root,intent,machine);
|
|
274
280
|
if(key!=='root')recordDeclaration(root,org,intent.id,declaration);
|
|
275
281
|
const above=parent??(key==='root'?null:{id:rootIntent.id,path:basename(root)});
|
|
276
282
|
const identity={id:intent.id,kind:'agent',org,name,path:above?`${above.path}/${name}`:name,parent:above?.id??null,
|
|
277
283
|
lineage:{starter},sponsor:options.sponsor??null,explicit_sponsor:options.sponsor??null,profile,state:'active',
|
|
278
|
-
synced
|
|
284
|
+
synced:!!intent.declared,main_session:null,revision:0};
|
|
279
285
|
const assigned=assignAgent(root,org,intent.id,assignment);
|
|
280
286
|
return {identity,intended:intent.intended,assignment:assigned,assignments:[assignment],org,synced:false};
|
|
281
287
|
}
|
|
282
288
|
export function localCardAgent(options){
|
|
283
|
-
const {card,role,profile}=options;
|
|
289
|
+
const {root,org,server,profiles,card,role,profile}=options;
|
|
290
|
+
// A card role's assignment the manager moved to another agent (`workflow reassign --agent`) runs as that agent: the
|
|
291
|
+
// active assignment names it, not the key's own head, and an agent whose end was synchronized here is not used.
|
|
292
|
+
const held=activeAgentAssignments(root).find(item=>item.org===org&&item.kind==='card'&&item.target===card&&item.role===role);
|
|
293
|
+
if(held&&held.agent_id!==declarationHead(root,org,`${server}/${org}/card/${card}/${role}`)&&!endedHere(root,org,held.agent_id)){
|
|
294
|
+
const record=localAgentRecord(root,org,held.agent_id);
|
|
295
|
+
const identity=record?{...record.identity,synced:true}:{id:held.agent_id,kind:'agent',org,name:held.agent_id,path:held.agent_id,parent:null,
|
|
296
|
+
lineage:{starter:null},sponsor:options.sponsor??null,explicit_sponsor:options.sponsor??null,profile,state:'active',synced:true,main_session:null,revision:0};
|
|
297
|
+
return {identity,intended:record?.intended??resolveAgentProfile(profiles,profile),assignment:held.id,assignments:[{kind:'card',target:card,role}],org,synced:false};
|
|
298
|
+
}
|
|
284
299
|
return localAgent(options,{key:`card/${card}/${role}`,name:`${card}-${role}`,assignment:{kind:'card',target:card,role},
|
|
285
300
|
declaration:{kind:'card',card,role,profile,sponsor:options.sponsor??null,starterSession:options.starterSession??null},
|
|
286
301
|
starter:options.starterSession?{kind:'session',id:options.starterSession}:{kind:'automation',id:`board:${basename(options.root)}`}});
|
|
@@ -459,6 +474,13 @@ export function agentPreamble(agent,{main=true,previous=[]}={}){
|
|
|
459
474
|
const {identity:a,intended,assignments=[]}=agent;
|
|
460
475
|
return `You are agent ${a.path} (${a.id}), ${main?'its main session':'a concurrent session'}.${a.synced===false?' This identity was minted on this machine and is not yet declared on a Teams server; it keeps this id when it is.':''}\nSponsor: ${a.sponsor??'none yet'}; parent: ${a.parent??'none'}; lineage: ${JSON.stringify(a.lineage)}.\nAssignments: ${assignments.map(value=>`${value.kind} ${value.target}, role ${value.role}`).join("; ")}; profile: ${a.profile}; permission layers: ${JSON.stringify(intended.resolved.permissions.layers)}.\nAsk again with supercode agent whoami. List your sessions with supercode agent sessions; read a transcript only when needed.${previous.length?'\nEarlier sessions:\n'+previous.map(s=>`${s.session} · ${s.machine??'unknown machine'} · ended ${s.ended_at??'live'} · ${s.end_reason??''}`).join('\n'):''}`;
|
|
461
476
|
}
|
|
477
|
+
/** The agent's reader context this home already holds (a session of it was bound here before), read from the contexts
|
|
478
|
+
* file without a request, or null. A launch on this machine hands it to the session, so its Teams verbs run delegated
|
|
479
|
+
* from the first turn; a context that has since expired fails there, as an absent one does. */
|
|
480
|
+
export function localReaderContext(org,id){
|
|
481
|
+
const contexts=new WorkspaceContexts(supercodeHome()).read().contexts??{};
|
|
482
|
+
return Object.entries(contexts).find(([,value])=>value?.principal_id===id&&value?.team_id===org)?.[0]??null;
|
|
483
|
+
}
|
|
462
484
|
export function agentProfileMismatch(intent,resolved){return compareAgentProfile(intent?.resolved??intent,resolved);}
|
|
463
485
|
|
|
464
486
|
export function agentEnvironment(agent,{root,context,session=null,main=true}={}) {
|
package/bin/agent-door.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// The orchestrator's synchronous host reaches Teams through the installed neutral SDK.
|
|
3
|
-
import { fromContext } from '@volter/teams/context';
|
|
3
|
+
import { fromContext, WorkspaceContexts } from '@volter/teams/context';
|
|
4
4
|
import { supercodeHome } from '@volter/teams/home';
|
|
5
5
|
import { openMachine, normalMachineName } from '@volter/teams/machine-door';
|
|
6
6
|
import { hostname } from 'node:os';
|
|
@@ -11,6 +11,9 @@ const reach=machine=>machine&&normalMachineName(machine)!==normalMachineName(hos
|
|
|
11
11
|
let input='';for await(const chunk of process.stdin)input+=chunk;
|
|
12
12
|
try{
|
|
13
13
|
const {op,args=[],context}=JSON.parse(input);
|
|
14
|
+
// No context selected: identity is this home's own (Org `local`). A context that is named but unreadable is an error.
|
|
15
|
+
const named=context??process.env.SUPERCODE_CONTEXT??new WorkspaceContexts(supercodeHome()).read().current;
|
|
16
|
+
if(op==='scope'&&(!named||named==='local')){process.stdout.write(JSON.stringify({server:'local',org:'local',principal:null,offline:true,reason:'no Teams context is selected'})+'\n');process.exit(0);}
|
|
14
17
|
const {client,context:scope}=fromContext({supercodeHome:supercodeHome(),name:context??process.env.SUPERCODE_CONTEXT});
|
|
15
18
|
const team=client.team(scope.team_id),options={signal:AbortSignal.timeout(20000)};
|
|
16
19
|
let result;
|
package/board/dispatch.mjs
CHANGED
|
@@ -20,7 +20,7 @@ import { readEventFleet } from './event-fleet.mjs';
|
|
|
20
20
|
let eventFleetRoot = null;
|
|
21
21
|
import { inspectCompletionEvidence, noCodeChange, openReviewPullRequests, reviewDue, withCompletionEvidence, reviewCandidate, workflowRevision, retainWorkflowTick } from './ci.mjs';
|
|
22
22
|
import { runtimeDoor } from './runtime-door.mjs';
|
|
23
|
-
import {
|
|
23
|
+
import { agentScope, localCardAgent, localReaderContext, localAgentSessions, synchronizeLocalAgents, runBudgetRefusal, runsAwaitingUsage, recordRunUsage, agentPreamble, agentEnvironment, recordAgentRun, pendingAgentRuns, synchronizeAgentRun, abandonAgentRun,agentProfileMismatch,activeAgentAssignments,finishAgentAssignment,synchronizeAgentEnds, agentRunForSession } from '../agents.mjs';
|
|
24
24
|
import { materializeAgentHome } from '../agent-home.mjs';
|
|
25
25
|
import { memorySnapshotSync } from '../hermes-cli.mjs';
|
|
26
26
|
import { createRequire } from 'node:module';
|
|
@@ -684,28 +684,20 @@ export function launchRun(board, entry, id, lane, fleet, settings) {
|
|
|
684
684
|
// the model it runs is recorded with the session, for caps that count by model
|
|
685
685
|
const meta = { status, role: spec.role, slot, model: task.model_override ?? lane.model ?? null, ...(spec.role === 'reviewer' ? { review_candidate: reviewCandidate(board, id) } : {}) };
|
|
686
686
|
try {
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
if(!team.offline){
|
|
693
|
-
try{
|
|
694
|
-
agent=ensureCardAgent(cardOptions);
|
|
695
|
-
previous=agentDoor('sessions',[agent.identity.id,{limit:200}],{context}).items??[];
|
|
696
|
-
reader=agentDoor('readerContext',[agent.identity.id,machine],{context,timeout:60000});
|
|
697
|
-
}catch(error){if(!teamsUnavailable(error))throw error;tx(board,db=>event(db,id,'agent_identity_local',{reason:error.message},runId));}
|
|
698
|
-
}
|
|
699
|
-
agent??=localCardAgent(cardOptions);
|
|
687
|
+
// Teams is never on a launch's path: the card's agent is its identity minted on this host under its retained intent,
|
|
688
|
+
// and its sessions are joined to it off this path, after the launch. No Teams answer starts, stops or holds a card.
|
|
689
|
+
const context=process.env.SUPERCODE_CONTEXT,team=agentScope(context);
|
|
690
|
+
const agent=localCardAgent({root:board.root,org:team.org,server:team.server,profiles:entry.profiles,
|
|
691
|
+
profile:lane.name,card:id,role:spec.role??slot,sponsor:task.sponsor??null});
|
|
700
692
|
// G3: a run of an agent over its budget does not start; the reason waits on the card until the window passes
|
|
701
693
|
const budgets=[...(entry.profiles?.default?.budgets??[]),...(lane.name!=='default'?entry.profiles?.[lane.name]?.budgets??[]:[])];
|
|
702
694
|
const refused=budgets.length?runBudgetRefusal(board.root,agent.identity.id,budgets,new Date().toISOString()):null;
|
|
703
695
|
if(refused)throw Object.assign(new Error(`not started, over budget: ${refused}`),{launchDeferred:true});
|
|
704
|
-
previous
|
|
696
|
+
const previous=localAgentSessions(board.root,team.org,agent.identity.id);
|
|
705
697
|
prompt=`${agentPreamble(agent,{previous:previous.map(item=>({session:item.session,machine:item.machine,ended_at:item.ended_at,end_reason:item.end_reason}))})}\n\n${prompt}`;
|
|
706
698
|
Object.assign(meta,{agent_id:agent.identity.id,agent_org:team.org,agent_context:context??null,
|
|
707
699
|
agent_profile_intent:structuredClone(agent.intended.resolved),
|
|
708
|
-
agent_env:agentEnvironment(agent,{root:board.root,context:
|
|
700
|
+
agent_env:agentEnvironment(agent,{root:board.root,context:normalMachine(task.machine??localMachine())===localMachine()?localReaderContext(team.org,agent.identity.id)??undefined:undefined})});
|
|
709
701
|
// The agent's own memory reaches every session of it: headless runs open its home; a pane, which may sit on
|
|
710
702
|
// another machine, gets the home's snapshot in its first prompt. A shared profile's memory is never used.
|
|
711
703
|
const agentHome=materializeAgentHome(board.root,team.org,agent.identity.id,lane);
|
|
@@ -718,14 +710,7 @@ export function launchRun(board, entry, id, lane, fleet, settings) {
|
|
|
718
710
|
setRunSession(db,runId,{launch_deferred:false,launch_deferred_error:null,restart_authorized:false});
|
|
719
711
|
event(db,id,'restart_authorization_consumed',{},runId);
|
|
720
712
|
});
|
|
721
|
-
if(result?.session_id){
|
|
722
|
-
const ref=`${result.harness}:${result.session_id}`;
|
|
723
|
-
recordAgentRun(board.root,team.org,agent.identity.id,{session:ref,machine:result.machine,run:runId});
|
|
724
|
-
if(agent.synced!==false){
|
|
725
|
-
try{synchronizeAgentRun(board.root,team.org,{agent_id:agent.identity.id,session_ref:ref,machine:result.machine},{context,intended:meta.agent_profile_intent});}
|
|
726
|
-
catch(error){tx(board,db=>event(db,id,'agent_session_sync_pending',{session:ref,agent:agent.identity.id,error:error.message},runId));}
|
|
727
|
-
}
|
|
728
|
-
}
|
|
713
|
+
if(result?.session_id)recordAgentRun(board.root,team.org,agent.identity.id,{session:`${result.harness}:${result.session_id}`,machine:result.machine,run:runId});
|
|
729
714
|
return result;
|
|
730
715
|
} catch(error) {
|
|
731
716
|
// An issued asynchronous launch may bind later. Its original permission
|
|
@@ -1831,7 +1816,7 @@ function adoptSession(board, id, who, { request = null, open = request, progress
|
|
|
1831
1816
|
const agentRole = view(board, (db) => adoptedRole(db, board, entry0, card0, beforeRun)) ?? 'implementer';
|
|
1832
1817
|
const laneName = view(board, (db) => evaluate(entry0.workflow.roles?.[agentRole] ?? 'card.assignee', scopeOf(db, board, card0, { params: entry0.params }))) ?? assignee ?? card0?.assignee;
|
|
1833
1818
|
const lane = laneName ? lanes?.[laneName] : null, context = process.env.SUPERCODE_CONTEXT, team = agentScope(context);
|
|
1834
|
-
// The request is claimed before this adoption's first write outside the board (the agent
|
|
1819
|
+
// The request is claimed before this adoption's first write outside the board (the agent store, below), in
|
|
1835
1820
|
// a transaction that finds it still open and the card's run the one read: a request answered or claimed meanwhile, or
|
|
1836
1821
|
// a run that changed, ends it here with nothing written anywhere, and no other round acts on a request claimed here.
|
|
1837
1822
|
const stale = (db) => {
|
|
@@ -1845,11 +1830,8 @@ function adoptSession(board, id, who, { request = null, open = request, progress
|
|
|
1845
1830
|
event(db, id, 'adoption_claimed', { request, session: who, ...newClaim(ttl) });
|
|
1846
1831
|
});
|
|
1847
1832
|
progress.claimed = request !== null;
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
try { agent = ensureCardAgent({ root: board.root, org: team.org, server: team.server, profiles: entry0.profiles, profile: lane.name, card: id, role: agentRole, sponsor: card0?.sponsor ?? null, machine: normalMachine(host) === localMachine() ? null : host, context }); }
|
|
1851
|
-
catch (error) { if (!teamsUnavailable(error)) throw error; }
|
|
1852
|
-
}
|
|
1833
|
+
// The adopted session joins the role's agent as a launch does: its identity minted on this host, never asked of Teams.
|
|
1834
|
+
const agent = lane ? localCardAgent({ root: board.root, org: team.org, server: team.server, profiles: entry0.profiles, profile: lane.name, card: id, role: agentRole, sponsor: card0?.sponsor ?? null }) : null;
|
|
1853
1835
|
// A session another agent already owns is refused before anything is recorded: the run would name one agent and the
|
|
1854
1836
|
// agent store another, and every round after would trip on it.
|
|
1855
1837
|
const owner = agent ? agentRunForSession(board.root, [`${harness}:${sessionId}`]) : null;
|
package/index.d.ts
CHANGED
|
@@ -128,10 +128,10 @@ export function synchronizeAgentRun(root: Path, org: string, row: {agent_id: str
|
|
|
128
128
|
export function agentRunForSession(root: Path, references: Array<string | null | undefined>): Record<string, unknown> | null;
|
|
129
129
|
export function agentPreamble(agent: ReturnType<typeof ensureCardAgent>, options?: {main?: boolean; previous?: Array<{session: string; machine?: string | null; ended_at?: number | null; end_reason?: string | null}>}): string;
|
|
130
130
|
export function agentEnvironment(agent: ReturnType<typeof ensureCardAgent>, options: {root: Path; context?: string; session?: string | null; main?: boolean}): Record<string, string>;
|
|
131
|
-
/** The Org for agent identity; `offline` when
|
|
131
|
+
/** The Org for agent identity, read from this home's selected context without a request; `offline` when none is selected. Throws when a named context cannot be read. */
|
|
132
132
|
export function agentScope(context?: string): {server: string; org: string; principal: string | null; offline?: true; reason?: string};
|
|
133
133
|
export function teamsUnavailable(error: unknown): boolean;
|
|
134
|
-
/**
|
|
134
|
+
/** The identity a run starts as without asking Teams; `identity.synced` is false until Teams has its declaration, made later under the same id. */
|
|
135
135
|
export function localCardAgent(options: Parameters<typeof ensureCardAgent>[0]): ReturnType<typeof ensureCardAgent> & {synced: false};
|
|
136
136
|
export function localProfileAgent(options: Parameters<typeof ensureProfileAgent>[0]): ReturnType<typeof ensureCardAgent> & {synced: false};
|
|
137
137
|
export function localAgentRecord(root: Path, org: string, id: string): ReturnType<typeof ensureCardAgent> | null;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@volter/supercode-orchestrator",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.37",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "The orchestrator runtime over the Volter Harness ontology: one typed operational model whose folder is its serialization, read and written through the harness orchestration doors (docs/ORCHESTRATOR-IR.md)",
|
|
6
6
|
"exports": {
|