@volter/supercode-orchestrator 0.4.160 → 0.5.1
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 +16 -5
- package/adapters/chat-sdk.mjs +4 -1
- package/adapters/rh2.mjs +18 -20
- package/agent-home.mjs +35 -0
- package/agent-profiles.mjs +53 -0
- package/agents.mjs +443 -0
- package/bin/agent-door.mjs +53 -0
- package/bin/runtime-door.mjs +23 -0
- package/bin/runtime.mjs +59 -4
- package/board/agent-declarations.mjs +29 -0
- package/board/ci.mjs +6 -1
- package/board/cli.mjs +158 -31
- package/board/dispatch.mjs +299 -121
- package/board/engine.mjs +30 -10
- package/board/event-fleet.mjs +59 -0
- package/board/event-relay.mjs +45 -0
- package/board/event-stream.mjs +104 -0
- package/board/incidents.mjs +57 -0
- package/board/published.mjs +86 -0
- package/board/runtime-door.mjs +12 -0
- package/board/store.mjs +39 -6
- package/board/tools.mjs +1 -0
- package/board/wake.mjs +17 -56
- package/board/workflow.mjs +28 -7
- package/board/workspace.mjs +18 -15
- package/commands.mjs +2 -2
- package/conversations.mjs +108 -0
- package/cron.mjs +30 -4
- package/gates.mjs +3 -2
- package/hermes-cli.mjs +19 -2
- package/index.d.ts +43 -2
- package/index.mjs +4 -0
- package/installed-apps.mjs +52 -0
- package/ir.mjs +3 -3
- package/kanban-worker.mjs +16 -6
- package/loop.mjs +211 -21
- package/mcp/bridge.mjs +13 -2
- package/mcp/tools.mjs +17 -0
- package/mcp/verbs.mjs +29 -7
- package/ontology.d.ts +246 -2
- package/operator.mjs +6 -1
- package/package.json +2 -2
- package/reducer.mjs +133 -28
- package/webhook.mjs +8 -4
package/board/wake.mjs
CHANGED
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
import { watch, existsSync, readFileSync, readdirSync, writeFileSync, renameSync, unlinkSync, realpathSync } from 'node:fs';
|
|
4
4
|
import { basename, dirname, join, resolve } from 'node:path';
|
|
5
5
|
import { randomUUID } from 'node:crypto';
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
6
|
+
import { followFleet } from './event-fleet.mjs';
|
|
7
|
+
import { boardEventRelay } from './event-relay.mjs';
|
|
8
8
|
import { fileURLToPath } from 'node:url';
|
|
9
9
|
|
|
10
10
|
export function signalBoardWrite(root, actor) {
|
|
@@ -20,12 +20,14 @@ export function signalBoardWrite(root, actor) {
|
|
|
20
20
|
/** One coalescing queue shared by standalone serve and the hosted dispatcher. */
|
|
21
21
|
export function watchBoard({ root, binary, env, log = () => {} }) {
|
|
22
22
|
const watchers = new Map();
|
|
23
|
-
let pending = null, waiter = null, closed = false
|
|
23
|
+
let pending = null, waiter = null, closed = false;
|
|
24
|
+
let relay = null;
|
|
24
25
|
const wake = (source, at = Date.now()) => {
|
|
25
26
|
if (closed) return;
|
|
26
27
|
if (!pending) pending = { at, sources: new Set() };
|
|
27
28
|
pending.at = Math.min(pending.at, at);
|
|
28
29
|
pending.sources.add(source);
|
|
30
|
+
if (!source.startsWith('session-events')) void relay?.wake();
|
|
29
31
|
waiter?.();
|
|
30
32
|
};
|
|
31
33
|
const directory = (path, key, accept) => {
|
|
@@ -61,7 +63,12 @@ export function watchBoard({ root, binary, env, log = () => {} }) {
|
|
|
61
63
|
directory(join(root, 'kanban'), 'board-registry', name => ['boards', 'ztrack.json', 'current'].includes(name));
|
|
62
64
|
directory(boards, 'board-registry', () => true);
|
|
63
65
|
const dirs = [join(root, 'kanban'), ...(existsSync(boards) ? readdirSync(boards).map(name => join(boards, name)) : [])];
|
|
66
|
+
file(join(root,'kanban.db'),'board-storage');
|
|
67
|
+
file(join(root,'kanban.db-wal'),'board-storage');
|
|
64
68
|
for (const dir of dirs) {
|
|
69
|
+
file(join(dir,'kanban.db'),'board-storage');
|
|
70
|
+
file(join(dir,'kanban.db-wal'),'board-storage');
|
|
71
|
+
directory(join(dir,'board'),'board-storage',name=>name.endsWith('.json'));
|
|
65
72
|
file(join(dir, 'ztrack.json'), 'board-config');
|
|
66
73
|
try {
|
|
67
74
|
const config = JSON.parse(readFileSync(join(dir, 'ztrack.json'), 'utf8'));
|
|
@@ -82,57 +89,11 @@ export function watchBoard({ root, binary, env, log = () => {} }) {
|
|
|
82
89
|
}
|
|
83
90
|
}
|
|
84
91
|
};
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
const command = env.SUPERCODE_TEAMS_ENTRY ? process.execPath : binary;
|
|
91
|
-
const args = [...(env.SUPERCODE_TEAMS_ENTRY ? [env.SUPERCODE_TEAMS_ENTRY] : []), 'sessions', 'list', '--watch', '--json', '--limit', '200'];
|
|
92
|
-
child = spawn(command, args, { cwd: tmpdir(), env, stdio: ['ignore', 'pipe', 'pipe'] });
|
|
93
|
-
child.stdout.setEncoding('utf8').on('data', chunk => {
|
|
94
|
-
buffer += chunk;
|
|
95
|
-
if (buffer.length > 8 * 1024 * 1024) { error = 'catalogue frame exceeds 8 MiB'; child.kill(); return; }
|
|
96
|
-
let end;
|
|
97
|
-
while ((end = buffer.indexOf('\n')) >= 0) {
|
|
98
|
-
const line = buffer.slice(0, end); buffer = buffer.slice(end + 1);
|
|
99
|
-
try {
|
|
100
|
-
const event = JSON.parse(line);
|
|
101
|
-
lastError = null;
|
|
102
|
-
if (['heartbeat', 'ping', 'keepalive'].includes(event.type)) continue;
|
|
103
|
-
if (event.type === 'catalog_reset' && Array.isArray(event.data?.items)) {
|
|
104
|
-
// Transcript revisions and sort order are not scheduling events.
|
|
105
|
-
// The safety sweep reads quiet timestamps; wake for session identity,
|
|
106
|
-
// availability and available delivery/control doors changing now.
|
|
107
|
-
const next = JSON.stringify(event.data.items.map(item => ({
|
|
108
|
-
id:item.id, harness:item.harness, machines:[...(item.machine_ids ?? [])].sort(),
|
|
109
|
-
bindings:(item.bindings ?? []).map(binding => ({ id:binding.id, machine:binding.machine_id,
|
|
110
|
-
incarnation:binding.incarnation_id, runtime:binding.native_runtime_id, session:binding.native_session_id,
|
|
111
|
-
harness:binding.harness, state:binding.state, capabilities:[...(binding.native_capabilities ?? [])].sort(),
|
|
112
|
-
})).sort((a,b)=>String(a.id).localeCompare(String(b.id))), actions:item.actions,
|
|
113
|
-
presence:item.activity?.presence, turn:item.activity?.turn,
|
|
114
|
-
status:item.live_status, delivery:item.delivery,
|
|
115
|
-
})).sort((a,b)=>String(a.id).localeCompare(String(b.id))));
|
|
116
|
-
if (next === catalogue) continue;
|
|
117
|
-
catalogue = next;
|
|
118
|
-
}
|
|
119
|
-
wake('session-catalogue');
|
|
120
|
-
} catch { /* CLI diagnostics are not events */ }
|
|
121
|
-
}
|
|
122
|
-
});
|
|
123
|
-
child.stderr.setEncoding('utf8').on('data', chunk => { error = (error + chunk).slice(-500); });
|
|
124
|
-
child.on('error', failure => { error = failure.message; });
|
|
125
|
-
child.on('close', () => {
|
|
126
|
-
child = null;
|
|
127
|
-
if (!closed) {
|
|
128
|
-
const reason = error.trim() || 'connection closed';
|
|
129
|
-
if (reason !== lastError) log(`dispatch session stream ended: ${reason}; reconnecting, safety sweep remains active`);
|
|
130
|
-
lastError = reason;
|
|
131
|
-
retry = setTimeout(follow, 5000);
|
|
132
|
-
}
|
|
133
|
-
});
|
|
134
|
-
};
|
|
135
|
-
refresh(); follow();
|
|
92
|
+
refresh();
|
|
93
|
+
const supercodeHome = env.SUPERCODE_HOME || (env.XDG_CONFIG_HOME ? join(env.XDG_CONFIG_HOME,'supercode') : join(env.HOME ?? '', '.config','supercode'));
|
|
94
|
+
relay = boardEventRelay({root,supercodeHome,log});
|
|
95
|
+
const fleet = followFleet({root,supercodeHome,wake,log});
|
|
96
|
+
void relay.wake();
|
|
136
97
|
return {
|
|
137
98
|
refresh,
|
|
138
99
|
wake,
|
|
@@ -148,9 +109,9 @@ export function watchBoard({ root, binary, env, log = () => {} }) {
|
|
|
148
109
|
return result ? { at: result.at, sources: [...result.sources] } : null;
|
|
149
110
|
},
|
|
150
111
|
close() {
|
|
151
|
-
closed = true; waiter?.();
|
|
112
|
+
closed = true; waiter?.(); fleet.close(); relay.close();
|
|
152
113
|
for (const handle of watchers.values()) handle.close();
|
|
153
|
-
watchers.clear();
|
|
114
|
+
watchers.clear();
|
|
154
115
|
},
|
|
155
116
|
};
|
|
156
117
|
}
|
package/board/workflow.mjs
CHANGED
|
@@ -64,8 +64,8 @@ export function statusOf(db, task) {
|
|
|
64
64
|
/** Creation history, not an editable display name or notification subscriber. */
|
|
65
65
|
export function creatorOf(db, id) {
|
|
66
66
|
const payload = json(one(db, "SELECT payload FROM task_events WHERE task_id=? AND kind='created' ORDER BY id LIMIT 1", id)?.payload, {}) ?? {};
|
|
67
|
-
|
|
68
|
-
return payload.
|
|
67
|
+
if (payload.source === 'document') return null;
|
|
68
|
+
return [payload.created_by,payload.creator,payload.by].find(ref => /^(?:person:|agent:|automation:|session:sc:|sc:[^:]+:[^:]+:|hermes-run:)/.test(ref ?? '')) ?? null;
|
|
69
69
|
}
|
|
70
70
|
|
|
71
71
|
export function completionOwnerOf(db, id) {
|
|
@@ -221,7 +221,7 @@ function onlyTicks(was, body) {
|
|
|
221
221
|
/** Seconds a card's runs spent by role, and the rest of its life as `waiting`. */
|
|
222
222
|
export function runWorkingSeconds(row, at = now()) {
|
|
223
223
|
const end = Number(row.ended_at ?? at);
|
|
224
|
-
return Math.max(0, end - Number(row.started_at ?? end));
|
|
224
|
+
return Math.max(0, end - Number(row.started_at ?? end) - Number(json(row.metadata,{})?.supercode?.stopped_seconds??0));
|
|
225
225
|
}
|
|
226
226
|
|
|
227
227
|
function timeOf(db, task) {
|
|
@@ -316,8 +316,8 @@ export function scopeOf(db, board, task, { params, event = {}, extra = {} } = {}
|
|
|
316
316
|
last_completed: lastCompleted ?? null,
|
|
317
317
|
events_since: (t, kinds = []) => all(db, `SELECT kind FROM task_events WHERE task_id = ? AND created_at >= ?${kinds.length ? ` AND kind IN (${kinds.map(() => '?').join(', ')})` : ''}`, task.id, t, ...kinds),
|
|
318
318
|
// the latest event of each kind on the card: kind → { created_at, payload }
|
|
319
|
-
last_events: Object.fromEntries(all(db, 'SELECT e.kind, e.created_at, e.payload FROM task_events e JOIN (SELECT kind, MAX(id) AS id FROM task_events WHERE task_id = ? GROUP BY kind) l ON l.id = e.id', task.id)
|
|
320
|
-
.map((r) => [r.kind, { created_at: r.created_at, payload: json(r.payload, {}) ?? {} }])),
|
|
319
|
+
last_events: Object.fromEntries(all(db, 'SELECT e.id, e.kind, e.created_at, e.payload FROM task_events e JOIN (SELECT kind, MAX(id) AS id FROM task_events WHERE task_id = ? GROUP BY kind) l ON l.id = e.id', task.id)
|
|
320
|
+
.map((r) => [r.kind, { id: r.id, created_at: r.created_at, payload: json(r.payload, {}) ?? {} }])),
|
|
321
321
|
comments_since: (t) => all(db, 'SELECT author, body, created_at FROM task_comments WHERE task_id = ? AND created_at >= ?', task.id, t),
|
|
322
322
|
// the latest commit naming this card on its work branches and default branch, in its workspace's repository (a
|
|
323
323
|
// subtask's: its arc's), as epoch seconds
|
|
@@ -437,6 +437,12 @@ export function fire(db, board, entry, id, name, { roles = [], payload = {}, ext
|
|
|
437
437
|
const to = take(db, board, entry, task, from, taken, scope, effects);
|
|
438
438
|
settle(db, board, entry, id, effects);
|
|
439
439
|
projectStatus(db, board, entry, id);
|
|
440
|
+
// A manager's or creator's explicit act releases a hold, and a run it ends is their decision: the ended-run audit
|
|
441
|
+
// never turns it into an incident that waits for a second act
|
|
442
|
+
const ended = task.current_run_id && one(db, 'SELECT ended_at FROM task_runs WHERE id = ?', task.current_run_id)?.ended_at != null ? task.current_run_id : null;
|
|
443
|
+
if ((scope.card.dispatch_hold || ended) && roles.some(role => ['manager','creator'].includes(role)) &&
|
|
444
|
+
(['resume','reclaim','replace_session','reopen','reopen_review'].includes(name) || (name === 'set' && ['assignee','machine','model'].includes(payload.field))))
|
|
445
|
+
record(db,id,'dispatch_released',{reason:`explicit ${name}`,incident:scope.card.last_events.run_incident?.payload?.run ?? null,...(ended ? { ended_run: ended } : {})});
|
|
440
446
|
queueTransitionNotice(db, board, entry, task, from, name, payload, beforeRun, effects);
|
|
441
447
|
return { from, to: statusOf(db, cardOf(db, id)), moved: to !== from, effects };
|
|
442
448
|
}
|
|
@@ -586,6 +592,8 @@ const FORMAT = {
|
|
|
586
592
|
// the reviewer is the lane that reviewed (the card's assignee while in review), unless the verdict names one
|
|
587
593
|
changes_requested: ({ e, before, after }) => ({ reason: e.reason ?? null, reviewer: e.reviewer ?? before.assignee ?? null, implementer: after.assignee }),
|
|
588
594
|
review_reopened: ({ after, to }) => ({ status: to, implementer: after.assignee }),
|
|
595
|
+
review_voided: ({ e, before }) => ({ reason: e.reason, review_request: before.last_events.review_requested?.id ?? null,
|
|
596
|
+
review_run: before.latest_review?.id ?? null, verdict: Math.max(before.last_events.review_pass?.id ?? 0, before.last_events.changes_requested?.id ?? 0) || null }),
|
|
589
597
|
// what arrived (an unblock says it), and where the card resumes
|
|
590
598
|
unblocked: ({ e, to }) => ({ reason: e.reason ?? null, ...(to !== 'ready' ? { status: to, resume_status: to } : {}) }),
|
|
591
599
|
gave_up: ({ e, after, params, outcome }) => ({ error: String(e.error ?? '').slice(0, 500) || null, failures: Number(after.consecutive_failures || 0), limit: after.max_retries ?? params.failure_limit, outcome }),
|
|
@@ -694,6 +702,18 @@ const ACTIONS = {
|
|
|
694
702
|
return;
|
|
695
703
|
}
|
|
696
704
|
if (!ASSIGNABLE.has(field)) throw new Error(`the workflow assigns ${field}, which is not a card field it may set`);
|
|
705
|
+
if (field === 'assignee') {
|
|
706
|
+
// A model override names a model of the harness it was chosen for; a lane of another harness cannot run it
|
|
707
|
+
// (a Claude model left on a Codex lane fails every turn), so it goes with the old lane, on the record
|
|
708
|
+
const task = one(ctx.db, 'SELECT assignee, model_override, provider_override FROM tasks WHERE id = ?', ctx.id);
|
|
709
|
+
const harnessOf = (name) => name ? ctx.entry.profiles?.[name]?.worker?.harness ?? null : null;
|
|
710
|
+
const before = harnessOf(task?.assignee), after = harnessOf(value);
|
|
711
|
+
if (task?.model_override && before && after && before !== after) {
|
|
712
|
+
run(ctx.db, 'UPDATE tasks SET model_override = NULL, provider_override = NULL WHERE id = ?', ctx.id);
|
|
713
|
+
record(ctx.db, ctx.id, 'model_override_cleared', { model: task.model_override, provider: task.provider_override ?? null,
|
|
714
|
+
from: task.assignee, from_harness: before, to: value, to_harness: after }, ctx.runId);
|
|
715
|
+
}
|
|
716
|
+
}
|
|
697
717
|
run(ctx.db, `UPDATE tasks SET ${field} = ? WHERE id = ?`, typeof value === 'boolean' ? Number(value) : value ?? null, ctx.id);
|
|
698
718
|
},
|
|
699
719
|
count(ctx, counter) {
|
|
@@ -1052,6 +1072,7 @@ export function storedStatus(db, board, entry, id) {
|
|
|
1052
1072
|
export function projectStatus(db, board, entry, id) {
|
|
1053
1073
|
if (!entry.workflow.stored_as?.length) return;
|
|
1054
1074
|
const { status, stored } = storedStatus(db, board, entry, id);
|
|
1055
|
-
|
|
1056
|
-
run(db, '
|
|
1075
|
+
// Only a change is written: an unchanged write still commits, and a commit wakes the dispatcher that called this.
|
|
1076
|
+
run(db, 'UPDATE tasks SET status = ? WHERE id = ? AND status IS NOT ?', stored, id, stored);
|
|
1077
|
+
run(db, 'INSERT INTO supercode_cards (task_id, stage) VALUES (?, ?) ON CONFLICT(task_id) DO UPDATE SET stage = excluded.stage WHERE supercode_cards.stage IS NOT excluded.stage', id, status);
|
|
1057
1078
|
}
|
package/board/workspace.mjs
CHANGED
|
@@ -1,9 +1,18 @@
|
|
|
1
1
|
// Filesystem/RPC work happens before a board transaction; the ownership check is pure.
|
|
2
|
-
import { spawnSync } from 'node:child_process';
|
|
3
2
|
import { existsSync } from 'node:fs';
|
|
4
3
|
import { basename, dirname } from 'node:path';
|
|
5
|
-
import {
|
|
6
|
-
import { localMachine, normalMachine,
|
|
4
|
+
import { runtimeDoor } from './runtime-door.mjs';
|
|
5
|
+
import { localMachine, normalMachine, commandEnv } from './dispatch.mjs';
|
|
6
|
+
|
|
7
|
+
// Repository identities are portable input values; the daemon owns their checkout resolution.
|
|
8
|
+
function repositoryId(value) {
|
|
9
|
+
let s=String(value??'').trim().replace(/\.git\/?$/,'').replace(/\/$/,'');
|
|
10
|
+
if(/^[^/@]+@[^:]+:/.test(s))s=s.replace(/^[^@]+@/,'').replace(':','/');
|
|
11
|
+
else if(/^[a-z]+:\/\//i.test(s)){const url=new URL(s);s=url.host+url.pathname;}
|
|
12
|
+
s=s.replace(/^github\.com\//i,'');
|
|
13
|
+
if(!/^[\w.-]+(?:\/[\w.-]+)+$/.test(s)||s.split('/').some(p=>p==='.'||p==='..'))throw new Error('repository must name a portable Git remote identity');
|
|
14
|
+
return s;
|
|
15
|
+
}
|
|
7
16
|
export const isWorkspacePath = value => /^(\/|~|[A-Za-z]:[\\/])/.test(value ?? '');
|
|
8
17
|
// The dispatcher's latest presence reading (observeFleet): a machine it reads as offline is not asked.
|
|
9
18
|
let presence = null;
|
|
@@ -12,17 +21,10 @@ function workspaceDoor(machine, verb, value, options = {}) {
|
|
|
12
21
|
if (machine && presence?.known === true && normalMachine(machine) !== localMachine() && !presence.online.has(normalMachine(machine))) {
|
|
13
22
|
throw new Error(`workspace on ${machine}: machine ${normalMachine(machine)} is offline`);
|
|
14
23
|
}
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
return map.resolve(value, options.branch, options);
|
|
20
|
-
}
|
|
21
|
-
const args = ['teams', 'workspace', verb, value, '--machine', machine];
|
|
22
|
-
for (const key of ['branch', 'card']) if (options[key]) args.push(`--${key}`, options[key]);
|
|
23
|
-
for (const key of ['solo', 'create']) if (options[key]) args.push(`--${key}`);
|
|
24
|
-
const r = spawnSync(supercodeBin(), args, { env: commandEnv(), encoding: 'utf8', timeout: 60_000 });
|
|
25
|
-
if (r.status !== 0) throw new Error(`workspace on ${machine}: ${(r.stderr || r.stdout || r.error?.message).trim()}`);
|
|
24
|
+
const local = !machine || normalMachine(machine) === localMachine();
|
|
25
|
+
const params = verb === 'list' ? {} : verb === 'inspect' || verb === 'register' ? {path:value} : {repository:value,...options};
|
|
26
|
+
const r = runtimeDoor(local?null:machine,`harness.v1.workspaces.${verb}`,params,{env:commandEnv()});
|
|
27
|
+
if (r.code !== 0) throw new Error(`workspace on ${machine}: ${(r.stderr || r.stdout || r.error?.message).trim()}`);
|
|
26
28
|
return JSON.parse(r.stdout);
|
|
27
29
|
}
|
|
28
30
|
export function declareWorkspace(kind, value, machine, branch = null) {
|
|
@@ -40,7 +42,8 @@ export function declareWorkspace(kind, value, machine, branch = null) {
|
|
|
40
42
|
* keeps its work in each of them (its own worktree aside). Another machine's map is not read from here. */
|
|
41
43
|
export function branchCheckouts(machine, branch) {
|
|
42
44
|
if (!branch || (machine && normalMachine(machine) !== localMachine())) return [];
|
|
43
|
-
|
|
45
|
+
const listed = workspaceDoor(null, 'list', null);
|
|
46
|
+
return (Array.isArray(listed) ? listed : listed.workspaces ?? listed.items ?? []).filter((row) => (branch === '*' || row.branch === branch) && existsSync(row.path));
|
|
44
47
|
}
|
|
45
48
|
/** The registered checkout of `repository` on `branch` on `machine`: where a card changing several repositories
|
|
46
49
|
* keeps its work in each of them. */
|
package/commands.mjs
CHANGED
|
@@ -15,12 +15,12 @@ export function parseCommand(text) {
|
|
|
15
15
|
* message. Text because the orchestrator does not own the worker's system
|
|
16
16
|
* prompt (§0.3). One line, stable, greppable by the mock worker in tests.
|
|
17
17
|
*/
|
|
18
|
-
export function frame(profile, key, sender, senderName, at, text) {
|
|
18
|
+
export function frame(profile, key, sender, senderName, at, text, identity = null) {
|
|
19
19
|
const where = [key.platform, key.kind, key.chat_id, key.thread_id ? `thread ${key.thread_id}` : null].filter(Boolean).join(' ');
|
|
20
20
|
// In a Room the author reads as the token that addresses them (`aaron <@prn_…>`): written back, it is a mention RH2
|
|
21
21
|
// records and notifies, so the agent learns the form from every line it hears.
|
|
22
22
|
const who = key.platform === 'rh2' && /^prn_[a-z0-9]+$/.test(String(sender)) ? `${senderName ?? sender} <@${sender}>` : senderName ? `${senderName} (${sender})` : sender;
|
|
23
|
-
return `[channel: ${where} · from: ${who} · at: ${at} · profile: ${profile}]\n${text}`;
|
|
23
|
+
return `[channel: ${where} · from: ${who} · identity: ${identity?.classification==='proven'?`${identity.principal.name} (${identity.principal.id}; proven)`:identity?.label??'unresolved'} · at: ${at} · profile: ${profile}]\n${text}`;
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
export const REPLY = Object.freeze({
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
// The agent layer as the reducer computes it (docs/architecture/orchestrator.md §2.10): which agent a
|
|
2
|
+
// profile is, conversations separate from sessions (G5, amended by M3), the speaker policy that picks
|
|
3
|
+
// the receiving agents on a shared surface (G1), and usage summed against budgets (G3).
|
|
4
|
+
// Pure: no I/O and no clock; time comes from the caller's `now`.
|
|
5
|
+
|
|
6
|
+
import { surfaceKeyString } from './ir.mjs';
|
|
7
|
+
|
|
8
|
+
/** A named agent of this home: declared in `agents`, or a profile's own agent of the same name. */
|
|
9
|
+
export function isAgent(state, name) {
|
|
10
|
+
return Boolean(name && (state.agents?.[name] || state.profiles?.[name]));
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** The profile an agent runs as. */
|
|
14
|
+
export function profileOfAgent(state, agent) {
|
|
15
|
+
return state.agents?.[agent]?.profile ?? (state.profiles?.[agent] ? agent : null);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** The binding slot an agent holds on a surface: the surface's key for a profile's own agent, else qualified. */
|
|
19
|
+
export function agentSlot(profileName, agent, keyString) {
|
|
20
|
+
return !agent || agent === profileName ? keyString : `${keyString}@${agent}`;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export const conversationId = (key) => `thread:${surfaceKeyString(key)}`;
|
|
24
|
+
|
|
25
|
+
/** The thread a surface shows, created on first sight with the routed agent as its default speaker. */
|
|
26
|
+
export function ensureConversation(state, key, routedAgent, now) {
|
|
27
|
+
const id = conversationId(key);
|
|
28
|
+
state.conversations ??= {};
|
|
29
|
+
return state.conversations[id] ??= {
|
|
30
|
+
id, kind: 'thread', surface: key, participants: [],
|
|
31
|
+
speaker: { kind: 'addressed', default: routedAgent }, history: { kind: 'platform' },
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Add a participant (an agent's name, or a person as `<platform>:<user id>`); its first role stays. */
|
|
36
|
+
export function addParticipant(conversation, principal, role, now) {
|
|
37
|
+
const live = conversation.participants.find((p) => p.principal === principal && !p.until);
|
|
38
|
+
if (!live) conversation.participants.push({ principal, role, since: now });
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const mentioned = (text, name) => new RegExp(`(^|[^\\w@])@${name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\b`, 'i').test(text ?? '');
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* The agents that receive a line (G1). `addressed`: the agents the line mentions, else the holder a
|
|
45
|
+
* transfer left, else the default; `all`: every agent in the conversation; `round_robin`: the next in
|
|
46
|
+
* order; `selector`: the selecting agent, which answers with a transfer; `floor`: what the meeting's
|
|
47
|
+
* adapter named. Never a hidden model call.
|
|
48
|
+
*/
|
|
49
|
+
export function receivingAgents(state, conversation, ev, routedAgent) {
|
|
50
|
+
const policy = conversation.speaker ?? { kind: 'addressed' };
|
|
51
|
+
const agentsIn = conversation.participants.filter((p) => !p.until && isAgent(state, p.principal)).map((p) => p.principal);
|
|
52
|
+
const fallback = conversation.holder ?? policy.default ?? routedAgent;
|
|
53
|
+
switch (policy.kind) {
|
|
54
|
+
case 'all': return agentsIn.length ? agentsIn : [fallback];
|
|
55
|
+
case 'round_robin': {
|
|
56
|
+
const order = (policy.order ?? []).filter((a) => isAgent(state, a));
|
|
57
|
+
if (!order.length) return [fallback];
|
|
58
|
+
const next = order[(order.indexOf(conversation.last_speaker) + 1) % order.length];
|
|
59
|
+
return [next];
|
|
60
|
+
}
|
|
61
|
+
case 'selector': return [isAgent(state, policy.selector) ? policy.selector : fallback];
|
|
62
|
+
case 'floor': return [isAgent(state, ev.speaker_agent) ? ev.speaker_agent : fallback];
|
|
63
|
+
default: {
|
|
64
|
+
const candidates = [...new Set([...agentsIn, ...Object.keys(state.agents ?? {}), ...Object.keys(state.profiles ?? {})])];
|
|
65
|
+
const named = candidates.filter((a) => mentioned(ev.text, a));
|
|
66
|
+
return named.length ? named : [fallback];
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Store a session's usage, recomputed whole from its native records. */
|
|
72
|
+
export function recordUsage(state, session, agent, usage) {
|
|
73
|
+
state.usage ??= {};
|
|
74
|
+
state.usage[session] = { ...(agent ? { agent } : {}), usage };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const tokens = (u) => (u.input_tokens ?? 0) + (u.output_tokens ?? 0);
|
|
78
|
+
|
|
79
|
+
/** An agent's use within a budget's window (`run`: this session only). */
|
|
80
|
+
export function usedWithin(state, agent, period, now, session = null) {
|
|
81
|
+
const day = now.slice(0, 10), month = now.slice(0, 7);
|
|
82
|
+
let total = 0, cost = 0;
|
|
83
|
+
for (const [ref, record] of Object.entries(state.usage ?? {})) {
|
|
84
|
+
if (period === 'run' ? ref !== session : record.agent !== agent) continue;
|
|
85
|
+
for (const u of record.usage ?? []) {
|
|
86
|
+
if (period === 'day' && !u.at.startsWith(day)) continue;
|
|
87
|
+
if (period === 'month' && !u.at.startsWith(month)) continue;
|
|
88
|
+
total += tokens(u);
|
|
89
|
+
cost += u.cost?.amount ?? 0;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return { tokens: total, cost };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** The budget a start or resume would exceed, as a sentence, or null. A gate, never a pause. */
|
|
96
|
+
export function budgetRefusal(state, profile, agent, now, session = null) {
|
|
97
|
+
const layers = [state.profiles?.default?.budgets ?? [], profile === state.profiles?.default ? [] : profile?.budgets ?? []];
|
|
98
|
+
for (const budget of layers.flat()) {
|
|
99
|
+
const used = usedWithin(state, agent, budget.period, now, session);
|
|
100
|
+
if (budget.limit?.tokens != null && used.tokens >= budget.limit.tokens) {
|
|
101
|
+
return `${agent} has used ${used.tokens} of its ${budget.limit.tokens}-token ${budget.period} budget`;
|
|
102
|
+
}
|
|
103
|
+
if (budget.limit?.cost != null && used.cost >= budget.limit.cost.amount) {
|
|
104
|
+
return `${agent} has spent ${used.cost} of its ${budget.limit.cost.amount} ${budget.limit.cost.currency} ${budget.period} budget`;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return null;
|
|
108
|
+
}
|
package/cron.mjs
CHANGED
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
import { surfaceKeyString } from './ir.mjs';
|
|
14
14
|
import { homeChannel } from './ir.mjs';
|
|
15
15
|
import { endBinding, ensureRuntime, leaseId, newObligation, permissionResponse, reply, startWorker } from './reducer.mjs';
|
|
16
|
+
import { budgetRefusal } from './conversations.mjs';
|
|
16
17
|
|
|
17
18
|
/** Retry delays after the 1st, 2nd and 3rd send attempt (§4.3 "retry pending obligations"). */
|
|
18
19
|
export const BACKOFF_MINUTES = Object.freeze([1, 2, 4]);
|
|
@@ -805,7 +806,20 @@ function launchFire(state, profile, job, fire, prompt, now, effects) {
|
|
|
805
806
|
deliverResult(state, profile, { job, fire }, fire.error, true, now, effects);
|
|
806
807
|
return;
|
|
807
808
|
}
|
|
808
|
-
|
|
809
|
+
// Which session the fire runs in (C8): `current` (the conversation it delivers to — Hermes's attach_to_session),
|
|
810
|
+
// `main` (its agent's main conversation), `session:<id>` (that one), else `isolated`: a session of its own.
|
|
811
|
+
const sessionTarget = job.session_target ?? (job.attach_to_session === true ? 'current' : 'isolated');
|
|
812
|
+
if (sessionTarget === 'current' && attachFire(state, profile, job, fire, prompt, now, effects)) return;
|
|
813
|
+
if (sessionTarget === 'main' && attachTo(state, profile, mainBinding(profile), job, fire, prompt, now, effects)) return;
|
|
814
|
+
if (sessionTarget.startsWith('session:') && attachTo(state, profile, Object.entries(profile.bindings).find(([ks, b]) => !b.ended_at && !ks.includes('#') && b.worker.session_id === sessionTarget.slice(8)), job, fire, prompt, now, effects)) return;
|
|
815
|
+
// G3: a fire of an agent over its budget does not start; it fails with the reason, which is told
|
|
816
|
+
const refused = budgetRefusal(state, profile, profile.name, now);
|
|
817
|
+
if (refused) {
|
|
818
|
+
finishFire(profile, fire, job, 'failed', now, `not started: ${refused}`, effects);
|
|
819
|
+
deliverResult(state, profile, { job, fire }, fire.error, true, now, effects);
|
|
820
|
+
effects.push({ type: 'budget_refused', profile: profile.name, agent: profile.name, reason: refused });
|
|
821
|
+
return;
|
|
822
|
+
}
|
|
809
823
|
const rt = ensureRuntime(state);
|
|
810
824
|
|
|
811
825
|
const key = { platform: 'cron', kind: 'dm', chat_id: job.id };
|
|
@@ -815,17 +829,24 @@ function launchFire(state, profile, job, fire, prompt, now, effects) {
|
|
|
815
829
|
key,
|
|
816
830
|
worker: { harness: profile.worker.harness, session_id: fire.id, locator: null },
|
|
817
831
|
started_at: now, last_activity_at: now, ended_at: null, end_reason: null,
|
|
818
|
-
|
|
832
|
+
agent: profile.name, recurrence: { job_id: job.id }, residue: { fire_id: fire.id },
|
|
819
833
|
};
|
|
820
834
|
fire.started_at = now;
|
|
821
835
|
const worker = { ...profile.worker, model: job.model ?? profile.worker.model, cwd: job.workdir ?? profile.worker.cwd };
|
|
822
836
|
// The prompt rides the queue: `handleWorker`'s `started` case flushes it once
|
|
823
837
|
// the lease exists, the same path an inbound message takes.
|
|
824
838
|
(rt.queues[leaseId(profile.name, ks)] ??= []).push({ text: prompt, at: now, context_from: job.context_from ?? null, skills: job.skills ?? [], job_id: job.id });
|
|
825
|
-
effects.push({ type: 'runtime_start', profile: profile.name, keyString: ks, worker, prompt });
|
|
839
|
+
effects.push({ type: 'runtime_start', profile: profile.name, keyString: ks, worker, prompt, agent: profile.name });
|
|
826
840
|
effects.push({ type: 'save' });
|
|
827
841
|
}
|
|
828
842
|
|
|
843
|
+
/** The agent's main conversation on this profile: the live channel binding with the latest activity. */
|
|
844
|
+
function mainBinding(profile) {
|
|
845
|
+
return Object.entries(profile.bindings)
|
|
846
|
+
.filter(([ks, b]) => !b.ended_at && !ks.includes('#') && !b.recurrence && !['cron', 'webhook'].includes(b.key.platform))
|
|
847
|
+
.sort(([, a], [, b]) => String(b.last_activity_at ?? '').localeCompare(String(a.last_activity_at ?? '')))[0];
|
|
848
|
+
}
|
|
849
|
+
|
|
829
850
|
/**
|
|
830
851
|
* `attach_to_session`: the fire is a turn in the live session of the conversation it delivers to, taken the way an
|
|
831
852
|
* inbound message there is: it steers the running turn where the worker takes input mid-turn, waits behind it
|
|
@@ -839,13 +860,18 @@ function attachFire(state, profile, job, fire, prompt, now, effects) {
|
|
|
839
860
|
const found = Object.entries(profile.bindings).find(([ks, b]) => !b.ended_at && !ks.includes('#')
|
|
840
861
|
&& b.key.platform === target.platform && String(b.key.chat_id ?? '') === String(target.chat_id ?? '')
|
|
841
862
|
&& String(b.key.thread_id ?? '') === String(target.thread_id ?? ''));
|
|
863
|
+
return attachTo(state, profile, found, job, fire, prompt, now, effects);
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
/** The fire as a turn in a live binding's session (steering, queued or sent), when there is one. */
|
|
867
|
+
function attachTo(state, profile, found, job, fire, prompt, now, effects) {
|
|
842
868
|
if (!found) return false;
|
|
843
869
|
const [keyString, binding] = found;
|
|
844
870
|
const rt = ensureRuntime(state);
|
|
845
871
|
const lid = leaseId(profile.name, keyString);
|
|
846
872
|
if (!rt.leases[lid] && !rt.turns[lid] && !rt.starting[lid]) {
|
|
847
873
|
if (binding.worker.session_id) binding.residue.resume_runtime_id ??= binding.residue.runtime_id ?? binding.worker.session_id;
|
|
848
|
-
startWorker(state, profile, keyString, effects);
|
|
874
|
+
if (!startWorker(state, profile, keyString, effects, now)) return false;
|
|
849
875
|
}
|
|
850
876
|
binding.last_activity_at = now;
|
|
851
877
|
const text = `[scheduled: ${job.name ?? job.id}] ${prompt}`;
|
package/gates.mjs
CHANGED
|
@@ -78,7 +78,7 @@ function scriptEnv() {
|
|
|
78
78
|
* else with python3; the cwd is the job's workdir, else the scripts folder.
|
|
79
79
|
* Returns `{ok, output}`; a failure's output is the reason, in Hermes's words.
|
|
80
80
|
*/
|
|
81
|
-
export async function runJobScript(profile, scriptPath, workdir = null, { env = process.env, signal = null, secrets = [] } = {}) {
|
|
81
|
+
export async function runJobScript(profile, scriptPath, workdir = null, { env = process.env, signal = null, secrets = [], automation = null } = {}) {
|
|
82
82
|
const raw = String(scriptPath ?? '');
|
|
83
83
|
if (raw.includes('\0')) return { ok: false, output: `Blocked: script path contains a NUL byte: ${JSON.stringify(raw)}` };
|
|
84
84
|
const scriptsDir = join(profile.dir, 'scripts');
|
|
@@ -100,7 +100,8 @@ export async function runJobScript(profile, scriptPath, workdir = null, { env =
|
|
|
100
100
|
const out = { stdout: [], stderr: [] };
|
|
101
101
|
const size = { stdout: 0, stderr: 0 };
|
|
102
102
|
let finished = false;
|
|
103
|
-
|
|
103
|
+
// A scheduled script is an automation: what it files names the automation, never an agent or the OS user (C4)
|
|
104
|
+
const child = spawn(argv[0], argv[1], { cwd, env: { ...scriptEnv(), HERMES_HOME: profile.dir, ...(automation ? { SUPERCODE_AUTOMATION: automation } : {}) }, detached: true, stdio: ['ignore', 'pipe', 'pipe'] });
|
|
104
105
|
const kill = () => { try { process.kill(-child.pid, 'SIGKILL'); } catch { /* already gone */ } };
|
|
105
106
|
const end = (result) => {
|
|
106
107
|
if (finished) return;
|
package/hermes-cli.mjs
CHANGED
|
@@ -40,7 +40,10 @@ export function hermesInstall() {
|
|
|
40
40
|
export function hermesToolEnv(profileDir) {
|
|
41
41
|
const install = hermesInstall();
|
|
42
42
|
const env = { HERMES_HOME: profileDir };
|
|
43
|
-
|
|
43
|
+
// Hermes's folder may hold other programs (a global supercode beside it): the worker's `supercode` stays the one
|
|
44
|
+
// this daemon runs, ahead of it
|
|
45
|
+
const own = process.env.SUPERCODE_BIN ? dirname(process.env.SUPERCODE_BIN) : null;
|
|
46
|
+
if (install?.bin) env.PATH = [own, install.bin, process.env.PATH ?? ''].filter(Boolean).join(':');
|
|
44
47
|
return env;
|
|
45
48
|
}
|
|
46
49
|
|
|
@@ -102,7 +105,12 @@ elif op in ("allow", "revoke"):
|
|
|
102
105
|
save_env_value(var, ",".join(current + [a["user_id"]]))
|
|
103
106
|
else:
|
|
104
107
|
store.revoke(a["platform"], a["user_id"])
|
|
105
|
-
|
|
108
|
+
if _allowlist_env_for_platform(a["platform"]):
|
|
109
|
+
_sync_allowlist_remove(a["platform"], a["user_id"])
|
|
110
|
+
elif a["user_id"] in current:
|
|
111
|
+
# a platform Hermes does not name (rh2) keeps its list where allow wrote it; Hermes's remover returns early
|
|
112
|
+
from hermes_cli.config import save_env_value
|
|
113
|
+
save_env_value(var, ",".join(x for x in current if x != a["user_id"]))
|
|
106
114
|
out = True
|
|
107
115
|
elif op in ("admin_add", "admin_remove"):
|
|
108
116
|
from hermes_cli.config import load_config, save_config
|
|
@@ -341,6 +349,15 @@ export async function memorySnapshot(profileDir) {
|
|
|
341
349
|
return runHermes(profileDir, { op: 'snapshot' });
|
|
342
350
|
}
|
|
343
351
|
|
|
352
|
+
/** memorySnapshot for a synchronous caller (the board's launch). An unreadable store contributes nothing. */
|
|
353
|
+
export function memorySnapshotSync(profileDir) {
|
|
354
|
+
const install = hermesInstall();
|
|
355
|
+
if (!install?.python) return '';
|
|
356
|
+
const run = spawnSync(install.python, ['-c', HERMES_TOOL], { input: JSON.stringify({ op: 'snapshot' }), encoding: 'utf8', timeout: 30_000, cwd: install.source, env: hermesEnv(install, profileDir) });
|
|
357
|
+
try { const out = hermesAnswer('snapshot', { stdout: run.stdout ?? '', stderr: run.stderr || run.error?.message || '' }); return typeof out === 'string' ? out : ''; }
|
|
358
|
+
catch { return ''; }
|
|
359
|
+
}
|
|
360
|
+
|
|
344
361
|
/**
|
|
345
362
|
* A schedule string as Hermes parses it (`{ kind: once|interval|cron, … }`), or null when no Hermes is
|
|
346
363
|
* installed; throws Hermes's own refusal. Synchronous: schedules are parsed where a job is built.
|
package/index.d.ts
CHANGED
|
@@ -101,6 +101,43 @@ export type UserId = string;
|
|
|
101
101
|
export type JobId = string;
|
|
102
102
|
export type JsonValue = string | number | boolean | null | JsonValue[] | { [key: string]: JsonValue };
|
|
103
103
|
|
|
104
|
+
// C7 host APIs refer to the neutral Teams identity contract. Profile records remain the ontology's.
|
|
105
|
+
export type AgentIdentity = import('@volter/teams/http-client').AgentRecord;
|
|
106
|
+
export function resolveAgentProfile(profiles: Record<string, Profile>, name: string, overlays?: Array<Record<string, unknown>>): {profile: string; layers: string[]; resolved: Record<string, unknown>};
|
|
107
|
+
export function profileDifferences(intended: unknown, actual: unknown): Array<{field: string; intended: unknown; actual: unknown}>;
|
|
108
|
+
export function sessionAgent(options: {starter?: {kind?: string; id?: string; agent?: string | null}; declared?: string | null; asAgent?: string | null; profileAgent?: string | null}): string | null;
|
|
109
|
+
export function agentStateHome(root: Path, org: string, id: string): Path;
|
|
110
|
+
export function recordNativeAgentSession(root: Path, runtimeRef: string, session: string): Record<string, unknown> | null;
|
|
111
|
+
export function materializeAgentHome(root: Path, org: string, id: string, profile: Profile): Path;
|
|
112
|
+
export function declareAgentIntent(options: {root: Path; org: string; server: string; profiles: Record<string, Profile>; profile: string; key: string; machine?: string | null}): Record<string, unknown>;
|
|
113
|
+
export function ensureRootAgent(options: {root: Path; org: string; server: string; profiles: Record<string, Profile>; profile?: string; context?: string; machine?: string | null}): AgentIdentity;
|
|
114
|
+
export function ensureCardAgent(options: {root: Path; org: string; server: string; profiles: Record<string, Profile>; profile: string; card: string; role: string; sponsor?: string | null; context?: string; machine?: string | null; starterSession?: string | null}): {identity: AgentIdentity; intended: ReturnType<typeof resolveAgentProfile>; assignment: string; assignments: Array<Record<string, unknown>>; org: string};
|
|
115
|
+
export function ensureProfileAgent(options: {root: Path; org: string; server: string; profiles: Record<string, Profile>; profile: string; context?: string; machine?: string | null}): ReturnType<typeof ensureCardAgent>;
|
|
116
|
+
export function ensureForkAgent(options: {root: Path; org: string; server: string; profiles: Record<string, Profile>; parent: string; session: string; name: string; key: string; context?: string; machine?: string | null}): ReturnType<typeof ensureCardAgent>;
|
|
117
|
+
export function agentRunForKey(root: Path, org: string, agent: string, key: string): Record<string, unknown> | null;
|
|
118
|
+
export function agentIntent(root: Path, org: string, id: string): Record<string, unknown> | null;
|
|
119
|
+
export function assignAgent(root: Path, org: string, id: string, assignment: {kind: string; target: string; role: string}): string;
|
|
120
|
+
export function reassignAgent(root: Path, org: string, assignment: string, to: string, options?: {context?: string}): Record<string, unknown>;
|
|
121
|
+
export function activeAgentAssignments(root: Path): Array<Record<string, unknown>>;
|
|
122
|
+
export function finishAgentAssignment(root: Path, org: string, assignment: string, reason: string): void;
|
|
123
|
+
export function synchronizeAgentEnds(root: Path, org: string, options?: {context?: string}): Array<{agent: string; error: string}>;
|
|
124
|
+
export function recordAgentRun(root: Path, org: string, id: string, run: {session: string; machine?: string | null; run: string | number; profile?: unknown; main?: boolean; starterSession?: string | null}): void;
|
|
125
|
+
export function pendingAgentRuns(root: Path, org: string): Array<Record<string, unknown>>;
|
|
126
|
+
export function synchronizeAgentRun(root: Path, org: string, row: {agent_id: string; session_ref: string; machine?: string | null; main?: boolean | number; starter_session?: string | null}, options?: {context?: string; intended?: unknown}): import('@volter/teams/http-client').SessionIdentity & {runtime: unknown};
|
|
127
|
+
export function agentRunForSession(root: Path, references: Array<string | null | undefined>): Record<string, unknown> | null;
|
|
128
|
+
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;
|
|
129
|
+
export function agentEnvironment(agent: ReturnType<typeof ensureCardAgent>, options: {root: Path; context?: string; session?: string | null; main?: boolean}): Record<string, string>;
|
|
130
|
+
/** The Org for agent identity; `offline` when no Teams context is selected or reachable (situation 16). */
|
|
131
|
+
export function agentScope(context?: string): {server: string; org: string; principal: string | null; offline?: true; reason?: string};
|
|
132
|
+
export function teamsUnavailable(error: unknown): boolean;
|
|
133
|
+
/** Locally minted identities (`identity.synced === false`), declared later under the same id. */
|
|
134
|
+
export function localCardAgent(options: Parameters<typeof ensureCardAgent>[0]): ReturnType<typeof ensureCardAgent> & {synced: false};
|
|
135
|
+
export function localProfileAgent(options: Parameters<typeof ensureProfileAgent>[0]): ReturnType<typeof ensureCardAgent> & {synced: false};
|
|
136
|
+
export function localAgentRecord(root: Path, org: string, id: string): ReturnType<typeof ensureCardAgent> | null;
|
|
137
|
+
export function localAgentSessions(root: Path, org: string, id: string): Array<{session: string; machine: string | null; ended_at: number | null; end_reason: string | null}>;
|
|
138
|
+
export function synchronizeLocalAgents(options: {root: Path; profiles: Record<string, Profile>; context?: string; machine?: string | null}): Array<{agent: string; error: string}>;
|
|
139
|
+
export function agentProfileMismatch(intent: unknown, resolved: unknown): import('@volter/teams/agent-profile').ProfileComparison;
|
|
140
|
+
|
|
104
141
|
export type ChatType = 'dm' | 'group' | 'channel' | 'thread';
|
|
105
142
|
|
|
106
143
|
export interface RuntimeState {
|
|
@@ -137,6 +174,8 @@ export type OrchestratorEvent =
|
|
|
137
174
|
text: string; attachments: Attachment[]; at: Instant; adapter: Platform; guild_id: string | null;
|
|
138
175
|
/** The platform's id for this message: the threading, reaction and edit target (ORC-1 F4). */
|
|
139
176
|
message_id: string | null;
|
|
177
|
+
/** Enriched from the public identity record by the loop; never trusted from an adapter. */
|
|
178
|
+
identity?: Awaited<ReturnType<import('@volter/teams/http-client').TeamClient['identity']['resolve']>>['data'];
|
|
140
179
|
/** Our own echo (§4.1 step 0 drops it), a bot sender (`null` = the SDK's "unknown"), and whether we were @-mentioned. */
|
|
141
180
|
is_me: boolean; is_bot: boolean | null; mention: boolean;
|
|
142
181
|
}
|
|
@@ -173,7 +212,7 @@ export function expired(binding: Binding, reset: SessionReset, now: Instant): En
|
|
|
173
212
|
export function leaseId(profile: ProfileName, keyString: string): string;
|
|
174
213
|
|
|
175
214
|
export namespace events {
|
|
176
|
-
function inbound(fields: Omit<Extract<OrchestratorEvent, { type: 'inbound' }>, 'type' | 'attachments' | 'adapter' | 'guild_id' | 'sender_name' | 'message_id' | 'is_me' | 'is_bot' | 'mention'> & { attachments?: Attachment[]; adapter?: Platform; guild_id?: string | null; sender_name?: string | null; message_id?: string | null; is_me?: boolean; is_bot?: boolean | null; mention?: boolean }): Extract<OrchestratorEvent, { type: 'inbound' }>;
|
|
215
|
+
function inbound(fields: Omit<Extract<OrchestratorEvent, { type: 'inbound' }>, 'type' | 'identity' | 'attachments' | 'adapter' | 'guild_id' | 'sender_name' | 'message_id' | 'is_me' | 'is_bot' | 'mention'> & { attachments?: Attachment[]; adapter?: Platform; guild_id?: string | null; sender_name?: string | null; message_id?: string | null; is_me?: boolean; is_bot?: boolean | null; mention?: boolean }): Extract<OrchestratorEvent, { type: 'inbound' }>;
|
|
177
216
|
function tick(at: Instant, inactivity_s?: number | null, max_parallel?: number | null): Extract<OrchestratorEvent, { type: 'tick' }>;
|
|
178
217
|
function worker(fields: Omit<Extract<OrchestratorEvent, { type: 'worker' }>, 'type'>): Extract<OrchestratorEvent, { type: 'worker' }>;
|
|
179
218
|
function adapterStatus(fields: Omit<Extract<OrchestratorEvent, { type: 'adapter' }>, 'type' | 'error'> & { error?: string | null }): Extract<OrchestratorEvent, { type: 'adapter' }>;
|
|
@@ -184,7 +223,7 @@ export namespace events {
|
|
|
184
223
|
}
|
|
185
224
|
|
|
186
225
|
export function parseCommand(text: string): { name: string; args: string } | null;
|
|
187
|
-
export function frame(profile: ProfileName, key: SurfaceKey, sender: UserId, senderName: string | null, at: Instant, text: string): string;
|
|
226
|
+
export function frame(profile: ProfileName, key: SurfaceKey, sender: UserId, senderName: string | null, at: Instant, text: string, identity?: Extract<OrchestratorEvent,{type:'inbound'}>['identity'] | null): string;
|
|
188
227
|
export const COMMANDS: Set<string>;
|
|
189
228
|
export const REPLY: Record<string, string | ((arg: string) => string)>;
|
|
190
229
|
|
|
@@ -301,6 +340,8 @@ export class Orchestrator {
|
|
|
301
340
|
stop(): Promise<void>;
|
|
302
341
|
tick(at?: Instant): Promise<Effect[]>;
|
|
303
342
|
drainHosted(): Promise<void>;
|
|
343
|
+
/** A fork starts in a registered, unoccupied conversation on this execution host. */
|
|
344
|
+
forkRuntime(options: {profile: ProfileName; keyString: string; parent: string; locator: import('@volter/supercode-harness-sdk').SessionLocator; name: string; key: string; machine?: string | null}): Promise<{agent: AgentIdentity; locator: import('@volter/supercode-harness-sdk').SessionLocator; runtime: unknown}>;
|
|
304
345
|
dispatch(event: OrchestratorEvent): Promise<Effect[]>;
|
|
305
346
|
enqueue(event: OrchestratorEvent): void;
|
|
306
347
|
/** §7: effects produced by an MCP tool call, run on the loop's own queue. */
|
package/index.mjs
CHANGED
|
@@ -36,3 +36,7 @@ export { LOCK_FILE, thisHost, pidIsLive, leaseIsHere, leaseIsLive, readLease } f
|
|
|
36
36
|
export { adoptHome, adoptPersona, adoptSkills, managedSkills, skillSources, profileDirs, AGENTS_SKILLS } from './hermes-home.mjs';
|
|
37
37
|
export { hermesAccess, recordIncident, hermesToolEnv, hermesInstall } from './hermes-cli.mjs';
|
|
38
38
|
export { holdGatewayLock, gatewayLockHeld, acquireTokenLock, releaseTokenLock, looksLikeGateway, TOKEN_SCOPES } from './hermes-locks.mjs';
|
|
39
|
+
|
|
40
|
+
export { resolveAgentProfile, profileDifferences, sessionAgent } from "./agent-profiles.mjs";
|
|
41
|
+
export { agentScope, teamsUnavailable, localCardAgent, localProfileAgent, localAgentRecord, localAgentSessions, synchronizeLocalAgents, declareAgentIntent, ensureRootAgent, ensureCardAgent,ensureProfileAgent,ensureForkAgent,agentRunForKey, agentIntent, assignAgent, reassignAgent,activeAgentAssignments,finishAgentAssignment,synchronizeAgentEnds,recordAgentRun, recordNativeAgentSession,pendingAgentRuns, synchronizeAgentRun, agentRunForSession,agentPreamble, agentEnvironment, agentProfileMismatch } from "./agents.mjs";
|
|
42
|
+
export { agentStateHome,materializeAgentHome } from './agent-home.mjs';
|