@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
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
import { constants, openSync, closeSync, readFileSync, fstatSync, lstatSync, existsSync, readdirSync, } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { randomUUID } from 'node:crypto';
|
|
4
|
+
import { canonical, digest, integer, object, ServiceError, string, textDigest, } from './protocol.js';
|
|
5
|
+
import { providerRoute } from './provider-config.js';
|
|
6
|
+
import { DIAGNOSTIC_PROFILE } from './worker-boundary.js';
|
|
7
|
+
const ROLE_TOOLS = {
|
|
8
|
+
explorer: ['salaam_probe', 'salaam_repository'],
|
|
9
|
+
reviewer: ['salaam_probe', 'salaam_repository', 'salaam_findings'],
|
|
10
|
+
builder: ['salaam_probe', 'salaam_repository', 'salaam_files', 'salaam_command'],
|
|
11
|
+
tester: ['salaam_probe', 'salaam_repository', 'salaam_command', 'salaam_findings'],
|
|
12
|
+
integrator: ['salaam_probe', 'salaam_repository', 'salaam_findings'],
|
|
13
|
+
};
|
|
14
|
+
export function profileTools(role) {
|
|
15
|
+
return [...ROLE_TOOLS[role]].sort();
|
|
16
|
+
}
|
|
17
|
+
export const BUILTIN_PROFILES = [
|
|
18
|
+
DIAGNOSTIC_PROFILE,
|
|
19
|
+
...['explorer', 'reviewer', 'builder', 'tester', 'integrator'].map((role) => ({
|
|
20
|
+
schema_version: 1,
|
|
21
|
+
id: 'salaam/' + role,
|
|
22
|
+
revision: 1,
|
|
23
|
+
role,
|
|
24
|
+
tools: profileTools(role),
|
|
25
|
+
max_depth: 1,
|
|
26
|
+
max_requests: 8,
|
|
27
|
+
description: role === 'explorer'
|
|
28
|
+
? 'Bounded evidence from an immutable repository snapshot'
|
|
29
|
+
: role === 'reviewer'
|
|
30
|
+
? 'Independent findings over an immutable repository diff'
|
|
31
|
+
: role === 'builder'
|
|
32
|
+
? 'Implement a proposal in a private owned workspace'
|
|
33
|
+
: role === 'tester'
|
|
34
|
+
? 'Run bounded checks in an isolated disposable copy'
|
|
35
|
+
: 'Review integration evidence without applying changes',
|
|
36
|
+
prompt: role === 'explorer'
|
|
37
|
+
? 'Investigate only the supplied repository snapshot. Cite actual file paths, line numbers and hashes. Do not claim edits or execution.'
|
|
38
|
+
: role === 'builder'
|
|
39
|
+
? 'Implement only the assigned change in your private owned workspace using salaam_files. Run bounded checks through salaam_command. Command edits are disposable. Do not claim the root workspace was changed or verified; return a proposal for explicit integration.'
|
|
40
|
+
: role === 'tester'
|
|
41
|
+
? 'Inspect the admitted snapshot and run bounded checks using salaam_command. You cannot edit persistent files. Report actual outcomes; a model answer is not a root verification receipt.'
|
|
42
|
+
: 'Review only the supplied immutable diff and repository snapshot. Report concrete findings with file/line evidence through salaam_findings. A finding is analysis, not a verification receipt.',
|
|
43
|
+
})),
|
|
44
|
+
];
|
|
45
|
+
/** JSON front matter is a deliberately bounded subset, not an executable config format. */
|
|
46
|
+
export function parseAgentProfile(name, source) {
|
|
47
|
+
if (!/^[a-z][a-z0-9-]{0,63}$/.test(name) || Buffer.byteLength(source) > 49152)
|
|
48
|
+
throw new ServiceError('invalid_profile', 'Invalid profile name or size');
|
|
49
|
+
const match = /^---\r?\n([\s\S]*?)\r?\n---\r?\n([\s\S]*)$/.exec(source);
|
|
50
|
+
if (!match)
|
|
51
|
+
throw new ServiceError('invalid_profile', 'Use JSON front matter between --- lines');
|
|
52
|
+
let metadata;
|
|
53
|
+
try {
|
|
54
|
+
metadata = object(JSON.parse(match[1]));
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
throw new ServiceError('invalid_profile', 'Invalid JSON profile metadata');
|
|
58
|
+
}
|
|
59
|
+
const allowed = [
|
|
60
|
+
'description',
|
|
61
|
+
'revision',
|
|
62
|
+
'role',
|
|
63
|
+
'tools',
|
|
64
|
+
'provider',
|
|
65
|
+
'model',
|
|
66
|
+
'max_requests',
|
|
67
|
+
'max_depth',
|
|
68
|
+
];
|
|
69
|
+
if (Object.keys(metadata).some((key) => !allowed.includes(key)))
|
|
70
|
+
throw new ServiceError('invalid_profile', 'Profiles cannot define executables, credentials, plugins, environment or additional fields');
|
|
71
|
+
const role = string(metadata.role, 'role', 32);
|
|
72
|
+
if (!Object.hasOwn(ROLE_TOOLS, role))
|
|
73
|
+
throw new ServiceError('invalid_profile', 'Unsupported specialist role');
|
|
74
|
+
const tools = metadata.tools === undefined
|
|
75
|
+
? profileTools(role)
|
|
76
|
+
: [
|
|
77
|
+
...new Set((Array.isArray(metadata.tools) ? metadata.tools : []).map((x) => string(x, 'tool', 128))),
|
|
78
|
+
].sort();
|
|
79
|
+
if (!tools.length || tools.some((x) => !profileTools(role).includes(x)))
|
|
80
|
+
throw new ServiceError('invalid_profile', 'Tools exceed the specialist role');
|
|
81
|
+
const revision = integer(metadata.revision ?? 1, 'revision');
|
|
82
|
+
const maxRequests = integer(metadata.max_requests ?? 8, 'max_requests');
|
|
83
|
+
if (revision < 1 || maxRequests < 1 || maxRequests > 8 || (metadata.max_depth ?? 1) !== 1)
|
|
84
|
+
throw new ServiceError('invalid_profile', 'Invalid revision, request limit or delegation depth');
|
|
85
|
+
if ((metadata.provider === undefined) !== (metadata.model === undefined))
|
|
86
|
+
throw new ServiceError('invalid_profile', 'Select both provider and model');
|
|
87
|
+
const prompt = string(match[2].trim(), 'prompt', 32768);
|
|
88
|
+
return {
|
|
89
|
+
schema_version: 1,
|
|
90
|
+
id: 'project/' + name,
|
|
91
|
+
revision,
|
|
92
|
+
role,
|
|
93
|
+
tools,
|
|
94
|
+
max_depth: 1,
|
|
95
|
+
description: string(metadata.description ?? name, 'description', 512),
|
|
96
|
+
prompt,
|
|
97
|
+
max_requests: maxRequests,
|
|
98
|
+
source_digest: textDigest(source),
|
|
99
|
+
...(metadata.provider
|
|
100
|
+
? {
|
|
101
|
+
provider: string(metadata.provider, 'provider', 128),
|
|
102
|
+
model: string(metadata.model, 'model', 256),
|
|
103
|
+
}
|
|
104
|
+
: {}),
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
export class AgentProfiles {
|
|
108
|
+
store;
|
|
109
|
+
route;
|
|
110
|
+
prepareRoute;
|
|
111
|
+
assertExecution;
|
|
112
|
+
validateFrame;
|
|
113
|
+
constructor(store, route, prepareRoute) {
|
|
114
|
+
this.store = store;
|
|
115
|
+
this.route = route;
|
|
116
|
+
this.prepareRoute = prepareRoute;
|
|
117
|
+
}
|
|
118
|
+
list(session) {
|
|
119
|
+
const root = this.store.session(session).cwd;
|
|
120
|
+
const control = join(root, '.salaam'), directory = join(control, 'agents');
|
|
121
|
+
for (const path of [control, directory]) {
|
|
122
|
+
if (!existsSync(path))
|
|
123
|
+
return structuredClone(BUILTIN_PROFILES);
|
|
124
|
+
const stat = lstatSync(path);
|
|
125
|
+
if (!stat.isDirectory() || stat.isSymbolicLink())
|
|
126
|
+
throw new ServiceError('invalid_profile', 'Profile directories must not be symlinks');
|
|
127
|
+
}
|
|
128
|
+
const names = readdirSync(directory)
|
|
129
|
+
.filter((name) => name.endsWith('.md'))
|
|
130
|
+
.sort();
|
|
131
|
+
if (names.length > 32)
|
|
132
|
+
throw new ServiceError('invalid_profile', 'At most 32 project profiles');
|
|
133
|
+
const profiles = names.map((file) => {
|
|
134
|
+
const path = join(directory, file), fd = openSync(path, constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
135
|
+
try {
|
|
136
|
+
const stat = fstatSync(fd);
|
|
137
|
+
if (!stat.isFile() || stat.nlink !== 1 || stat.size > 49152)
|
|
138
|
+
throw new ServiceError('invalid_profile', 'Profile must be a bounded regular file');
|
|
139
|
+
return parseAgentProfile(file.slice(0, -3), readFileSync(fd, 'utf8'));
|
|
140
|
+
}
|
|
141
|
+
finally {
|
|
142
|
+
closeSync(fd);
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
return [...structuredClone(BUILTIN_PROFILES), ...profiles];
|
|
146
|
+
}
|
|
147
|
+
inspect(session, id) {
|
|
148
|
+
this.store.session(session);
|
|
149
|
+
const builtin = BUILTIN_PROFILES.find((profile) => profile.id === id);
|
|
150
|
+
if (builtin)
|
|
151
|
+
return structuredClone(builtin);
|
|
152
|
+
const profile = this.list(session).find((x) => x.id === id);
|
|
153
|
+
if (!profile)
|
|
154
|
+
throw new ServiceError('not_found', 'Agent profile not found', 404);
|
|
155
|
+
return profile;
|
|
156
|
+
}
|
|
157
|
+
async admit(session, request, profileId, expectedDigest, factIds, controller) {
|
|
158
|
+
const input = { kind: 'agent-admit', session, profileId, expectedDigest, factIds };
|
|
159
|
+
const saved = this.store.cachedRequest('local-operator', request, input);
|
|
160
|
+
if (saved)
|
|
161
|
+
return saved;
|
|
162
|
+
this.store.authorizeControl(session, controller);
|
|
163
|
+
const profile = this.inspect(session, profileId);
|
|
164
|
+
if (digest(profile) !== expectedDigest)
|
|
165
|
+
throw new ServiceError('profile_changed', 'Review the current profile digest');
|
|
166
|
+
const writing = profile.tools.some((tool) => ['salaam_files', 'salaam_command'].includes(tool));
|
|
167
|
+
if (writing) {
|
|
168
|
+
if (!this.assertExecution)
|
|
169
|
+
throw new ServiceError('profile_unavailable', 'Writing roles require the parallel execution boundary');
|
|
170
|
+
this.assertExecution();
|
|
171
|
+
}
|
|
172
|
+
const prepared = await this.prepareRoute(profile.provider
|
|
173
|
+
? providerRoute(profile.provider, profile.model, Math.min(2048, this.route.max_tokens))
|
|
174
|
+
: this.route, session);
|
|
175
|
+
return this.store.request('local-operator', request, input, () => {
|
|
176
|
+
this.store.authorizeControl(session, controller);
|
|
177
|
+
if (writing)
|
|
178
|
+
this.assertExecution();
|
|
179
|
+
if (digest(this.inspect(session, profileId)) !== expectedDigest)
|
|
180
|
+
throw new ServiceError('profile_changed', 'Profile changed during admission');
|
|
181
|
+
if (profile.role &&
|
|
182
|
+
!this.store.db.prepare('SELECT id FROM workspaces WHERE session_id=?').get(session))
|
|
183
|
+
throw new ServiceError('workspace_unavailable', 'Specialists need an admitted coding workspace');
|
|
184
|
+
if (factIds.length > 16 || new Set(factIds).size !== factIds.length)
|
|
185
|
+
throw new ServiceError('invalid_profile', 'Select at most 16 distinct facts');
|
|
186
|
+
const facts = factIds.map((id) => {
|
|
187
|
+
const fact = this.store.db
|
|
188
|
+
.prepare('SELECT id,kind,text FROM task_facts WHERE id=? AND session_id=? AND id NOT IN (SELECT supersedes FROM task_facts WHERE supersedes IS NOT NULL)')
|
|
189
|
+
.get(id, session);
|
|
190
|
+
if (!fact)
|
|
191
|
+
throw new ServiceError('not_found', 'Selected fact is unavailable', 404);
|
|
192
|
+
return { id: String(fact.id), kind: String(fact.kind), text: String(fact.text) };
|
|
193
|
+
});
|
|
194
|
+
const grant = {
|
|
195
|
+
schema_version: 1,
|
|
196
|
+
id: randomUUID(),
|
|
197
|
+
owner_session: session,
|
|
198
|
+
profile,
|
|
199
|
+
profile_digest: expectedDigest,
|
|
200
|
+
route: prepared.route,
|
|
201
|
+
output_cap: prepared.output_cap,
|
|
202
|
+
facts,
|
|
203
|
+
};
|
|
204
|
+
const priorProfile = this.store.db
|
|
205
|
+
.prepare('SELECT digest FROM worker_profiles WHERE id=? AND revision=?')
|
|
206
|
+
.get(profile.id, profile.revision);
|
|
207
|
+
if (priorProfile && priorProfile.digest !== expectedDigest)
|
|
208
|
+
throw new ServiceError('profile_revision_conflict', 'Changed profile contents require a new revision');
|
|
209
|
+
this.store.db
|
|
210
|
+
.prepare('INSERT INTO agent_grants(id,owner_session,profile_id,document,digest,state) VALUES (?,?,?,?,?,?)')
|
|
211
|
+
.run(grant.id, session, profileId, canonical(grant), digest(grant), 'active');
|
|
212
|
+
this.store.db
|
|
213
|
+
.prepare('INSERT OR IGNORE INTO worker_profiles(id,revision,digest,document) VALUES (?,?,?,?)')
|
|
214
|
+
.run(profile.id, profile.revision, expectedDigest, canonical(profile));
|
|
215
|
+
this.store.event(session, 'agent.admitted', {
|
|
216
|
+
grant_id: grant.id,
|
|
217
|
+
profile_id: profileId,
|
|
218
|
+
profile_digest: expectedDigest,
|
|
219
|
+
route: prepared.route,
|
|
220
|
+
});
|
|
221
|
+
this.validateFrame?.(session);
|
|
222
|
+
return grant;
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
resolve(session, id) {
|
|
226
|
+
const row = this.store.db
|
|
227
|
+
.prepare("SELECT document,digest FROM agent_grants WHERE owner_session=? AND profile_id=? AND state='active' ORDER BY rowid DESC LIMIT 1")
|
|
228
|
+
.get(session, id);
|
|
229
|
+
if (!row)
|
|
230
|
+
throw new ServiceError('authority_denied', 'Operator must admit this agent profile first');
|
|
231
|
+
const grant = JSON.parse(String(row.document));
|
|
232
|
+
if (digest(grant) !== row.digest || digest(this.inspect(session, id)) !== grant.profile_digest)
|
|
233
|
+
throw new ServiceError('profile_changed', 'Profile changed; renew its admission');
|
|
234
|
+
return grant;
|
|
235
|
+
}
|
|
236
|
+
validate(grant) {
|
|
237
|
+
const row = this.store.db
|
|
238
|
+
.prepare('SELECT document,digest,state FROM agent_grants WHERE id=? AND owner_session=?')
|
|
239
|
+
.get(grant.id, grant.owner_session);
|
|
240
|
+
if (!row ||
|
|
241
|
+
row.state !== 'active' ||
|
|
242
|
+
row.digest !== digest(grant) ||
|
|
243
|
+
row.document !== canonical(grant) ||
|
|
244
|
+
digest(this.inspect(grant.owner_session, grant.profile.id)) !== grant.profile_digest)
|
|
245
|
+
throw new ServiceError('worker_profile_revoked', 'Specialist admission was revoked or changed');
|
|
246
|
+
}
|
|
247
|
+
revoke(session, grantId, request, controller) {
|
|
248
|
+
return this.store.request('local-operator', request, { kind: 'agent-revoke', session, grantId }, () => {
|
|
249
|
+
this.store.authorizeControl(session, controller);
|
|
250
|
+
const result = this.store.db
|
|
251
|
+
.prepare("UPDATE agent_grants SET state='revoked' WHERE id=? AND owner_session=?")
|
|
252
|
+
.run(grantId, session);
|
|
253
|
+
if (!result.changes)
|
|
254
|
+
throw new ServiceError('not_found', 'Agent admission not found', 404);
|
|
255
|
+
this.store.event(session, 'agent.revoked', { grant_id: grantId });
|
|
256
|
+
return { grant_id: grantId, state: 'revoked' };
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
snapshot(session) {
|
|
260
|
+
return this.store.db
|
|
261
|
+
.prepare('SELECT document,state FROM agent_grants WHERE owner_session=? ORDER BY rowid DESC LIMIT 64')
|
|
262
|
+
.all(session)
|
|
263
|
+
.reverse()
|
|
264
|
+
.map((row) => {
|
|
265
|
+
const grant = JSON.parse(String(row.document));
|
|
266
|
+
let state = String(row.state);
|
|
267
|
+
if (state === 'active') {
|
|
268
|
+
try {
|
|
269
|
+
this.validate(grant);
|
|
270
|
+
}
|
|
271
|
+
catch {
|
|
272
|
+
state = 'stale';
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
return {
|
|
276
|
+
grant_id: grant.id,
|
|
277
|
+
profile_id: grant.profile.id,
|
|
278
|
+
revision: grant.profile.revision,
|
|
279
|
+
profile_digest: grant.profile_digest,
|
|
280
|
+
tools: grant.profile.tools,
|
|
281
|
+
role: grant.profile.role,
|
|
282
|
+
route: grant.route,
|
|
283
|
+
output_cap: grant.output_cap,
|
|
284
|
+
selected_facts: grant.facts.map((f) => f.id),
|
|
285
|
+
state,
|
|
286
|
+
};
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
}
|
package/dist/src/children.js
CHANGED
|
@@ -4,12 +4,16 @@ export class ChildWork {
|
|
|
4
4
|
store;
|
|
5
5
|
artifacts;
|
|
6
6
|
supervisor;
|
|
7
|
+
repositories;
|
|
8
|
+
external;
|
|
9
|
+
workspaces;
|
|
7
10
|
paused = false;
|
|
8
11
|
starts = new Set();
|
|
9
|
-
constructor(store, artifacts, supervisor) {
|
|
12
|
+
constructor(store, artifacts, supervisor, repositories) {
|
|
10
13
|
this.store = store;
|
|
11
14
|
this.artifacts = artifacts;
|
|
12
15
|
this.supervisor = supervisor;
|
|
16
|
+
this.repositories = repositories;
|
|
13
17
|
store.finishChild = (attempt) => this.finish(attempt);
|
|
14
18
|
store.cancelChildren = (attempt) => this.cancelIntents(attempt);
|
|
15
19
|
}
|
|
@@ -21,7 +25,7 @@ export class ChildWork {
|
|
|
21
25
|
list(parent) {
|
|
22
26
|
return this.store.db
|
|
23
27
|
.prepare('SELECT * FROM child_tasks WHERE parent_session=? ORDER BY rowid')
|
|
24
|
-
.all(parent);
|
|
28
|
+
.all(parent).map((child) => ({ ...child, worker: this.store.workerBoundary.view(child.id) }));
|
|
25
29
|
}
|
|
26
30
|
inspect(parent, id) {
|
|
27
31
|
const row = this.store.db
|
|
@@ -29,9 +33,9 @@ export class ChildWork {
|
|
|
29
33
|
.get(id, parent);
|
|
30
34
|
if (!row)
|
|
31
35
|
throw new ServiceError('not_found', 'Child task not found', 404);
|
|
32
|
-
return row;
|
|
36
|
+
return { ...row, worker: this.store.workerBoundary.view(row.id) };
|
|
33
37
|
}
|
|
34
|
-
spawn(parent, epoch, attempt, step, prompt, budget, requested) {
|
|
38
|
+
spawn(parent, epoch, attempt, step, prompt, budget, requested, profileId, admission) {
|
|
35
39
|
if (!Number.isSafeInteger(budget) || budget < 1 || budget > 8)
|
|
36
40
|
throw new ServiceError('invalid_budget', 'Child request budget must be 1–8');
|
|
37
41
|
if (step.length > 128 ||
|
|
@@ -41,12 +45,18 @@ export class ChildWork {
|
|
|
41
45
|
prompt.trimStart().startsWith('/'))
|
|
42
46
|
throw new ServiceError('invalid_child', 'Invalid child step or prompt');
|
|
43
47
|
// Current child authority is deliberately read-only and closed-world.
|
|
44
|
-
const
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
.sort()
|
|
48
|
-
|
|
49
|
-
|
|
48
|
+
const input = digest({
|
|
49
|
+
prompt,
|
|
50
|
+
budget,
|
|
51
|
+
requested: [...new Set(requested)].sort(),
|
|
52
|
+
...(profileId ? { profile: profileId } : {}),
|
|
53
|
+
...(admission ? { assignment: admission.assignment } : {}),
|
|
54
|
+
...(admission?.runtime ? { runtime: admission.runtime } : {}),
|
|
55
|
+
});
|
|
56
|
+
let fresh = false;
|
|
57
|
+
if (this.store.db.isTransaction && !admission?.defer)
|
|
58
|
+
throw new Error('Nested child dispatch requires an owned deferred admission');
|
|
59
|
+
const prepare = () => {
|
|
50
60
|
const old = this.store.db
|
|
51
61
|
.prepare('SELECT * FROM child_tasks WHERE parent_session=? AND step_key=?')
|
|
52
62
|
.get(parent, step);
|
|
@@ -57,34 +67,83 @@ export class ChildWork {
|
|
|
57
67
|
}
|
|
58
68
|
if (this.paused)
|
|
59
69
|
throw new ServiceError('stopping', 'Child dispatch is stopping');
|
|
60
|
-
|
|
70
|
+
if (admission)
|
|
71
|
+
admission.authorize();
|
|
72
|
+
else
|
|
73
|
+
this.supervisor.assertActive(parent, epoch, attempt);
|
|
74
|
+
const agent = profileId
|
|
75
|
+
? this.store.workerBoundary.profiles?.resolve(parent, profileId)
|
|
76
|
+
: undefined;
|
|
77
|
+
if (profileId && !agent)
|
|
78
|
+
throw new ServiceError('authority_denied', 'Agent profile is unavailable');
|
|
79
|
+
if (!admission?.runtime)
|
|
80
|
+
this.supervisor.assertSessionProviderSupported(parent, agent?.output_cap);
|
|
81
|
+
if (!admission?.runtime && agent && budget > (agent.profile.max_requests ?? 8))
|
|
82
|
+
throw new ServiceError('invalid_budget', 'Budget exceeds the admitted profile limit');
|
|
83
|
+
const available = [
|
|
84
|
+
...(agent?.profile.tools ?? ['salaam_probe']),
|
|
85
|
+
...(admission && !admission.runtime ? ['salaam_team'] : []),
|
|
86
|
+
];
|
|
87
|
+
const authority = [
|
|
88
|
+
...new Set(agent && !requested.length ? available : requested.filter((x) => available.includes(x))),
|
|
89
|
+
].sort();
|
|
90
|
+
const effectivePrompt = agent
|
|
91
|
+
? `${agent.profile.prompt ?? 'Run the diagnostic probe.'}${admission?.runtime ? '\n\nRuntime binding: use the admitted external harness tools for the corresponding file and command actions. Its private draft is /workspace. Return evidence for explicit root integration; this invocation cannot verify the root task.' : ''}\n\nGoal:\n${prompt}\n\nExplicitly selected facts:\n${JSON.stringify(agent.facts)}`
|
|
92
|
+
: prompt;
|
|
93
|
+
if (Buffer.byteLength(effectivePrompt) > 65536)
|
|
94
|
+
throw new ServiceError('invalid_child', 'Specialist context exceeds 64 KiB');
|
|
61
95
|
if (this.owned(parent))
|
|
62
96
|
throw new ServiceError('depth_exceeded', 'Child depth is limited to one');
|
|
63
97
|
if (!authority.length)
|
|
64
98
|
throw new ServiceError('authority_denied', 'No requested read-only authority is available');
|
|
65
|
-
if (this.
|
|
99
|
+
if (this.activeCount(parent) >= 2)
|
|
66
100
|
throw new ServiceError('child_limit', 'At most two children may be active');
|
|
67
|
-
this.
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
.
|
|
71
|
-
|
|
72
|
-
|
|
101
|
+
if (this.activeCount() >= 8)
|
|
102
|
+
throw new ServiceError('child_limit', 'Daemon worker admission limit reached');
|
|
103
|
+
if (!admission?.runtime) {
|
|
104
|
+
this.store.usage.budget(attempt);
|
|
105
|
+
const changed = this.store.db
|
|
106
|
+
.prepare('UPDATE attempt_budgets SET reserved_requests=reserved_requests+? WHERE attempt_id=? AND spent_requests+reserved_requests+?<=limit_requests')
|
|
107
|
+
.run(budget, attempt, budget);
|
|
108
|
+
if (!changed.changes)
|
|
109
|
+
throw new ServiceError('budget_exhausted', 'Parent cannot reserve this child budget');
|
|
110
|
+
}
|
|
73
111
|
const id = randomUUID(), session = randomUUID(), submission = randomUUID(), childAttempt = randomUUID();
|
|
74
112
|
this.store.db
|
|
75
113
|
.prepare('INSERT INTO sessions(id,cwd,pi_session_id) VALUES (?,?,?)')
|
|
76
114
|
.run(session, this.store.session(parent).cwd, session);
|
|
77
115
|
this.store.db
|
|
78
116
|
.prepare('INSERT INTO submissions(id,session_id,text,attempt_id) VALUES (?,?,?,?)')
|
|
79
|
-
.run(submission, session,
|
|
117
|
+
.run(submission, session, effectivePrompt, childAttempt);
|
|
80
118
|
this.store.db
|
|
81
119
|
.prepare("INSERT INTO attempts(id,session_id,submission_id,state) VALUES (?,?,?,'accepted')")
|
|
82
120
|
.run(childAttempt, session, submission);
|
|
83
121
|
this.store.db
|
|
84
122
|
.prepare("INSERT INTO child_tasks(id,parent_session,parent_attempt,step_key,input_digest,child_session,child_attempt,authority,budget,state) VALUES (?,?,?,?,?,?,?,?,?,'accepted')")
|
|
85
123
|
.run(id, parent, attempt, step, input, session, childAttempt, JSON.stringify(authority), budget);
|
|
86
|
-
|
|
87
|
-
|
|
124
|
+
if (!admission?.runtime) {
|
|
125
|
+
this.store.taskChildReserve?.(parent, childAttempt, budget);
|
|
126
|
+
this.store.usage.budget(childAttempt, budget);
|
|
127
|
+
}
|
|
128
|
+
const artifact = this.artifacts.prepare(session, effectivePrompt);
|
|
129
|
+
artifact.commit();
|
|
130
|
+
const repository = agent?.profile.role
|
|
131
|
+
? this.repositories?.capture(parent, session)
|
|
132
|
+
: undefined;
|
|
133
|
+
if (agent?.profile.role && !repository)
|
|
134
|
+
throw new ServiceError('workspace_unavailable', 'No specialist snapshot boundary');
|
|
135
|
+
const writing = authority.some((tool) => ['salaam_files', 'salaam_command'].includes(tool));
|
|
136
|
+
const isolation = (writing || admission?.runtime) && repository
|
|
137
|
+
? this.workspaces?.prepare(session, repository)
|
|
138
|
+
: undefined;
|
|
139
|
+
if (writing && !isolation)
|
|
140
|
+
throw new ServiceError('authority_denied', 'No isolated coding boundary');
|
|
141
|
+
this.store.workerBoundary.freeze(this.owned(session), artifact.id, false, agent, repository, admission?.assignment, isolation, admission?.runtime);
|
|
142
|
+
if (admission?.runtime) {
|
|
143
|
+
if (!this.external)
|
|
144
|
+
throw new ServiceError('runtime_unavailable', 'External runtime is unavailable');
|
|
145
|
+
this.external.prepare(this.owned(session));
|
|
146
|
+
}
|
|
88
147
|
this.store.event(session, 'session.created', { parent_session: parent, child_id: id });
|
|
89
148
|
this.store.event(session, 'submission.accepted', {
|
|
90
149
|
attempt_id: childAttempt,
|
|
@@ -97,17 +156,37 @@ export class ChildWork {
|
|
|
97
156
|
budget,
|
|
98
157
|
authority,
|
|
99
158
|
});
|
|
159
|
+
fresh = true;
|
|
100
160
|
return this.inspect(parent, id);
|
|
101
|
-
}
|
|
161
|
+
};
|
|
162
|
+
const child = this.store.db.isTransaction ? prepare() : this.store.transaction(prepare);
|
|
102
163
|
// Repeated calls only return the durable binding. They never restart a child.
|
|
103
|
-
if (
|
|
164
|
+
if (fresh &&
|
|
165
|
+
!admission?.defer &&
|
|
166
|
+
this.store.session(child.child_session).epoch === 0 &&
|
|
104
167
|
this.store.attempt(child.child_attempt).state === 'accepted')
|
|
105
168
|
this.start(child);
|
|
106
169
|
return child;
|
|
107
170
|
}
|
|
171
|
+
activeCount(parent) {
|
|
172
|
+
return Number(this.store.db
|
|
173
|
+
.prepare(`SELECT count(*) AS n FROM child_tasks c WHERE (? IS NULL OR c.parent_session=?) AND (c.state='accepted' OR EXISTS(SELECT 1 FROM workers w WHERE w.session_id=c.child_session AND w.state!='exited') OR EXISTS(SELECT 1 FROM external_runs e WHERE e.child_id=c.id AND e.stopped=0))`)
|
|
174
|
+
.get(parent ?? null, parent ?? null).n);
|
|
175
|
+
}
|
|
176
|
+
launchPrepared(child) {
|
|
177
|
+
if (this.store.db.isTransaction)
|
|
178
|
+
throw new Error('Commit child intent before launch');
|
|
179
|
+
if (this.store.workerBoundary.view(child.id).dispatch_state === 'intent')
|
|
180
|
+
this.start(child);
|
|
181
|
+
}
|
|
182
|
+
cancelSessionInTransaction(parent) {
|
|
183
|
+
const child = this.list(parent)[0];
|
|
184
|
+
if (child)
|
|
185
|
+
this.cancelIntents(child.parent_attempt);
|
|
186
|
+
}
|
|
108
187
|
start(child) {
|
|
109
|
-
const
|
|
110
|
-
|
|
188
|
+
const external = this.store.workerBoundary.view(child.id).binding.external;
|
|
189
|
+
const task = (external ? this.external.start(child) : this.supervisor.start(child.child_session))
|
|
111
190
|
.catch((error) => {
|
|
112
191
|
this.store.transaction(() => {
|
|
113
192
|
const attempt = this.store.attempt(child.child_attempt);
|
|
@@ -119,6 +198,7 @@ export class ChildWork {
|
|
|
119
198
|
this.store.db
|
|
120
199
|
.prepare("UPDATE submissions SET state='interrupted' WHERE id=?")
|
|
121
200
|
.run(attempt.submission_id);
|
|
201
|
+
this.store.projectTaskAttempt?.(this.store.attempt(attempt.id));
|
|
122
202
|
this.finish(this.store.attempt(attempt.id));
|
|
123
203
|
});
|
|
124
204
|
})
|
|
@@ -143,6 +223,7 @@ export class ChildWork {
|
|
|
143
223
|
this.store.db
|
|
144
224
|
.prepare('UPDATE submissions SET state=? WHERE id=?')
|
|
145
225
|
.run(state, attempt.submission_id);
|
|
226
|
+
this.store.projectTaskAttempt?.(this.store.attempt(attempt.id));
|
|
146
227
|
this.finish(this.store.attempt(attempt.id));
|
|
147
228
|
});
|
|
148
229
|
}
|
|
@@ -159,12 +240,22 @@ export class ChildWork {
|
|
|
159
240
|
state: attempt.state,
|
|
160
241
|
text: attempt.result,
|
|
161
242
|
error: attempt.error,
|
|
243
|
+
...(this.store.workerBoundary.view(child.id).binding.agent
|
|
244
|
+
? {
|
|
245
|
+
worker_id: this.store.workerBoundary.view(child.id).binding.worker_id,
|
|
246
|
+
report: this.store.db
|
|
247
|
+
.prepare('SELECT artifact_id FROM specialist_reports WHERE worker_id=?')
|
|
248
|
+
.get(this.store.workerBoundary.view(child.id).binding.worker_id)?.artifact_id ??
|
|
249
|
+
null,
|
|
250
|
+
}
|
|
251
|
+
: {}),
|
|
162
252
|
}));
|
|
163
253
|
artifact.commit();
|
|
164
254
|
const result = randomUUID();
|
|
165
255
|
this.store.db
|
|
166
256
|
.prepare('UPDATE child_tasks SET state=?,result_id=?,artifact_id=? WHERE id=?')
|
|
167
257
|
.run(attempt.state, result, artifact.id, child.id);
|
|
258
|
+
this.store.workerBoundary.finish(child, attempt.state === 'interrupted');
|
|
168
259
|
this.store.event(child.parent_session, 'child.result_available', {
|
|
169
260
|
child_id: child.id,
|
|
170
261
|
result_id: result,
|
|
@@ -208,6 +299,7 @@ export class ChildWork {
|
|
|
208
299
|
this.store.db
|
|
209
300
|
.prepare("UPDATE submissions SET state='interrupted' WHERE id=?")
|
|
210
301
|
.run(attempt.submission_id);
|
|
302
|
+
this.store.projectTaskAttempt?.(this.store.attempt(attempt.id));
|
|
211
303
|
}
|
|
212
304
|
this.finish(this.store.attempt(attempt.id));
|
|
213
305
|
}
|
|
@@ -226,6 +318,7 @@ export class ChildWork {
|
|
|
226
318
|
this.store.db
|
|
227
319
|
.prepare("UPDATE submissions SET state='cancelled' WHERE id=?")
|
|
228
320
|
.run(work.submission_id);
|
|
321
|
+
this.store.projectTaskAttempt?.(this.store.attempt(work.id));
|
|
229
322
|
this.finish(this.store.attempt(work.id));
|
|
230
323
|
}
|
|
231
324
|
this.store.event(child.child_session, 'attempt.cancel_requested', {
|
|
@@ -238,9 +331,22 @@ export class ChildWork {
|
|
|
238
331
|
const children = this.store.db
|
|
239
332
|
.prepare('SELECT * FROM child_tasks WHERE parent_session=?')
|
|
240
333
|
.all(this.store.attempt(attempt).session_id);
|
|
334
|
+
for (const child of children.filter((child) => this.store.cancelled(child.child_attempt)))
|
|
335
|
+
for (const run of this.supervisor.coding?.list(child.child_session) ?? [])
|
|
336
|
+
if (![
|
|
337
|
+
'succeeded',
|
|
338
|
+
'failed',
|
|
339
|
+
'cancelled',
|
|
340
|
+
'timed_out',
|
|
341
|
+
'output_limited',
|
|
342
|
+
'interrupted',
|
|
343
|
+
].includes(run.state))
|
|
344
|
+
this.supervisor.coding.cancel(child.child_session, run.id, 'child-stop:' + run.id, () => { });
|
|
241
345
|
await Promise.all(children
|
|
242
346
|
.filter((child) => this.store.cancelled(child.child_attempt))
|
|
243
|
-
.map((child) => this.
|
|
347
|
+
.map((child) => this.store.workerBoundary.view(child.id).binding.external
|
|
348
|
+
? this.external.cancel(child)
|
|
349
|
+
: this.supervisor.cancelAttempt(child.child_attempt)));
|
|
244
350
|
}
|
|
245
351
|
pause() {
|
|
246
352
|
this.paused = true;
|