@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/src/children.ts
CHANGED
|
@@ -3,14 +3,21 @@ import type { Store } from './store.js';
|
|
|
3
3
|
import type { Artifacts } from './artifacts.js';
|
|
4
4
|
import type { Supervisor } from './supervisor.js';
|
|
5
5
|
import { digest, ServiceError, type Attempt, type ChildTask } from './protocol.js';
|
|
6
|
+
import type { RepositorySnapshots } from './repository-snapshots.js';
|
|
7
|
+
import type { WorkerWorkspaces } from './worker-workspaces.js';
|
|
8
|
+
import type { ExternalRuntime } from './external-runtime.js';
|
|
9
|
+
import type { ExternalWork } from './external-work.js';
|
|
6
10
|
|
|
7
11
|
export class ChildWork {
|
|
12
|
+
external?: ExternalWork;
|
|
13
|
+
workspaces?: WorkerWorkspaces;
|
|
8
14
|
private paused = false;
|
|
9
15
|
private starts = new Set<Promise<void>>();
|
|
10
16
|
constructor(
|
|
11
17
|
readonly store: Store,
|
|
12
18
|
private artifacts: Artifacts,
|
|
13
19
|
private supervisor: Supervisor,
|
|
20
|
+
private repositories?: RepositorySnapshots,
|
|
14
21
|
) {
|
|
15
22
|
store.finishChild = (attempt) => this.finish(attempt);
|
|
16
23
|
store.cancelChildren = (attempt) => this.cancelIntents(attempt);
|
|
@@ -21,16 +28,18 @@ export class ChildWork {
|
|
|
21
28
|
.get(session) as unknown as ChildTask | undefined;
|
|
22
29
|
}
|
|
23
30
|
list(parent: string): ChildTask[] {
|
|
24
|
-
return
|
|
25
|
-
.
|
|
26
|
-
|
|
31
|
+
return (
|
|
32
|
+
this.store.db
|
|
33
|
+
.prepare('SELECT * FROM child_tasks WHERE parent_session=? ORDER BY rowid')
|
|
34
|
+
.all(parent) as unknown as ChildTask[]
|
|
35
|
+
).map((child) => ({ ...child, worker: this.store.workerBoundary.view(child.id) }));
|
|
27
36
|
}
|
|
28
37
|
inspect(parent: string, id: string): ChildTask {
|
|
29
38
|
const row = this.store.db
|
|
30
39
|
.prepare('SELECT * FROM child_tasks WHERE id=? AND parent_session=?')
|
|
31
40
|
.get(id, parent) as unknown as ChildTask | undefined;
|
|
32
41
|
if (!row) throw new ServiceError('not_found', 'Child task not found', 404);
|
|
33
|
-
return row;
|
|
42
|
+
return { ...row, worker: this.store.workerBoundary.view(row.id) };
|
|
34
43
|
}
|
|
35
44
|
spawn(
|
|
36
45
|
parent: string,
|
|
@@ -40,6 +49,13 @@ export class ChildWork {
|
|
|
40
49
|
prompt: string,
|
|
41
50
|
budget: number,
|
|
42
51
|
requested: string[],
|
|
52
|
+
profileId?: string,
|
|
53
|
+
admission?: {
|
|
54
|
+
authorize: () => void;
|
|
55
|
+
defer: true;
|
|
56
|
+
assignment: { id: string; generation: number };
|
|
57
|
+
runtime?: ExternalRuntime;
|
|
58
|
+
},
|
|
43
59
|
): ChildTask {
|
|
44
60
|
if (!Number.isSafeInteger(budget) || budget < 1 || budget > 8)
|
|
45
61
|
throw new ServiceError('invalid_budget', 'Child request budget must be 1–8');
|
|
@@ -52,12 +68,18 @@ export class ChildWork {
|
|
|
52
68
|
)
|
|
53
69
|
throw new ServiceError('invalid_child', 'Invalid child step or prompt');
|
|
54
70
|
// Current child authority is deliberately read-only and closed-world.
|
|
55
|
-
const
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
.sort()
|
|
59
|
-
|
|
60
|
-
|
|
71
|
+
const input = digest({
|
|
72
|
+
prompt,
|
|
73
|
+
budget,
|
|
74
|
+
requested: [...new Set(requested)].sort(),
|
|
75
|
+
...(profileId ? { profile: profileId } : {}),
|
|
76
|
+
...(admission ? { assignment: admission.assignment } : {}),
|
|
77
|
+
...(admission?.runtime ? { runtime: admission.runtime } : {}),
|
|
78
|
+
});
|
|
79
|
+
let fresh = false;
|
|
80
|
+
if (this.store.db.isTransaction && !admission?.defer)
|
|
81
|
+
throw new Error('Nested child dispatch requires an owned deferred admission');
|
|
82
|
+
const prepare = () => {
|
|
61
83
|
const old = this.store.db
|
|
62
84
|
.prepare('SELECT * FROM child_tasks WHERE parent_session=? AND step_key=?')
|
|
63
85
|
.get(parent, step) as unknown as ChildTask | undefined;
|
|
@@ -67,21 +89,49 @@ export class ChildWork {
|
|
|
67
89
|
return old;
|
|
68
90
|
}
|
|
69
91
|
if (this.paused) throw new ServiceError('stopping', 'Child dispatch is stopping');
|
|
70
|
-
|
|
92
|
+
if (admission) admission.authorize();
|
|
93
|
+
else this.supervisor.assertActive(parent, epoch, attempt);
|
|
94
|
+
const agent = profileId
|
|
95
|
+
? this.store.workerBoundary.profiles?.resolve(parent, profileId)
|
|
96
|
+
: undefined;
|
|
97
|
+
if (profileId && !agent)
|
|
98
|
+
throw new ServiceError('authority_denied', 'Agent profile is unavailable');
|
|
99
|
+
if (!admission?.runtime)
|
|
100
|
+
this.supervisor.assertSessionProviderSupported(parent, agent?.output_cap);
|
|
101
|
+
if (!admission?.runtime && agent && budget > (agent.profile.max_requests ?? 8))
|
|
102
|
+
throw new ServiceError('invalid_budget', 'Budget exceeds the admitted profile limit');
|
|
103
|
+
const available = [
|
|
104
|
+
...(agent?.profile.tools ?? ['salaam_probe']),
|
|
105
|
+
...(admission && !admission.runtime ? ['salaam_team'] : []),
|
|
106
|
+
];
|
|
107
|
+
const authority = [
|
|
108
|
+
...new Set(
|
|
109
|
+
agent && !requested.length ? available : requested.filter((x) => available.includes(x)),
|
|
110
|
+
),
|
|
111
|
+
].sort();
|
|
112
|
+
const effectivePrompt = agent
|
|
113
|
+
? `${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)}`
|
|
114
|
+
: prompt;
|
|
115
|
+
if (Buffer.byteLength(effectivePrompt) > 65536)
|
|
116
|
+
throw new ServiceError('invalid_child', 'Specialist context exceeds 64 KiB');
|
|
71
117
|
if (this.owned(parent))
|
|
72
118
|
throw new ServiceError('depth_exceeded', 'Child depth is limited to one');
|
|
73
119
|
if (!authority.length)
|
|
74
120
|
throw new ServiceError('authority_denied', 'No requested read-only authority is available');
|
|
75
|
-
if (this.
|
|
121
|
+
if (this.activeCount(parent) >= 2)
|
|
76
122
|
throw new ServiceError('child_limit', 'At most two children may be active');
|
|
77
|
-
this.
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
123
|
+
if (this.activeCount() >= 8)
|
|
124
|
+
throw new ServiceError('child_limit', 'Daemon worker admission limit reached');
|
|
125
|
+
if (!admission?.runtime) {
|
|
126
|
+
this.store.usage.budget(attempt);
|
|
127
|
+
const changed = this.store.db
|
|
128
|
+
.prepare(
|
|
129
|
+
'UPDATE attempt_budgets SET reserved_requests=reserved_requests+? WHERE attempt_id=? AND spent_requests+reserved_requests+?<=limit_requests',
|
|
130
|
+
)
|
|
131
|
+
.run(budget, attempt, budget);
|
|
132
|
+
if (!changed.changes)
|
|
133
|
+
throw new ServiceError('budget_exhausted', 'Parent cannot reserve this child budget');
|
|
134
|
+
}
|
|
85
135
|
const id = randomUUID(),
|
|
86
136
|
session = randomUUID(),
|
|
87
137
|
submission = randomUUID(),
|
|
@@ -91,7 +141,7 @@ export class ChildWork {
|
|
|
91
141
|
.run(session, this.store.session(parent).cwd, session);
|
|
92
142
|
this.store.db
|
|
93
143
|
.prepare('INSERT INTO submissions(id,session_id,text,attempt_id) VALUES (?,?,?,?)')
|
|
94
|
-
.run(submission, session,
|
|
144
|
+
.run(submission, session, effectivePrompt, childAttempt);
|
|
95
145
|
this.store.db
|
|
96
146
|
.prepare(
|
|
97
147
|
"INSERT INTO attempts(id,session_id,submission_id,state) VALUES (?,?,?,'accepted')",
|
|
@@ -112,8 +162,39 @@ export class ChildWork {
|
|
|
112
162
|
JSON.stringify(authority),
|
|
113
163
|
budget,
|
|
114
164
|
);
|
|
115
|
-
|
|
116
|
-
|
|
165
|
+
if (!admission?.runtime) {
|
|
166
|
+
this.store.taskChildReserve?.(parent, childAttempt, budget);
|
|
167
|
+
this.store.usage.budget(childAttempt, budget);
|
|
168
|
+
}
|
|
169
|
+
const artifact = this.artifacts.prepare(session, effectivePrompt);
|
|
170
|
+
artifact.commit();
|
|
171
|
+
const repository = agent?.profile.role
|
|
172
|
+
? this.repositories?.capture(parent, session)
|
|
173
|
+
: undefined;
|
|
174
|
+
if (agent?.profile.role && !repository)
|
|
175
|
+
throw new ServiceError('workspace_unavailable', 'No specialist snapshot boundary');
|
|
176
|
+
const writing = authority.some((tool) => ['salaam_files', 'salaam_command'].includes(tool));
|
|
177
|
+
const isolation =
|
|
178
|
+
(writing || admission?.runtime) && repository
|
|
179
|
+
? this.workspaces?.prepare(session, repository)
|
|
180
|
+
: undefined;
|
|
181
|
+
if (writing && !isolation)
|
|
182
|
+
throw new ServiceError('authority_denied', 'No isolated coding boundary');
|
|
183
|
+
this.store.workerBoundary.freeze(
|
|
184
|
+
this.owned(session)!,
|
|
185
|
+
artifact.id,
|
|
186
|
+
false,
|
|
187
|
+
agent,
|
|
188
|
+
repository,
|
|
189
|
+
admission?.assignment,
|
|
190
|
+
isolation,
|
|
191
|
+
admission?.runtime,
|
|
192
|
+
);
|
|
193
|
+
if (admission?.runtime) {
|
|
194
|
+
if (!this.external)
|
|
195
|
+
throw new ServiceError('runtime_unavailable', 'External runtime is unavailable');
|
|
196
|
+
this.external.prepare(this.owned(session)!);
|
|
197
|
+
}
|
|
117
198
|
this.store.event(session, 'session.created', { parent_session: parent, child_id: id });
|
|
118
199
|
this.store.event(session, 'submission.accepted', {
|
|
119
200
|
attempt_id: childAttempt,
|
|
@@ -126,19 +207,42 @@ export class ChildWork {
|
|
|
126
207
|
budget,
|
|
127
208
|
authority,
|
|
128
209
|
});
|
|
210
|
+
fresh = true;
|
|
129
211
|
return this.inspect(parent, id);
|
|
130
|
-
}
|
|
212
|
+
};
|
|
213
|
+
const child = this.store.db.isTransaction ? prepare() : this.store.transaction(prepare);
|
|
131
214
|
// Repeated calls only return the durable binding. They never restart a child.
|
|
132
215
|
if (
|
|
216
|
+
fresh &&
|
|
217
|
+
!admission?.defer &&
|
|
133
218
|
this.store.session(child.child_session).epoch === 0 &&
|
|
134
219
|
this.store.attempt(child.child_attempt).state === 'accepted'
|
|
135
220
|
)
|
|
136
221
|
this.start(child);
|
|
137
222
|
return child;
|
|
138
223
|
}
|
|
224
|
+
activeCount(parent?: string): number {
|
|
225
|
+
return Number(
|
|
226
|
+
this.store.db
|
|
227
|
+
.prepare(
|
|
228
|
+
`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))`,
|
|
229
|
+
)
|
|
230
|
+
.get(parent ?? null, parent ?? null)!.n,
|
|
231
|
+
);
|
|
232
|
+
}
|
|
233
|
+
launchPrepared(child: ChildTask): void {
|
|
234
|
+
if (this.store.db.isTransaction) throw new Error('Commit child intent before launch');
|
|
235
|
+
if (this.store.workerBoundary.view(child.id).dispatch_state === 'intent') this.start(child);
|
|
236
|
+
}
|
|
237
|
+
cancelSessionInTransaction(parent: string): void {
|
|
238
|
+
const child = this.list(parent)[0];
|
|
239
|
+
if (child) this.cancelIntents(child.parent_attempt);
|
|
240
|
+
}
|
|
139
241
|
private start(child: ChildTask): void {
|
|
140
|
-
const
|
|
141
|
-
|
|
242
|
+
const external = this.store.workerBoundary.view(child.id).binding.external;
|
|
243
|
+
const task = (
|
|
244
|
+
external ? this.external!.start(child) : this.supervisor.start(child.child_session)
|
|
245
|
+
)
|
|
142
246
|
.catch((error) => {
|
|
143
247
|
this.store.transaction(() => {
|
|
144
248
|
const attempt = this.store.attempt(child.child_attempt);
|
|
@@ -149,6 +253,7 @@ export class ChildWork {
|
|
|
149
253
|
this.store.db
|
|
150
254
|
.prepare("UPDATE submissions SET state='interrupted' WHERE id=?")
|
|
151
255
|
.run(attempt.submission_id);
|
|
256
|
+
this.store.projectTaskAttempt?.(this.store.attempt(attempt.id));
|
|
152
257
|
this.finish(this.store.attempt(attempt.id));
|
|
153
258
|
});
|
|
154
259
|
})
|
|
@@ -172,6 +277,7 @@ export class ChildWork {
|
|
|
172
277
|
this.store.db
|
|
173
278
|
.prepare('UPDATE submissions SET state=? WHERE id=?')
|
|
174
279
|
.run(state, attempt.submission_id);
|
|
280
|
+
this.store.projectTaskAttempt?.(this.store.attempt(attempt.id));
|
|
175
281
|
this.finish(this.store.attempt(attempt.id));
|
|
176
282
|
});
|
|
177
283
|
}
|
|
@@ -192,6 +298,16 @@ export class ChildWork {
|
|
|
192
298
|
state: attempt.state,
|
|
193
299
|
text: attempt.result,
|
|
194
300
|
error: attempt.error,
|
|
301
|
+
...(this.store.workerBoundary.view(child.id).binding.agent
|
|
302
|
+
? {
|
|
303
|
+
worker_id: this.store.workerBoundary.view(child.id).binding.worker_id,
|
|
304
|
+
report:
|
|
305
|
+
this.store.db
|
|
306
|
+
.prepare('SELECT artifact_id FROM specialist_reports WHERE worker_id=?')
|
|
307
|
+
.get(this.store.workerBoundary.view(child.id).binding.worker_id)?.artifact_id ??
|
|
308
|
+
null,
|
|
309
|
+
}
|
|
310
|
+
: {}),
|
|
195
311
|
}),
|
|
196
312
|
);
|
|
197
313
|
artifact.commit();
|
|
@@ -199,6 +315,7 @@ export class ChildWork {
|
|
|
199
315
|
this.store.db
|
|
200
316
|
.prepare('UPDATE child_tasks SET state=?,result_id=?,artifact_id=? WHERE id=?')
|
|
201
317
|
.run(attempt.state, result, artifact.id, child.id);
|
|
318
|
+
this.store.workerBoundary.finish(child, attempt.state === 'interrupted');
|
|
202
319
|
this.store.event(child.parent_session, 'child.result_available', {
|
|
203
320
|
child_id: child.id,
|
|
204
321
|
result_id: result,
|
|
@@ -244,6 +361,7 @@ export class ChildWork {
|
|
|
244
361
|
this.store.db
|
|
245
362
|
.prepare("UPDATE submissions SET state='interrupted' WHERE id=?")
|
|
246
363
|
.run(attempt.submission_id);
|
|
364
|
+
this.store.projectTaskAttempt?.(this.store.attempt(attempt.id));
|
|
247
365
|
}
|
|
248
366
|
this.finish(this.store.attempt(attempt.id));
|
|
249
367
|
}
|
|
@@ -262,6 +380,7 @@ export class ChildWork {
|
|
|
262
380
|
this.store.db
|
|
263
381
|
.prepare("UPDATE submissions SET state='cancelled' WHERE id=?")
|
|
264
382
|
.run(work.submission_id);
|
|
383
|
+
this.store.projectTaskAttempt?.(this.store.attempt(work.id));
|
|
265
384
|
this.finish(this.store.attempt(work.id));
|
|
266
385
|
}
|
|
267
386
|
this.store.event(child.child_session, 'attempt.cancel_requested', {
|
|
@@ -274,10 +393,32 @@ export class ChildWork {
|
|
|
274
393
|
const children = this.store.db
|
|
275
394
|
.prepare('SELECT * FROM child_tasks WHERE parent_session=?')
|
|
276
395
|
.all(this.store.attempt(attempt).session_id) as unknown as ChildTask[];
|
|
396
|
+
for (const child of children.filter((child) => this.store.cancelled(child.child_attempt)))
|
|
397
|
+
for (const run of this.supervisor.coding?.list(child.child_session) ?? [])
|
|
398
|
+
if (
|
|
399
|
+
![
|
|
400
|
+
'succeeded',
|
|
401
|
+
'failed',
|
|
402
|
+
'cancelled',
|
|
403
|
+
'timed_out',
|
|
404
|
+
'output_limited',
|
|
405
|
+
'interrupted',
|
|
406
|
+
].includes(run.state)
|
|
407
|
+
)
|
|
408
|
+
this.supervisor.coding!.cancel(
|
|
409
|
+
child.child_session,
|
|
410
|
+
run.id,
|
|
411
|
+
'child-stop:' + run.id,
|
|
412
|
+
() => {},
|
|
413
|
+
);
|
|
277
414
|
await Promise.all(
|
|
278
415
|
children
|
|
279
416
|
.filter((child) => this.store.cancelled(child.child_attempt))
|
|
280
|
-
.map((child) =>
|
|
417
|
+
.map((child) =>
|
|
418
|
+
this.store.workerBoundary.view(child.id).binding.external
|
|
419
|
+
? this.external!.cancel(child)
|
|
420
|
+
: this.supervisor.cancelAttempt(child.child_attempt),
|
|
421
|
+
),
|
|
281
422
|
);
|
|
282
423
|
}
|
|
283
424
|
pause(): void {
|
package/src/claude-provider.ts
CHANGED
|
@@ -1,189 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
createAssistantMessageEventStream,
|
|
4
|
-
type AssistantMessage,
|
|
5
|
-
type AssistantMessageEvent,
|
|
6
|
-
type Model,
|
|
7
|
-
type ProviderHeaders,
|
|
8
|
-
} from '@earendil-works/pi-ai';
|
|
9
|
-
import { streamSimple as streamClaude } from '@earendil-works/pi-ai/api/anthropic-messages';
|
|
10
|
-
import { randomUUID } from 'node:crypto';
|
|
11
|
-
import { digest, textDigest } from './protocol.js';
|
|
12
|
-
import { getClaudeModel, validateClaudePayload, type ProviderRoute } from './provider-config.js';
|
|
13
|
-
|
|
14
|
-
export function registerClaude(
|
|
15
|
-
pi: ExtensionAPI,
|
|
16
|
-
route: ProviderRoute,
|
|
17
|
-
frame: () => string,
|
|
18
|
-
tools: string[],
|
|
19
|
-
bridge: (method: string, data: unknown) => Promise<unknown>,
|
|
20
|
-
fail: () => Promise<void>,
|
|
21
|
-
): void {
|
|
22
|
-
const catalog = getClaudeModel(route.model);
|
|
23
|
-
if (!catalog) throw new Error('Pinned Claude model missing');
|
|
24
|
-
const key = process.env.ANTHROPIC_API_KEY;
|
|
25
|
-
if (!key) throw new Error('Claude API key unavailable to worker');
|
|
26
|
-
pi.registerProvider('anthropic', {
|
|
27
|
-
baseUrl: 'https://api.anthropic.com',
|
|
28
|
-
apiKey: '$ANTHROPIC_API_KEY',
|
|
29
|
-
api: 'anthropic-messages',
|
|
30
|
-
models: [{ ...catalog, maxTokens: route.max_tokens }],
|
|
31
|
-
streamSimple(model, context, options) {
|
|
32
|
-
const output = createAssistantMessageEventStream();
|
|
33
|
-
const source = 'salaam-request:' + randomUUID();
|
|
34
|
-
let dispatched = false,
|
|
35
|
-
gateFailed = false,
|
|
36
|
-
status: number | undefined;
|
|
37
|
-
let last: AssistantMessage | undefined;
|
|
38
|
-
const normalize = (message: AssistantMessage): AssistantMessage => ({
|
|
39
|
-
...message,
|
|
40
|
-
responseId: source,
|
|
41
|
-
});
|
|
42
|
-
void (async () => {
|
|
43
|
-
try {
|
|
44
|
-
if (model.provider !== route.provider || model.id !== route.model)
|
|
45
|
-
throw new Error('Unapproved Claude route');
|
|
46
|
-
const native: Model<'anthropic-messages'> = {
|
|
47
|
-
...catalog,
|
|
48
|
-
provider: 'anthropic',
|
|
49
|
-
api: 'anthropic-messages',
|
|
50
|
-
baseUrl: 'https://api.anthropic.com',
|
|
51
|
-
compat: {
|
|
52
|
-
...catalog.compat,
|
|
53
|
-
supportsMidConvoSystemMessages: false,
|
|
54
|
-
supportsMidConvoToolChanges: false,
|
|
55
|
-
supportsMidConvoEffort: false,
|
|
56
|
-
},
|
|
57
|
-
};
|
|
58
|
-
const headers: ProviderHeaders = {};
|
|
59
|
-
for (const [name, value] of Object.entries(options?.headers ?? {}))
|
|
60
|
-
headers[name.toLowerCase()] = value;
|
|
61
|
-
delete headers.authorization;
|
|
62
|
-
delete headers['x-api-key'];
|
|
63
|
-
delete headers['anthropic-workspace-id'];
|
|
64
|
-
if (route.workspace_id) headers['anthropic-workspace-id'] = route.workspace_id;
|
|
65
|
-
let approvedDigest: string | undefined;
|
|
66
|
-
const upstream = streamClaude(native, context, {
|
|
67
|
-
...options,
|
|
68
|
-
apiKey: key,
|
|
69
|
-
env: {},
|
|
70
|
-
headers,
|
|
71
|
-
maxTokens: route.max_tokens,
|
|
72
|
-
reasoning: 'low',
|
|
73
|
-
maxRetries: 0,
|
|
74
|
-
timeoutMs: 120000,
|
|
75
|
-
onPayload: async (payload, rawModel) => {
|
|
76
|
-
const replacement = await options?.onPayload?.(payload, rawModel);
|
|
77
|
-
const actual = replacement === undefined ? payload : replacement;
|
|
78
|
-
try {
|
|
79
|
-
validateClaudePayload(actual, route, frame(), tools);
|
|
80
|
-
approvedDigest = digest(actual);
|
|
81
|
-
} catch {
|
|
82
|
-
gateFailed = true;
|
|
83
|
-
throw new Error('Claude payload denied');
|
|
84
|
-
}
|
|
85
|
-
return actual;
|
|
86
|
-
},
|
|
87
|
-
onResponse: async (response, rawModel) => {
|
|
88
|
-
status = response.status;
|
|
89
|
-
await options?.onResponse?.(response, rawModel);
|
|
90
|
-
},
|
|
91
|
-
fetch: async (input, init) => {
|
|
92
|
-
try {
|
|
93
|
-
const url = new URL(input instanceof Request ? input.url : String(input));
|
|
94
|
-
if (
|
|
95
|
-
url.href !== 'https://api.anthropic.com/v1/messages?beta=true' &&
|
|
96
|
-
url.href !== 'https://api.anthropic.com/v1/messages'
|
|
97
|
-
)
|
|
98
|
-
throw new Error('Unapproved Claude endpoint');
|
|
99
|
-
if (init?.method !== 'POST' || typeof init.body !== 'string' || dispatched)
|
|
100
|
-
throw new Error('Claude request replay denied');
|
|
101
|
-
const sentHeaders = new Headers(init.headers);
|
|
102
|
-
if (sentHeaders.get('x-api-key') !== key || sentHeaders.has('authorization'))
|
|
103
|
-
throw new Error('Unapproved Claude authentication');
|
|
104
|
-
const payload = JSON.parse(init.body);
|
|
105
|
-
validateClaudePayload(payload, route, frame(), tools);
|
|
106
|
-
if (approvedDigest !== digest(payload))
|
|
107
|
-
throw new Error('Claude payload changed after validation');
|
|
108
|
-
dispatched = true;
|
|
109
|
-
await bridge('model', {
|
|
110
|
-
source_key: source,
|
|
111
|
-
provider: 'anthropic',
|
|
112
|
-
model: route.model,
|
|
113
|
-
context_digest: textDigest(frame()),
|
|
114
|
-
payload_digest: approvedDigest,
|
|
115
|
-
payload,
|
|
116
|
-
});
|
|
117
|
-
} catch {
|
|
118
|
-
gateFailed = true;
|
|
119
|
-
throw new Error('Claude request denied by Salaam');
|
|
120
|
-
}
|
|
121
|
-
return globalThis.fetch(input, { ...init, redirect: 'error' });
|
|
122
|
-
},
|
|
123
|
-
});
|
|
124
|
-
for await (const event of upstream) {
|
|
125
|
-
if ('partial' in event) last = normalize(event.partial);
|
|
126
|
-
if (event.type === 'done') {
|
|
127
|
-
const message = normalize(event.message);
|
|
128
|
-
last = message;
|
|
129
|
-
await bridge('usage', {
|
|
130
|
-
source_key: source,
|
|
131
|
-
usage: message.usage,
|
|
132
|
-
provider_response_id: event.message.responseId ?? null,
|
|
133
|
-
});
|
|
134
|
-
output.push({ ...event, message });
|
|
135
|
-
} else if (event.type === 'error') {
|
|
136
|
-
const message = normalize(event.error);
|
|
137
|
-
const code = /^(?:Error:\s*)?(\d{3})\b/.exec(message.errorMessage ?? '')?.[1];
|
|
138
|
-
status = status ?? (code ? Number(code) : undefined);
|
|
139
|
-
delete message.diagnostics;
|
|
140
|
-
message.errorMessage = options?.signal?.aborted
|
|
141
|
-
? 'Claude request cancelled'
|
|
142
|
-
: `Claude request failed${status ? ` (HTTP ${status})` : ''}`;
|
|
143
|
-
message.content = [];
|
|
144
|
-
last = message;
|
|
145
|
-
output.push({ ...event, error: message });
|
|
146
|
-
} else
|
|
147
|
-
output.push(
|
|
148
|
-
('partial' in event
|
|
149
|
-
? { ...event, partial: last! }
|
|
150
|
-
: event) as AssistantMessageEvent,
|
|
151
|
-
);
|
|
152
|
-
}
|
|
153
|
-
output.end();
|
|
154
|
-
if (gateFailed) await fail();
|
|
155
|
-
} catch {
|
|
156
|
-
const error: AssistantMessage = {
|
|
157
|
-
role: 'assistant',
|
|
158
|
-
content: [],
|
|
159
|
-
api: 'anthropic-messages',
|
|
160
|
-
provider: 'anthropic',
|
|
161
|
-
model: route.model,
|
|
162
|
-
timestamp: Date.now(),
|
|
163
|
-
stopReason: options?.signal?.aborted ? 'aborted' : 'error',
|
|
164
|
-
usage: last?.usage ?? {
|
|
165
|
-
input: 0,
|
|
166
|
-
output: 0,
|
|
167
|
-
cacheRead: 0,
|
|
168
|
-
cacheWrite: 0,
|
|
169
|
-
totalTokens: 0,
|
|
170
|
-
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
|
|
171
|
-
},
|
|
172
|
-
responseId: source,
|
|
173
|
-
errorMessage: options?.signal?.aborted
|
|
174
|
-
? 'Claude request cancelled'
|
|
175
|
-
: 'Claude required control failed',
|
|
176
|
-
};
|
|
177
|
-
output.push({
|
|
178
|
-
type: 'error',
|
|
179
|
-
reason: error.stopReason === 'aborted' ? 'aborted' : 'error',
|
|
180
|
-
error,
|
|
181
|
-
});
|
|
182
|
-
output.end();
|
|
183
|
-
if (!options?.signal?.aborted) await fail();
|
|
184
|
-
}
|
|
185
|
-
})();
|
|
186
|
-
return output;
|
|
187
|
-
},
|
|
188
|
-
});
|
|
189
|
-
}
|
|
1
|
+
// Kept as an internal compatibility entry point for existing extensions.
|
|
2
|
+
export { registerLiveProvider as registerClaude } from './live-provider.js';
|