@superjackfruitlabs/salaam 0.0.1 → 0.0.2
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/CHANGELOG.md +15 -0
- package/README.md +25 -17
- package/dist/src/a2a-backend.js +225 -0
- package/dist/src/a2a-inbound.js +487 -0
- package/dist/src/a2a-peer.js +370 -0
- package/dist/src/a2a-protocol.js +250 -0
- package/dist/src/agent-profiles.js +289 -0
- package/dist/src/children.js +132 -26
- package/dist/src/claude-provider.js +2 -179
- package/dist/src/cli-help.js +107 -6
- package/dist/src/cli.js +477 -13
- package/dist/src/config.js +26 -3
- package/dist/src/credentials.js +7 -2
- package/dist/src/external-runtime.js +1 -0
- package/dist/src/external-work.js +583 -0
- package/dist/src/harness-backend.js +466 -0
- package/dist/src/harness-container.js +450 -0
- package/dist/src/harness-model-gateway.js +104 -0
- package/dist/src/harness-prepare.js +88 -0
- package/dist/src/harness-probe.js +111 -0
- package/dist/src/harness-protocol.js +325 -0
- package/dist/src/harness-readiness.js +50 -0
- package/dist/src/harness-rpc.js +206 -0
- package/dist/src/harness-tool-identity.js +87 -0
- package/dist/src/harness-wire-bridge.js +387 -0
- package/dist/src/host.js +6 -1
- package/dist/src/house-extension.js +147 -64
- package/dist/src/live-provider.js +301 -0
- package/dist/src/managed.js +33 -3
- package/dist/src/protocol.js +9 -5
- package/dist/src/provider-auth.js +300 -0
- package/dist/src/provider-config.js +65 -17
- package/dist/src/provider-model.js +78 -0
- package/dist/src/provider-payload.js +264 -0
- package/dist/src/provider-transport.js +36 -0
- package/dist/src/provider-usage.js +59 -0
- package/dist/src/repository-snapshots.js +219 -0
- package/dist/src/service.js +274 -9
- package/dist/src/store.js +44 -2
- package/dist/src/supervisor.js +246 -35
- package/dist/src/task-budget.js +17 -8
- package/dist/src/task-context.js +15 -0
- package/dist/src/task-driver.js +38 -14
- package/dist/src/task-policy.js +20 -1
- package/dist/src/task-verification.js +3 -0
- package/dist/src/task-work.js +31 -0
- package/dist/src/team-work.js +378 -0
- package/dist/src/tui-view.js +65 -10
- package/dist/src/tui.js +4 -2
- package/dist/src/usage.js +18 -0
- package/dist/src/worker-boundary.js +298 -0
- package/dist/src/worker-workspaces.js +274 -0
- package/dist/src/workspaces.js +6 -2
- package/harness-runtime/codex-pins.json +10 -0
- package/harness-runtime/package-lock.json +560 -0
- package/harness-runtime/package.json +9 -0
- package/npm-shrinkwrap.json +57 -2
- package/package.json +2 -1
- package/src/a2a-backend.ts +254 -0
- package/src/a2a-inbound.ts +585 -0
- package/src/a2a-peer.ts +441 -0
- package/src/a2a-protocol.ts +286 -0
- package/src/agent-profiles.ts +379 -0
- package/src/children.ts +168 -27
- package/src/claude-provider.ts +2 -189
- package/src/cli-help.ts +107 -6
- package/src/cli.ts +602 -12
- package/src/config.ts +34 -3
- package/src/credentials.ts +9 -2
- package/src/external-runtime.ts +54 -0
- package/src/external-work.ts +663 -0
- package/src/harness-backend.ts +557 -0
- package/src/harness-container.ts +543 -0
- package/src/harness-model-gateway.ts +138 -0
- package/src/harness-prepare.ts +113 -0
- package/src/harness-probe.ts +150 -0
- package/src/harness-protocol.ts +388 -0
- package/src/harness-readiness.ts +71 -0
- package/src/harness-rpc.ts +244 -0
- package/src/harness-tool-identity.ts +91 -0
- package/src/harness-wire-bridge.ts +418 -0
- package/src/host.ts +11 -6
- package/src/house-extension.ts +165 -66
- package/src/live-provider.ts +347 -0
- package/src/managed.ts +43 -4
- package/src/protocol.ts +36 -8
- package/src/provider-auth.ts +336 -0
- package/src/provider-config.ts +89 -18
- package/src/provider-model.ts +86 -0
- package/src/provider-payload.ts +263 -0
- package/src/provider-transport.ts +51 -0
- package/src/provider-usage.ts +64 -0
- package/src/repository-snapshots.ts +259 -0
- package/src/service.ts +446 -11
- package/src/store.ts +60 -4
- package/src/supervisor.ts +294 -41
- package/src/task-budget.ts +24 -11
- package/src/task-context.ts +17 -0
- package/src/task-driver.ts +51 -15
- package/src/task-policy.ts +23 -3
- package/src/task-verification.ts +2 -0
- package/src/task-work.ts +46 -1
- package/src/team-work.ts +554 -0
- package/src/tui-client.ts +5 -0
- package/src/tui-view.ts +164 -12
- package/src/tui.ts +5 -2
- package/src/usage.ts +17 -0
- package/src/worker-boundary.ts +395 -0
- package/src/worker-workspaces.ts +390 -0
- package/src/workspaces.ts +4 -2
package/dist/src/supervisor.js
CHANGED
|
@@ -6,9 +6,13 @@ import { join, resolve } from 'node:path';
|
|
|
6
6
|
import { PROTOCOL_VERSION, canonical, object, ServiceError, textDigest, workerTools, workerCapabilities, string, } from './protocol.js';
|
|
7
7
|
import { PI_CLI, HOUSE_EXTENSION, POLICY_DIGEST, bundleDigest } from './config.js';
|
|
8
8
|
import { PiProcess, PiCommandError } from './pi.js';
|
|
9
|
-
import { OFFLINE_ROUTE, providerRoute,
|
|
10
|
-
import {
|
|
9
|
+
import { OFFLINE_ROUTE, providerRoute, providerHasOutputCap, } from './provider-config.js';
|
|
10
|
+
import { validateApiKey } from './credentials.js';
|
|
11
|
+
import { resolveProviderAuth } from './provider-auth.js';
|
|
12
|
+
import { bindProviderConfiguration } from './provider-model.js';
|
|
13
|
+
import { validateProviderPayload } from './provider-payload.js';
|
|
11
14
|
import { digest } from './protocol.js';
|
|
15
|
+
import { repositoryPath } from './repository-snapshots.js';
|
|
12
16
|
export function processIdentity(pid) {
|
|
13
17
|
try {
|
|
14
18
|
return (execFileSync('/bin/ps', ['-ww', '-p', String(pid), '-o', 'lstart=', '-o', 'command='], {
|
|
@@ -42,14 +46,70 @@ export class Supervisor extends EventEmitter {
|
|
|
42
46
|
managed;
|
|
43
47
|
children;
|
|
44
48
|
coding;
|
|
49
|
+
repositories;
|
|
50
|
+
team;
|
|
51
|
+
routeFor(id) {
|
|
52
|
+
const child = this.children?.owned(id);
|
|
53
|
+
return child
|
|
54
|
+
? (this.store.workerBoundary.view(child.id).binding.agent?.route ?? this.route)
|
|
55
|
+
: this.route;
|
|
56
|
+
}
|
|
57
|
+
async prepareProviderRoute(input) {
|
|
58
|
+
const route = structuredClone(input);
|
|
59
|
+
if (route.provider === 'salaam-proof')
|
|
60
|
+
return { route, output_cap: true, auth: undefined };
|
|
61
|
+
const auth = route.provider === 'anthropic' && this.apiKey
|
|
62
|
+
? { auth: { apiKey: this.apiKey }, env: {} }
|
|
63
|
+
: await resolveProviderAuth(route, this.options.providerCredentials, this.options.providerEnvironment);
|
|
64
|
+
const bound = bindProviderConfiguration(route, auth);
|
|
65
|
+
return { route: bound, output_cap: providerHasOutputCap(bound, auth), auth };
|
|
66
|
+
}
|
|
45
67
|
codingBinding(id) {
|
|
46
|
-
|
|
68
|
+
const child = this.children?.owned(id), coding = this.coding?.binding(id);
|
|
69
|
+
if (!child)
|
|
70
|
+
return coding;
|
|
71
|
+
const binding = this.store.workerBoundary.view(child.id).binding;
|
|
72
|
+
return binding.isolation && coding
|
|
73
|
+
? {
|
|
74
|
+
...coding,
|
|
75
|
+
files: binding.tools.includes('salaam_files'),
|
|
76
|
+
commands: coding.commands && binding.tools.includes('salaam_command'),
|
|
77
|
+
}
|
|
78
|
+
: undefined;
|
|
47
79
|
}
|
|
48
80
|
tools(id) {
|
|
81
|
+
const child = this.children?.owned(id);
|
|
82
|
+
if (child)
|
|
83
|
+
return JSON.parse(child.authority);
|
|
49
84
|
return workerTools(!!this.options.effectsEnabled, !!this.children?.owned(id), this.codingBinding(id));
|
|
50
85
|
}
|
|
86
|
+
childBoundary(id, epoch) {
|
|
87
|
+
const child = this.children?.owned(id);
|
|
88
|
+
return child ? this.store.workerBoundary.assert(child, epoch, this.routeFor(id)) : undefined;
|
|
89
|
+
}
|
|
51
90
|
route;
|
|
52
91
|
apiKey;
|
|
92
|
+
outputCap = true;
|
|
93
|
+
authInitialized = false;
|
|
94
|
+
providerCapabilities() {
|
|
95
|
+
return {
|
|
96
|
+
output_token_cap: this.outputCap,
|
|
97
|
+
durable_tasks: true,
|
|
98
|
+
warning: this.outputCap
|
|
99
|
+
? null
|
|
100
|
+
: 'No provider-enforced output cap; tasks must explicitly disable token budgeting',
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
assertTaskProviderSupported(limits, outputCap = this.outputCap) {
|
|
104
|
+
if (!outputCap && (limits.token_units !== null || limits.output_token_bound !== null))
|
|
105
|
+
throw new ServiceError('provider_capability', 'This provider authentication has no output cap; set limits.token_units to null and omit output_token_bound');
|
|
106
|
+
}
|
|
107
|
+
assertSessionProviderSupported(id, outputCap = this.outputCap) {
|
|
108
|
+
const parent = this.children?.owned(id)?.parent_session ?? id;
|
|
109
|
+
const task = this.tasks.work?.forSession(parent);
|
|
110
|
+
if (task)
|
|
111
|
+
this.assertTaskProviderSupported(task.limits, outputCap);
|
|
112
|
+
}
|
|
53
113
|
bootId = randomUUID();
|
|
54
114
|
workers = new Map();
|
|
55
115
|
monitor;
|
|
@@ -58,23 +118,46 @@ export class Supervisor extends EventEmitter {
|
|
|
58
118
|
super();
|
|
59
119
|
this.store = store;
|
|
60
120
|
this.options = options;
|
|
61
|
-
this.route = options.providerRoute ??
|
|
121
|
+
this.route = structuredClone(options.providerRoute ?? OFFLINE_ROUTE);
|
|
62
122
|
// Validate public configuration without trusting programmatic callers.
|
|
63
|
-
const checked = providerRoute(this.route.provider === 'salaam-proof' ? 'offline' : this.route.provider, this.route.provider
|
|
123
|
+
const checked = providerRoute(this.route.provider === 'salaam-proof' ? 'offline' : this.route.provider, this.route.provider !== 'salaam-proof' ? this.route.model : undefined, this.route.provider !== 'salaam-proof' ? this.route.max_tokens : undefined, this.route.workspace_id, this.route.connection);
|
|
64
124
|
if (canonical(checked) !== canonical(this.route))
|
|
65
125
|
throw new Error('Invalid provider route');
|
|
66
|
-
if (
|
|
67
|
-
this.apiKey = options.anthropicApiKey
|
|
68
|
-
? validateApiKey(options.anthropicApiKey)
|
|
69
|
-
: loadApiKey();
|
|
126
|
+
if (options.anthropicApiKey)
|
|
127
|
+
this.apiKey = validateApiKey(options.anthropicApiKey);
|
|
70
128
|
this.bundle = bundleDigest();
|
|
71
129
|
this.monitor = setInterval(() => {
|
|
72
130
|
for (const worker of this.workers.values())
|
|
73
|
-
if (worker.ready
|
|
74
|
-
|
|
131
|
+
if (worker.ready) {
|
|
132
|
+
if (Date.now() - worker.lastBeat > (options.heartbeatTimeout ?? 2500))
|
|
133
|
+
this.invalidate(worker, 'Worker heartbeat expired');
|
|
134
|
+
const child = this.children?.owned(worker.id);
|
|
135
|
+
if (child?.state === 'accepted') {
|
|
136
|
+
try {
|
|
137
|
+
this.childBoundary(worker.id, worker.epoch);
|
|
138
|
+
}
|
|
139
|
+
catch (error) {
|
|
140
|
+
this.invalidate(worker, 'Worker boundary revoked: ' + error.message);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
75
144
|
}, 250);
|
|
76
145
|
this.monitor.unref();
|
|
77
146
|
}
|
|
147
|
+
async providerAuth() {
|
|
148
|
+
if (this.route.provider === 'salaam-proof')
|
|
149
|
+
return undefined;
|
|
150
|
+
if (this.route.provider === 'anthropic' && this.apiKey)
|
|
151
|
+
return { auth: { apiKey: this.apiKey }, env: {} };
|
|
152
|
+
const auth = await resolveProviderAuth(this.route, this.options.providerCredentials, this.options.providerEnvironment);
|
|
153
|
+
Object.assign(this.route, bindProviderConfiguration(this.route, auth));
|
|
154
|
+
const outputCap = providerHasOutputCap(this.route, auth);
|
|
155
|
+
if (this.authInitialized && this.outputCap !== outputCap)
|
|
156
|
+
throw new Error('Provider output-cap capability changed; restart the service before using the new authentication');
|
|
157
|
+
this.outputCap = outputCap;
|
|
158
|
+
this.authInitialized = true;
|
|
159
|
+
return auth;
|
|
160
|
+
}
|
|
78
161
|
async recover() {
|
|
79
162
|
const rows = this.store.db.prepare("SELECT * FROM workers WHERE state!='exited'").all();
|
|
80
163
|
for (const row of rows) {
|
|
@@ -102,6 +185,10 @@ export class Supervisor extends EventEmitter {
|
|
|
102
185
|
}
|
|
103
186
|
}
|
|
104
187
|
async start(id) {
|
|
188
|
+
const child = this.children?.owned(id);
|
|
189
|
+
if (child && this.store.workerBoundary.view(child.id).binding.runtime !== 'native-pi')
|
|
190
|
+
throw new ServiceError('external_worker_owned', 'Use the external worker lifecycle; Pi cannot claim this session');
|
|
191
|
+
const route = this.routeFor(id);
|
|
105
192
|
const prior = this.workers.get(id);
|
|
106
193
|
if (prior && !prior.stopping)
|
|
107
194
|
throw new ServiceError('worker_exists', 'Worker already supervised');
|
|
@@ -113,7 +200,7 @@ export class Supervisor extends EventEmitter {
|
|
|
113
200
|
.prepare("SELECT 1 FROM workers WHERE session_id=? AND state!='exited' LIMIT 1")
|
|
114
201
|
.get(id))
|
|
115
202
|
throw new ServiceError('reconciliation_required', previous.error ?? 'Session requires reconciliation');
|
|
116
|
-
this.store.bindRoute(id,
|
|
203
|
+
this.store.bindRoute(id, route);
|
|
117
204
|
const epoch = this.store.allocate(id, this.bootId);
|
|
118
205
|
let accept;
|
|
119
206
|
let reject;
|
|
@@ -124,6 +211,7 @@ export class Supervisor extends EventEmitter {
|
|
|
124
211
|
const binding = randomBytes(16).toString('hex');
|
|
125
212
|
const frame = this.tasks.frame(id, binding);
|
|
126
213
|
const worker = {
|
|
214
|
+
route: structuredClone(route),
|
|
127
215
|
id,
|
|
128
216
|
epoch,
|
|
129
217
|
token: randomBytes(32).toString('hex'),
|
|
@@ -144,6 +232,9 @@ export class Supervisor extends EventEmitter {
|
|
|
144
232
|
void handshake.catch(() => { });
|
|
145
233
|
let timer;
|
|
146
234
|
try {
|
|
235
|
+
const child = this.children?.owned(id);
|
|
236
|
+
if (child)
|
|
237
|
+
this.store.workerBoundary.claim(child, epoch, route);
|
|
147
238
|
const workerDir = join(this.options.stateDir, 'workers', id);
|
|
148
239
|
mkdirSync(join(workerDir, 'agent'), { recursive: true, mode: 0o700 });
|
|
149
240
|
writeFileSync(join(workerDir, 'agent', 'settings.json'), JSON.stringify({
|
|
@@ -161,7 +252,8 @@ export class Supervisor extends EventEmitter {
|
|
|
161
252
|
frame,
|
|
162
253
|
effects: !!this.options.effectsEnabled && !this.children?.owned(id),
|
|
163
254
|
child: !!this.children?.owned(id),
|
|
164
|
-
|
|
255
|
+
worker_binding: this.childBoundary(id, epoch),
|
|
256
|
+
route: route,
|
|
165
257
|
coding: this.codingBinding(id),
|
|
166
258
|
};
|
|
167
259
|
const args = [
|
|
@@ -187,9 +279,9 @@ export class Supervisor extends EventEmitter {
|
|
|
187
279
|
'--tools',
|
|
188
280
|
this.tools(id).join(','),
|
|
189
281
|
'--provider',
|
|
190
|
-
|
|
282
|
+
route.provider,
|
|
191
283
|
'--model',
|
|
192
|
-
|
|
284
|
+
route.model,
|
|
193
285
|
];
|
|
194
286
|
if (previous.pi_session_file && existsSync(previous.pi_session_file)) {
|
|
195
287
|
const index = args.indexOf('--session-id');
|
|
@@ -208,7 +300,7 @@ export class Supervisor extends EventEmitter {
|
|
|
208
300
|
LANG: process.env.LANG ?? 'en_US.UTF-8',
|
|
209
301
|
PI_CODING_AGENT_DIR: join(workerDir, 'agent'),
|
|
210
302
|
SALAAM_WORKER_CONFIG: JSON.stringify(config),
|
|
211
|
-
...(
|
|
303
|
+
...(route.connection?.configuration ?? {}),
|
|
212
304
|
};
|
|
213
305
|
worker.pi = (this.options.launch ?? ((x) => new PiProcess(x.command, x.args, x.cwd, x.env)))({
|
|
214
306
|
command: process.execPath,
|
|
@@ -298,8 +390,7 @@ export class Supervisor extends EventEmitter {
|
|
|
298
390
|
const file = typeof state.sessionFile === 'string' ? resolve(state.sessionFile) : null;
|
|
299
391
|
if (!file || !file.startsWith(resolve(sessionDir) + '/'))
|
|
300
392
|
throw new Error('Pi session is outside its managed directory');
|
|
301
|
-
if (object(state.model).provider !==
|
|
302
|
-
object(state.model).id !== this.route.model)
|
|
393
|
+
if (object(state.model).provider !== route.provider || object(state.model).id !== route.model)
|
|
303
394
|
throw new Error('Pi selected an unapproved provider route');
|
|
304
395
|
if (worker.invalid)
|
|
305
396
|
throw new Error(worker.invalid);
|
|
@@ -307,7 +398,9 @@ export class Supervisor extends EventEmitter {
|
|
|
307
398
|
// persist a cursor into that ephemeral tree before its file exists.
|
|
308
399
|
if (existsSync(file))
|
|
309
400
|
await this.syncEntries(worker);
|
|
310
|
-
|
|
401
|
+
if (child)
|
|
402
|
+
this.store.workerBoundary.accepted(child, epoch, route);
|
|
403
|
+
this.store.state(id, epoch, 'ready', null, workerCapabilities(this.codingBinding(id), this.tools(id)), file);
|
|
311
404
|
worker.ready = true;
|
|
312
405
|
worker.lastBeat = Date.now();
|
|
313
406
|
}
|
|
@@ -362,6 +455,7 @@ export class Supervisor extends EventEmitter {
|
|
|
362
455
|
}
|
|
363
456
|
async bridge(id, method, token, body) {
|
|
364
457
|
const w = this.authenticate(id, token, body);
|
|
458
|
+
const route = w.route;
|
|
365
459
|
if (method === 'failure') {
|
|
366
460
|
if (!w.stopping)
|
|
367
461
|
this.invalidate(w, 'Required extension failed');
|
|
@@ -373,6 +467,7 @@ export class Supervisor extends EventEmitter {
|
|
|
373
467
|
try {
|
|
374
468
|
if (w.manifest)
|
|
375
469
|
throw new Error('Duplicate handshake');
|
|
470
|
+
const boundary = this.childBoundary(id, w.epoch);
|
|
376
471
|
const expected = {
|
|
377
472
|
protocol_version: PROTOCOL_VERSION,
|
|
378
473
|
bundle_digest: w.bundle,
|
|
@@ -380,13 +475,14 @@ export class Supervisor extends EventEmitter {
|
|
|
380
475
|
session_id: id,
|
|
381
476
|
pi_session_id: id,
|
|
382
477
|
epoch: w.epoch,
|
|
383
|
-
capabilities: workerCapabilities(this.codingBinding(id)),
|
|
478
|
+
capabilities: workerCapabilities(this.codingBinding(id), this.tools(id)),
|
|
384
479
|
tools: this.tools(id),
|
|
385
480
|
active_tools: this.tools(id),
|
|
386
481
|
...(this.codingBinding(id) ? { coding: this.codingBinding(id) } : {}),
|
|
387
|
-
provider:
|
|
388
|
-
route:
|
|
482
|
+
provider: route.provider,
|
|
483
|
+
route: route,
|
|
389
484
|
context_digest: textDigest(w.frame),
|
|
485
|
+
...(boundary ? { worker_binding: boundary } : {}),
|
|
390
486
|
};
|
|
391
487
|
for (const [key, value] of Object.entries(expected))
|
|
392
488
|
if (canonical(body[key]) !== canonical(value))
|
|
@@ -409,13 +505,87 @@ export class Supervisor extends EventEmitter {
|
|
|
409
505
|
if (w.attempt && this.store.cancelled(w.attempt))
|
|
410
506
|
throw new ServiceError('attempt_cancelled', 'Attempt cancellation revoked further grants');
|
|
411
507
|
try {
|
|
508
|
+
this.childBoundary(id, w.epoch);
|
|
509
|
+
if (method.startsWith('team.')) {
|
|
510
|
+
const child = this.children?.owned(id);
|
|
511
|
+
if (!child || !this.team || !w.attempt)
|
|
512
|
+
throw new Error('No owned team assignment');
|
|
513
|
+
const binding = this.store.workerBoundary.view(child.id).binding;
|
|
514
|
+
if (!binding.assignment || !binding.tools.includes('salaam_team'))
|
|
515
|
+
throw new Error('No team message authority');
|
|
516
|
+
const authorize = () => {
|
|
517
|
+
this.assertActive(id, w.epoch, w.attempt);
|
|
518
|
+
this.team.assert(binding);
|
|
519
|
+
this.store.taskGrantCheck?.(id);
|
|
520
|
+
};
|
|
521
|
+
authorize();
|
|
522
|
+
const request = string(body.request_id, 'request_id', 128), assignment = binding.assignment.id;
|
|
523
|
+
if (method === 'team.send')
|
|
524
|
+
return this.team.send(binding.owner_task, assignment, string(body.recipient, 'recipient', 128), string(body.text, 'text', 4096), request, authorize);
|
|
525
|
+
if (method === 'team.receive')
|
|
526
|
+
return this.team.receive(assignment, request, authorize);
|
|
527
|
+
if (method === 'team.ack')
|
|
528
|
+
return this.team.acknowledge(assignment, string(body.message_id, 'message_id', 128), request, authorize);
|
|
529
|
+
throw new ServiceError('invalid_field', 'Unknown team method', 400);
|
|
530
|
+
}
|
|
531
|
+
if (method === 'specialist.read' || method === 'specialist.report') {
|
|
532
|
+
const child = this.children?.owned(id);
|
|
533
|
+
if (!child || !this.repositories || !w.attempt)
|
|
534
|
+
throw new Error('No active specialist assignment');
|
|
535
|
+
this.assertActive(id, w.epoch, w.attempt);
|
|
536
|
+
const binding = this.store.workerBoundary.view(child.id).binding;
|
|
537
|
+
const tool = method === 'specialist.read' ? 'salaam_repository' : 'salaam_findings';
|
|
538
|
+
if (!binding.tools.includes(tool))
|
|
539
|
+
throw new Error('Specialist authority is unavailable');
|
|
540
|
+
this.store.taskGrantCheck?.(id);
|
|
541
|
+
if (method === 'specialist.report')
|
|
542
|
+
return this.repositories.report(binding, body.findings);
|
|
543
|
+
const args = object(body.args);
|
|
544
|
+
return this.store.request(`specialist-read:${id}:${w.epoch}`, string(body.tool_call_id, 'tool_call_id', 512), args, () => {
|
|
545
|
+
const result = this.repositories.read(binding, args);
|
|
546
|
+
this.store.event(id, 'specialist.read', {
|
|
547
|
+
attempt_id: w.attempt,
|
|
548
|
+
snapshot_digest: binding.repository?.digest,
|
|
549
|
+
argument_digest: digest(args),
|
|
550
|
+
result_digest: digest(result),
|
|
551
|
+
});
|
|
552
|
+
return result;
|
|
553
|
+
});
|
|
554
|
+
}
|
|
555
|
+
if (method === 'provider.auth') {
|
|
556
|
+
if (!this.children?.owned(id)) {
|
|
557
|
+
const auth = await this.providerAuth();
|
|
558
|
+
if (canonical(this.route) !== canonical(route))
|
|
559
|
+
throw new Error('Provider route changed');
|
|
560
|
+
w.providerOutputCap = this.outputCap;
|
|
561
|
+
this.assertSessionProviderSupported(id, this.outputCap);
|
|
562
|
+
return auth;
|
|
563
|
+
}
|
|
564
|
+
const prepared = await this.prepareProviderRoute(route);
|
|
565
|
+
const expectedCap = this.children?.owned(id)
|
|
566
|
+
? this.store.workerBoundary.view(this.children.owned(id).id).binding.agent?.output_cap
|
|
567
|
+
: this.outputCap;
|
|
568
|
+
if (canonical(prepared.route) !== canonical(route) ||
|
|
569
|
+
(expectedCap ?? this.outputCap) !== prepared.output_cap)
|
|
570
|
+
throw new Error('Provider capability or route changed; restart or renew admission');
|
|
571
|
+
w.providerOutputCap = prepared.output_cap;
|
|
572
|
+
this.assertSessionProviderSupported(id, prepared.output_cap);
|
|
573
|
+
const auth = prepared.auth;
|
|
574
|
+
return auth;
|
|
575
|
+
}
|
|
412
576
|
if (method === 'coding.files' || method === 'coding.command') {
|
|
413
|
-
if (
|
|
577
|
+
if (!this.coding || !this.codingBinding(id) || !w.attempt)
|
|
414
578
|
throw new Error('Coding authority is unavailable');
|
|
415
579
|
this.assertActive(id, w.epoch, w.attempt);
|
|
580
|
+
this.store.taskGrantCheck?.(id);
|
|
416
581
|
const args = object(body.args);
|
|
417
|
-
if (method === 'coding.files')
|
|
582
|
+
if (method === 'coding.files') {
|
|
583
|
+
if (!this.codingBinding(id)?.files)
|
|
584
|
+
throw new Error('File authority is unavailable');
|
|
585
|
+
if (this.children?.owned(id) && args.path !== undefined)
|
|
586
|
+
repositoryPath(args.path);
|
|
418
587
|
return this.coding.file(id, w.epoch, w.attempt, string(body.tool_call_id, 'tool_call_id', 512), args);
|
|
588
|
+
}
|
|
419
589
|
if (!this.codingBinding(id)?.commands)
|
|
420
590
|
throw new Error('Command execution is unavailable');
|
|
421
591
|
if (args.action === 'start')
|
|
@@ -452,11 +622,18 @@ export class Supervisor extends EventEmitter {
|
|
|
452
622
|
if (method.startsWith('child.')) {
|
|
453
623
|
if (!this.children || !w.attempt)
|
|
454
624
|
throw new Error('No active parent attempt');
|
|
455
|
-
|
|
456
|
-
if (method
|
|
625
|
+
// spawn() resolves a saved step before testing a fresh invocation/grant.
|
|
626
|
+
if (method !== 'child.spawn')
|
|
627
|
+
this.assertActive(id, w.epoch, w.attempt);
|
|
628
|
+
if (method === 'child.spawn') {
|
|
629
|
+
if (Object.keys(body).some((key) => !['epoch', 'step_key', 'prompt', 'budget', 'authority', 'profile'].includes(key)))
|
|
630
|
+
throw new ServiceError('invalid_child', 'Unsupported child admission field');
|
|
457
631
|
return this.children.spawn(id, w.epoch, w.attempt, string(body.step_key, 'step_key', 128), string(body.prompt, 'prompt', 32768), Number(body.budget), Array.isArray(body.authority)
|
|
458
632
|
? body.authority.map((x) => string(x, 'authority', 128))
|
|
459
|
-
:
|
|
633
|
+
: body.profile
|
|
634
|
+
? []
|
|
635
|
+
: ['salaam_probe'], body.profile === undefined ? undefined : string(body.profile, 'profile', 128));
|
|
636
|
+
}
|
|
460
637
|
if (method === 'child.list')
|
|
461
638
|
return this.children.list(id);
|
|
462
639
|
const child = string(body.child_id, 'child_id', 128);
|
|
@@ -494,22 +671,25 @@ export class Supervisor extends EventEmitter {
|
|
|
494
671
|
.get(String(body.source_key));
|
|
495
672
|
if (!source || source.session_id !== id || source.attempt_id !== (w.attempt ?? null))
|
|
496
673
|
throw new Error('Usage does not belong to this request');
|
|
497
|
-
this.store.transaction(() => this.store.usage.observe(id, string(body.source_key, 'source_key', 256),
|
|
674
|
+
this.store.transaction(() => this.store.usage.observe(id, string(body.source_key, 'source_key', 256), route.provider, route.model, body.usage, route.provider !== 'salaam-proof' ? 'estimated' : 'known'));
|
|
498
675
|
return { ok: true };
|
|
499
676
|
}
|
|
500
677
|
if (method === 'model') {
|
|
501
678
|
this.coding?.assertSafe(id);
|
|
502
679
|
this.store.taskGrantCheck?.(id);
|
|
503
|
-
if (body.provider !==
|
|
504
|
-
body.model !==
|
|
680
|
+
if (body.provider !== route.provider ||
|
|
681
|
+
body.model !== route.model ||
|
|
505
682
|
body.context_digest !== textDigest(this.tasks.frame(id, w.binding)))
|
|
506
683
|
throw new Error('Provider or task context is invalid');
|
|
507
|
-
if (
|
|
508
|
-
|
|
684
|
+
if (route.provider !== 'salaam-proof') {
|
|
685
|
+
if (w.providerOutputCap === undefined)
|
|
686
|
+
throw new Error('Provider authentication was not admitted');
|
|
687
|
+
this.assertSessionProviderSupported(id, w.providerOutputCap);
|
|
688
|
+
validateProviderPayload(body.payload, route, this.tasks.frame(id, w.binding), this.tools(id), !w.providerOutputCap);
|
|
509
689
|
if (body.payload_digest !== digest(body.payload))
|
|
510
|
-
throw new Error('
|
|
690
|
+
throw new Error('Provider payload digest changed');
|
|
511
691
|
}
|
|
512
|
-
if (
|
|
692
|
+
if (route.provider === 'salaam-proof') {
|
|
513
693
|
const payload = object(body.payload);
|
|
514
694
|
if (!Array.isArray(payload.messages))
|
|
515
695
|
throw new Error('Proof payload missing');
|
|
@@ -526,7 +706,7 @@ export class Supervisor extends EventEmitter {
|
|
|
526
706
|
body.payload_digest !== digest(payload))
|
|
527
707
|
throw new Error('Proof task payload is stale or invalid');
|
|
528
708
|
}
|
|
529
|
-
this.store.usage.admit(id, w.attempt, string(body.source_key, 'source_key', 256), String(body.provider), String(body.model), { input: Buffer.byteLength(JSON.stringify(body.payload)), output:
|
|
709
|
+
this.store.usage.admit(id, w.attempt, string(body.source_key, 'source_key', 256), String(body.provider), String(body.model), { input: Buffer.byteLength(JSON.stringify(body.payload)), output: route.max_tokens });
|
|
530
710
|
this.store.transaction(() => {
|
|
531
711
|
this.store.event(id, 'model.admitted', {
|
|
532
712
|
attempt_id: w.attempt ?? null,
|
|
@@ -556,6 +736,17 @@ export class Supervisor extends EventEmitter {
|
|
|
556
736
|
}
|
|
557
737
|
catch (error) {
|
|
558
738
|
this.store.taskBudgetDenied?.(id, error);
|
|
739
|
+
if (method.startsWith('specialist.') &&
|
|
740
|
+
error instanceof ServiceError &&
|
|
741
|
+
[
|
|
742
|
+
'not_found',
|
|
743
|
+
'binary_file',
|
|
744
|
+
'line_limit',
|
|
745
|
+
'invalid_field',
|
|
746
|
+
'unsafe_path',
|
|
747
|
+
'request_conflict',
|
|
748
|
+
].includes(error.code))
|
|
749
|
+
throw error;
|
|
559
750
|
if (method.startsWith('coding.') &&
|
|
560
751
|
error instanceof ServiceError &&
|
|
561
752
|
[
|
|
@@ -599,9 +790,25 @@ export class Supervisor extends EventEmitter {
|
|
|
599
790
|
}
|
|
600
791
|
}
|
|
601
792
|
inspect(id) {
|
|
793
|
+
const externalChild = this.children?.owned(id);
|
|
794
|
+
if (externalChild && this.store.workerBoundary.view(externalChild.id).binding.external)
|
|
795
|
+
return this.store.session(id);
|
|
796
|
+
const child = this.children?.owned(id);
|
|
797
|
+
const cap = child
|
|
798
|
+
? this.store.workerBoundary.view(child.id).binding.agent?.output_cap
|
|
799
|
+
: undefined;
|
|
602
800
|
const state = {
|
|
603
801
|
...this.store.session(id),
|
|
604
802
|
provider_route: this.store.providerRoute(id) ?? undefined,
|
|
803
|
+
provider_capabilities: cap === undefined
|
|
804
|
+
? this.providerCapabilities()
|
|
805
|
+
: {
|
|
806
|
+
output_token_cap: cap,
|
|
807
|
+
durable_tasks: true,
|
|
808
|
+
warning: cap
|
|
809
|
+
? null
|
|
810
|
+
: 'No provider-enforced output cap; tasks must explicitly disable token budgeting',
|
|
811
|
+
},
|
|
605
812
|
};
|
|
606
813
|
const w = this.workers.get(id);
|
|
607
814
|
if (w?.invalid)
|
|
@@ -805,6 +1012,7 @@ export class Supervisor extends EventEmitter {
|
|
|
805
1012
|
throw new Error(w.invalid ?? 'Attempt aborted');
|
|
806
1013
|
const entries = await this.readEntries(w);
|
|
807
1014
|
const result = object((await w.pi.command('get_last_assistant_text')).data);
|
|
1015
|
+
this.childBoundary(id, w.epoch);
|
|
808
1016
|
this.store.finish(work.attempt.id, outcomeError ? 'failed' : 'completed', String(result.text ?? ''), outcomeError, { epoch: w.epoch, ...entries });
|
|
809
1017
|
}
|
|
810
1018
|
catch (error) {
|
|
@@ -816,6 +1024,9 @@ export class Supervisor extends EventEmitter {
|
|
|
816
1024
|
w.pi.off('record', record);
|
|
817
1025
|
w.busy = false;
|
|
818
1026
|
delete w.attempt;
|
|
1027
|
+
const child = this.children?.owned(id);
|
|
1028
|
+
if (child && child.state !== 'accepted')
|
|
1029
|
+
await this.stopWorker(w);
|
|
819
1030
|
this.emit('idle', id);
|
|
820
1031
|
}
|
|
821
1032
|
return true;
|
package/dist/src/task-budget.js
CHANGED
|
@@ -34,14 +34,18 @@ export class TaskBudget {
|
|
|
34
34
|
return child ? this.tasks.forSession(child.parent_session) : null;
|
|
35
35
|
}
|
|
36
36
|
state(id) {
|
|
37
|
-
|
|
37
|
+
const runtime = this.store.db.prepare('SELECT * FROM task_runtime WHERE task_id=?').get(id);
|
|
38
|
+
const n = Number(this.store.db.prepare('SELECT count(*) AS n FROM task_invocations WHERE task_id=?').get(id)
|
|
39
|
+
.n);
|
|
40
|
+
return { ...runtime, invocations_spent: n, external_model_usage: n ? 'unknown' : 'none' };
|
|
38
41
|
}
|
|
39
42
|
active(task) {
|
|
40
43
|
const row = this.store.db
|
|
41
44
|
.prepare(`WITH tree(id) AS (SELECT ? UNION ALL SELECT child_session FROM child_tasks WHERE parent_session=?) SELECT
|
|
42
45
|
(SELECT count(*) FROM attempts WHERE session_id IN (SELECT id FROM tree) AND state IN ('accepted','dispatching','running')) +
|
|
43
|
-
(SELECT count(*) FROM command_runs WHERE session_id IN (SELECT id FROM tree) AND state NOT IN ('succeeded','failed','cancelled','timed_out','output_limited','interrupted'))
|
|
44
|
-
|
|
46
|
+
(SELECT count(*) FROM command_runs WHERE session_id IN (SELECT id FROM tree) AND state NOT IN ('succeeded','failed','cancelled','timed_out','output_limited','interrupted')) +
|
|
47
|
+
(SELECT count(*) FROM external_runs e JOIN child_tasks c ON c.id=e.child_id JOIN attempts a ON a.id=c.child_attempt WHERE c.parent_session=? AND e.stopped=0 AND a.state NOT IN ('accepted','dispatching','running')) AS n`)
|
|
48
|
+
.get(task.session_id, task.session_id, task.session_id);
|
|
45
49
|
return Number(row.n);
|
|
46
50
|
}
|
|
47
51
|
deny(session, message) {
|
|
@@ -74,8 +78,9 @@ export class TaskBudget {
|
|
|
74
78
|
this.check(session);
|
|
75
79
|
if (!attempt)
|
|
76
80
|
this.deny(task.session_id, 'Task model work must belong to a durable attempt');
|
|
77
|
-
const l = task.limits, r = this.state(task.id), charge = l.input_token_bound + l.output_token_bound;
|
|
78
|
-
if (input > l.input_token_bound ||
|
|
81
|
+
const l = task.limits, r = this.state(task.id), charge = l.token_units === null ? 0 : l.input_token_bound + l.output_token_bound;
|
|
82
|
+
if (input > l.input_token_bound ||
|
|
83
|
+
(l.output_token_bound !== null && output > l.output_token_bound))
|
|
79
84
|
this.deny(task.session_id, 'Provider payload exceeds declared per-request token bounds');
|
|
80
85
|
const reservation = this.store.db
|
|
81
86
|
.prepare('SELECT * FROM task_reservations WHERE attempt_id=? AND task_id=?')
|
|
@@ -93,7 +98,8 @@ export class TaskBudget {
|
|
|
93
98
|
else {
|
|
94
99
|
if (Number(r.requests_spent) + Number(r.reserved_requests) >= l.requests)
|
|
95
100
|
this.deny(task.session_id, 'Task request limit reached');
|
|
96
|
-
if (
|
|
101
|
+
if (l.token_units !== null &&
|
|
102
|
+
Number(r.token_units) + Number(r.reserved_tokens) + charge > l.token_units)
|
|
97
103
|
this.deny(task.session_id, 'Task declared token budget reached');
|
|
98
104
|
this.store.db
|
|
99
105
|
.prepare('UPDATE task_runtime SET requests_spent=requests_spent+1,token_units=token_units+? WHERE task_id=?')
|
|
@@ -105,9 +111,12 @@ export class TaskBudget {
|
|
|
105
111
|
if (!task)
|
|
106
112
|
return;
|
|
107
113
|
this.check(session);
|
|
108
|
-
const r = this.state(task.id), tokens =
|
|
114
|
+
const r = this.state(task.id), tokens = task.limits.token_units === null
|
|
115
|
+
? 0
|
|
116
|
+
: count * (task.limits.input_token_bound + task.limits.output_token_bound);
|
|
109
117
|
if (Number(r.requests_spent) + Number(r.reserved_requests) + count > task.limits.requests ||
|
|
110
|
-
|
|
118
|
+
(task.limits.token_units !== null &&
|
|
119
|
+
Number(r.token_units) + Number(r.reserved_tokens) + tokens > task.limits.token_units))
|
|
111
120
|
this.deny(task.session_id, 'Task cannot reserve the child request/token bounds');
|
|
112
121
|
this.store.db
|
|
113
122
|
.prepare('INSERT INTO task_reservations VALUES (?,?,?,?)')
|
package/dist/src/task-context.js
CHANGED
|
@@ -36,6 +36,20 @@ export class TaskContext {
|
|
|
36
36
|
}
|
|
37
37
|
frame(id, binding) {
|
|
38
38
|
const state = this.state(id);
|
|
39
|
+
const admitted = [
|
|
40
|
+
...new Map((this.store.workerBoundary.profiles?.snapshot(id) ?? [])
|
|
41
|
+
.filter((p) => p.state === 'active')
|
|
42
|
+
.map((p) => [
|
|
43
|
+
p.profile_id,
|
|
44
|
+
{
|
|
45
|
+
profile_id: p.profile_id,
|
|
46
|
+
profile_digest: p.profile_digest,
|
|
47
|
+
tools: p.tools,
|
|
48
|
+
route: p.route,
|
|
49
|
+
max_depth: 1,
|
|
50
|
+
},
|
|
51
|
+
])).values(),
|
|
52
|
+
].sort((a, b) => a.profile_id.localeCompare(b.profile_id));
|
|
39
53
|
// The compaction cursor is a projection, not a task fact/version.
|
|
40
54
|
const frame = 'SALAAM_TASK_STATE ' +
|
|
41
55
|
canonical({
|
|
@@ -44,6 +58,7 @@ export class TaskContext {
|
|
|
44
58
|
version: state.version,
|
|
45
59
|
instructions: 'Use active facts. Superseded facts are historical. Do not repeat completed steps. Retrieve evidence with salaam_history.',
|
|
46
60
|
facts: state.facts,
|
|
61
|
+
...(admitted.length ? { admitted_agents: admitted } : {}),
|
|
47
62
|
...(this.work?.forSession(id) ? { work_task: this.work.frame(id) } : {}),
|
|
48
63
|
});
|
|
49
64
|
if (Buffer.byteLength(frame) > TASK_FRAME_BYTES)
|