blun-king-cli 9.1.469 → 9.1.471
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.
|
@@ -137,6 +137,24 @@ function publicErrorResponse(error) {
|
|
|
137
137
|
},
|
|
138
138
|
});
|
|
139
139
|
}
|
|
140
|
+
if (code === 'AGENT_API_SESSION_NOT_FOUND') {
|
|
141
|
+
return jsonResponse(404, {
|
|
142
|
+
error: {
|
|
143
|
+
type: 'invalid_request_error',
|
|
144
|
+
code,
|
|
145
|
+
message: 'Agent session was not found.',
|
|
146
|
+
},
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
if (code === 'AGENT_API_SESSION_LIMIT' || code === 'AGENT_API_PRINCIPAL_LIMIT') {
|
|
150
|
+
return jsonResponse(429, {
|
|
151
|
+
error: {
|
|
152
|
+
type: 'rate_limit_error',
|
|
153
|
+
code,
|
|
154
|
+
message: 'Agent session limit reached.',
|
|
155
|
+
},
|
|
156
|
+
});
|
|
157
|
+
}
|
|
140
158
|
if (code.startsWith('AGENT_API_INVALID_')) {
|
|
141
159
|
return jsonResponse(400, {
|
|
142
160
|
error: {
|
|
@@ -193,27 +211,35 @@ function createAgentApiHttpAdapter(options = {}) {
|
|
|
193
211
|
|
|
194
212
|
function requireOwner(principalId, sessionId, requestId) {
|
|
195
213
|
const key = scopeKey(sessionId, requestId);
|
|
196
|
-
const
|
|
197
|
-
if (
|
|
214
|
+
const scopeOwners = owners.get(key);
|
|
215
|
+
if (scopeOwners === undefined) {
|
|
198
216
|
throw adapterError(
|
|
199
217
|
'AGENT_API_TURN_NOT_ACTIVE',
|
|
200
218
|
'Agent turn is not active.',
|
|
201
219
|
409,
|
|
202
220
|
);
|
|
203
221
|
}
|
|
204
|
-
|
|
222
|
+
const internalSessionId = scopeOwners.get(principalId);
|
|
223
|
+
if (internalSessionId === undefined) {
|
|
205
224
|
throw adapterError(
|
|
206
225
|
'AGENT_API_FORBIDDEN',
|
|
207
226
|
'Agent turn belongs to a different account.',
|
|
208
227
|
403,
|
|
209
228
|
);
|
|
210
229
|
}
|
|
211
|
-
return key;
|
|
230
|
+
return { internalSessionId, key };
|
|
212
231
|
}
|
|
213
232
|
|
|
214
|
-
function trackScope(key, principalId, response) {
|
|
215
|
-
if (responseNeedsToolResult(response))
|
|
216
|
-
|
|
233
|
+
function trackScope(key, principalId, internalSessionId, response) {
|
|
234
|
+
if (responseNeedsToolResult(response)) {
|
|
235
|
+
const scopeOwners = owners.get(key) ?? new Map();
|
|
236
|
+
scopeOwners.set(principalId, internalSessionId);
|
|
237
|
+
owners.set(key, scopeOwners);
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
const scopeOwners = owners.get(key);
|
|
241
|
+
scopeOwners?.delete(principalId);
|
|
242
|
+
if (scopeOwners?.size === 0) owners.delete(key);
|
|
217
243
|
}
|
|
218
244
|
|
|
219
245
|
async function handleStart(input, principalId) {
|
|
@@ -232,46 +258,53 @@ function createAgentApiHttpAdapter(options = {}) {
|
|
|
232
258
|
sessionId,
|
|
233
259
|
create: true,
|
|
234
260
|
});
|
|
235
|
-
if (session?.id !==
|
|
261
|
+
if (typeof session?.id !== 'string' || session.id.trim().length === 0) {
|
|
236
262
|
throw adapterError(
|
|
237
263
|
'AGENT_API_SESSION_SCOPE_MISMATCH',
|
|
238
|
-
'resolved session does not
|
|
264
|
+
'resolved session does not have an isolated session ID',
|
|
239
265
|
403,
|
|
240
266
|
);
|
|
241
267
|
}
|
|
268
|
+
const internalSessionId = session.id.trim();
|
|
242
269
|
const response = publicCompletion(await controller.startTurn({
|
|
243
270
|
session,
|
|
271
|
+
publicSessionId: sessionId,
|
|
244
272
|
requestId,
|
|
245
273
|
prompt: normalizePrompt(body.input),
|
|
246
274
|
tools: normalizeTools(body.tools),
|
|
247
275
|
}));
|
|
248
|
-
trackScope(
|
|
276
|
+
trackScope(
|
|
277
|
+
scopeKey(sessionId, requestId),
|
|
278
|
+
principalId,
|
|
279
|
+
internalSessionId,
|
|
280
|
+
response,
|
|
281
|
+
);
|
|
249
282
|
return jsonResponse(200, response);
|
|
250
283
|
}
|
|
251
284
|
|
|
252
285
|
async function handleContinue(input, principalId, requestId) {
|
|
253
286
|
const body = requireBody(input.body);
|
|
254
287
|
const sessionId = requirePublicId(body.session_id, 'session_id');
|
|
255
|
-
const
|
|
288
|
+
const owner = requireOwner(principalId, sessionId, requestId);
|
|
256
289
|
const response = publicCompletion(await controller.continueTurn({
|
|
257
|
-
sessionId,
|
|
290
|
+
sessionId: owner.internalSessionId,
|
|
258
291
|
requestId,
|
|
259
292
|
toolResults: body.tool_results,
|
|
260
293
|
}));
|
|
261
|
-
trackScope(key, principalId, response);
|
|
294
|
+
trackScope(owner.key, principalId, owner.internalSessionId, response);
|
|
262
295
|
return jsonResponse(200, response);
|
|
263
296
|
}
|
|
264
297
|
|
|
265
298
|
async function handleCancel(input, principalId, requestId) {
|
|
266
299
|
const body = requireBody(input.body ?? {});
|
|
267
300
|
const sessionId = requirePublicId(body.session_id, 'session_id');
|
|
268
|
-
const
|
|
301
|
+
const owner = requireOwner(principalId, sessionId, requestId);
|
|
269
302
|
await controller.cancelTurn({
|
|
270
|
-
sessionId,
|
|
303
|
+
sessionId: owner.internalSessionId,
|
|
271
304
|
requestId,
|
|
272
305
|
reason: typeof body.reason === 'string' ? body.reason : undefined,
|
|
273
306
|
});
|
|
274
|
-
|
|
307
|
+
trackScope(owner.key, principalId, owner.internalSessionId, null);
|
|
275
308
|
return jsonResponse(200, {
|
|
276
309
|
id: `agent-${requestId}`,
|
|
277
310
|
object: 'agent.turn.cancelled',
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
|
|
3
|
+
import { createAgentApiHttpAdapter } from './agent-api-http-adapter.mjs';
|
|
4
|
+
import { createAgentApiPrivateHttpServer } from './agent-api-private-http-server.mjs';
|
|
5
|
+
import { createAgentApiRuntime } from './agent-api-runtime.mjs';
|
|
6
|
+
import { createAgentApiSessionRegistry } from './agent-api-session-registry.mjs';
|
|
7
|
+
import { createAgentApiTurnController } from './agent-api-turn-controller.mjs';
|
|
8
|
+
|
|
9
|
+
const MIN_SECRET_BYTES = 32;
|
|
10
|
+
|
|
11
|
+
function requireSecret(value, label) {
|
|
12
|
+
if (typeof value !== 'string' || Buffer.byteLength(value, 'utf8') < MIN_SECRET_BYTES) {
|
|
13
|
+
throw new TypeError(`${label} must contain at least ${MIN_SECRET_BYTES} bytes`);
|
|
14
|
+
}
|
|
15
|
+
return value;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function requireAbsoluteRoot(value) {
|
|
19
|
+
if (typeof value !== 'string' || !path.isAbsolute(value)) {
|
|
20
|
+
throw new TypeError('rootDir must be an absolute path');
|
|
21
|
+
}
|
|
22
|
+
return path.resolve(value);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function requireObject(value, label) {
|
|
26
|
+
if (value === undefined) return {};
|
|
27
|
+
if (value === null || typeof value !== 'object' || Array.isArray(value)) {
|
|
28
|
+
throw new TypeError(`${label} must be an object`);
|
|
29
|
+
}
|
|
30
|
+
return value;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function cloneHarnessConfig(value) {
|
|
34
|
+
if (value === null || typeof value !== 'object' || Array.isArray(value)) {
|
|
35
|
+
throw new TypeError('harnessConfig must be an object');
|
|
36
|
+
}
|
|
37
|
+
let config;
|
|
38
|
+
try {
|
|
39
|
+
config = structuredClone(value);
|
|
40
|
+
} catch {
|
|
41
|
+
throw new TypeError('harnessConfig must be structured-cloneable');
|
|
42
|
+
}
|
|
43
|
+
if (config.defaultModel !== 'king') {
|
|
44
|
+
throw new TypeError('harnessConfig.defaultModel must be king');
|
|
45
|
+
}
|
|
46
|
+
if (config.models === null
|
|
47
|
+
|| typeof config.models !== 'object'
|
|
48
|
+
|| Array.isArray(config.models)
|
|
49
|
+
|| config.models.king === null
|
|
50
|
+
|| typeof config.models.king !== 'object'
|
|
51
|
+
|| Array.isArray(config.models.king)) {
|
|
52
|
+
throw new TypeError('harnessConfig.models.king must be an object');
|
|
53
|
+
}
|
|
54
|
+
const provider = config.models.king.provider;
|
|
55
|
+
if (typeof provider !== 'string'
|
|
56
|
+
|| provider.length === 0
|
|
57
|
+
|| config.providers === null
|
|
58
|
+
|| typeof config.providers !== 'object'
|
|
59
|
+
|| Array.isArray(config.providers)
|
|
60
|
+
|| !Object.hasOwn(config.providers, provider)) {
|
|
61
|
+
throw new TypeError('harnessConfig must define the provider used by models.king');
|
|
62
|
+
}
|
|
63
|
+
return config;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async function createBundledHarness(input) {
|
|
67
|
+
const { createBlunHarness } = await import('../blun.mjs');
|
|
68
|
+
return createBlunHarness(input);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function resolveHarnessPreparation(options) {
|
|
72
|
+
const hasConfig = options.harnessConfig !== undefined;
|
|
73
|
+
const hasPrepare = options.prepareHarness !== undefined;
|
|
74
|
+
if (hasConfig && hasPrepare) {
|
|
75
|
+
throw new TypeError('only one of harnessConfig or prepareHarness may be provided');
|
|
76
|
+
}
|
|
77
|
+
if (!hasConfig && !hasPrepare) {
|
|
78
|
+
throw new TypeError('harnessConfig or prepareHarness is required');
|
|
79
|
+
}
|
|
80
|
+
if (hasPrepare) {
|
|
81
|
+
if (typeof options.prepareHarness !== 'function') {
|
|
82
|
+
throw new TypeError('prepareHarness must be a function');
|
|
83
|
+
}
|
|
84
|
+
return options.prepareHarness;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const config = cloneHarnessConfig(options.harnessConfig);
|
|
88
|
+
return async ({ harness }) => {
|
|
89
|
+
if (typeof harness?.setConfig !== 'function') {
|
|
90
|
+
throw new TypeError('Agent API harness must provide setConfig');
|
|
91
|
+
}
|
|
92
|
+
await harness.setConfig(structuredClone(config));
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function createAgentApiServiceHost(options = {}) {
|
|
97
|
+
const rootDir = requireAbsoluteRoot(options.rootDir);
|
|
98
|
+
const internalSecret = requireSecret(options.internalSecret, 'internalSecret');
|
|
99
|
+
const isolationSecret = requireSecret(options.isolationSecret, 'isolationSecret');
|
|
100
|
+
if (internalSecret === isolationSecret) {
|
|
101
|
+
throw new TypeError('internalSecret and isolationSecret must be different');
|
|
102
|
+
}
|
|
103
|
+
if (options.createHarness !== undefined && typeof options.createHarness !== 'function') {
|
|
104
|
+
throw new TypeError('createHarness must be a function');
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const runtimeOptions = requireObject(options.runtimeOptions, 'runtimeOptions');
|
|
108
|
+
const controllerOptions = requireObject(options.controllerOptions, 'controllerOptions');
|
|
109
|
+
const prepareHarness = resolveHarnessPreparation(options);
|
|
110
|
+
const runtime = createAgentApiRuntime(runtimeOptions);
|
|
111
|
+
const controller = createAgentApiTurnController({
|
|
112
|
+
...controllerOptions,
|
|
113
|
+
runtime,
|
|
114
|
+
publicModel: 'king',
|
|
115
|
+
});
|
|
116
|
+
const sessionRegistry = createAgentApiSessionRegistry({
|
|
117
|
+
rootDir,
|
|
118
|
+
isolationSecret,
|
|
119
|
+
createHarness: options.createHarness ?? createBundledHarness,
|
|
120
|
+
prepareHarness,
|
|
121
|
+
sessionOptions: options.sessionOptions,
|
|
122
|
+
maxPrincipals: options.maxPrincipals,
|
|
123
|
+
maxSessionsPerPrincipal: options.maxSessionsPerPrincipal,
|
|
124
|
+
});
|
|
125
|
+
const adapter = createAgentApiHttpAdapter({
|
|
126
|
+
controller,
|
|
127
|
+
resolveSession: sessionRegistry.resolveSession,
|
|
128
|
+
});
|
|
129
|
+
const privateServer = createAgentApiPrivateHttpServer({
|
|
130
|
+
adapter,
|
|
131
|
+
internalSecret,
|
|
132
|
+
maxBodyBytes: options.maxBodyBytes,
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
let state = 'idle';
|
|
136
|
+
let listenPromise;
|
|
137
|
+
let closePromise;
|
|
138
|
+
|
|
139
|
+
async function listen(input = {}) {
|
|
140
|
+
if (state === 'closed' || state === 'closing') {
|
|
141
|
+
throw new Error('Agent API service host is closed');
|
|
142
|
+
}
|
|
143
|
+
if (state !== 'idle') {
|
|
144
|
+
throw new Error('Agent API service host is already listening');
|
|
145
|
+
}
|
|
146
|
+
state = 'starting';
|
|
147
|
+
try {
|
|
148
|
+
listenPromise = privateServer.listen(input);
|
|
149
|
+
const address = await listenPromise;
|
|
150
|
+
if (state === 'closing' || state === 'closed') {
|
|
151
|
+
await privateServer.close();
|
|
152
|
+
throw new Error('Agent API service host is closed');
|
|
153
|
+
}
|
|
154
|
+
state = 'listening';
|
|
155
|
+
return address;
|
|
156
|
+
} catch (error) {
|
|
157
|
+
if (state === 'starting') state = 'idle';
|
|
158
|
+
throw error;
|
|
159
|
+
} finally {
|
|
160
|
+
listenPromise = undefined;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function close() {
|
|
165
|
+
if (closePromise !== undefined) return closePromise;
|
|
166
|
+
state = 'closing';
|
|
167
|
+
closePromise = (async () => {
|
|
168
|
+
let firstError;
|
|
169
|
+
if (listenPromise !== undefined) {
|
|
170
|
+
try {
|
|
171
|
+
await listenPromise;
|
|
172
|
+
} catch {
|
|
173
|
+
// The listen failure remains with its caller; cleanup continues here.
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
try {
|
|
177
|
+
await privateServer.close();
|
|
178
|
+
} catch (error) {
|
|
179
|
+
firstError = error;
|
|
180
|
+
}
|
|
181
|
+
try {
|
|
182
|
+
await sessionRegistry.close();
|
|
183
|
+
} catch (error) {
|
|
184
|
+
firstError ??= error;
|
|
185
|
+
}
|
|
186
|
+
state = 'closed';
|
|
187
|
+
if (firstError !== undefined) throw firstError;
|
|
188
|
+
})();
|
|
189
|
+
return closePromise;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
return Object.freeze({ close, listen });
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export {
|
|
196
|
+
MIN_SECRET_BYTES,
|
|
197
|
+
createAgentApiServiceHost,
|
|
198
|
+
};
|
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
import { createHmac } from 'node:crypto';
|
|
2
|
+
import { mkdir } from 'node:fs/promises';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
|
|
5
|
+
const DEFAULT_MAX_SESSIONS_PER_PRINCIPAL = 16;
|
|
6
|
+
const DEFAULT_MAX_PRINCIPALS = 1024;
|
|
7
|
+
const PRINCIPAL_PATTERN = /^[A-Za-z0-9@._:+-]{1,256}$/u;
|
|
8
|
+
const PUBLIC_SESSION_PATTERN = /^[A-Za-z0-9._:-]{1,128}$/u;
|
|
9
|
+
|
|
10
|
+
class AgentApiSessionRegistryError extends Error {
|
|
11
|
+
constructor(code, message) {
|
|
12
|
+
super(message);
|
|
13
|
+
this.name = 'AgentApiSessionRegistryError';
|
|
14
|
+
this.code = code;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function registryError(code, message) {
|
|
19
|
+
return new AgentApiSessionRegistryError(code, message);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function requirePattern(value, pattern, label) {
|
|
23
|
+
if (typeof value !== 'string' || !pattern.test(value)) {
|
|
24
|
+
throw registryError('AGENT_API_INVALID_SCOPE', `${label} is invalid`);
|
|
25
|
+
}
|
|
26
|
+
return value;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function requireRootDir(value) {
|
|
30
|
+
if (typeof value !== 'string' || value.trim().length === 0) {
|
|
31
|
+
throw new TypeError('rootDir must be a non-empty path');
|
|
32
|
+
}
|
|
33
|
+
return path.resolve(value);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function requireLimit(value) {
|
|
37
|
+
if (!Number.isSafeInteger(value) || value < 1) {
|
|
38
|
+
throw new TypeError('maxSessionsPerPrincipal must be a positive integer');
|
|
39
|
+
}
|
|
40
|
+
return value;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function validateIsolationSecret(value) {
|
|
44
|
+
if (typeof value !== 'string' || Buffer.byteLength(value, 'utf8') < 32) {
|
|
45
|
+
throw new TypeError('isolationSecret must contain at least 32 bytes');
|
|
46
|
+
}
|
|
47
|
+
return value;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function opaqueHash(secret, ...parts) {
|
|
51
|
+
return createHmac('sha256', secret).update(parts.join('\0')).digest('hex');
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function validateHarness(harness) {
|
|
55
|
+
if (harness === null || typeof harness !== 'object'
|
|
56
|
+
|| typeof harness.createSession !== 'function') {
|
|
57
|
+
throw new TypeError('createHarness must return a harness with createSession');
|
|
58
|
+
}
|
|
59
|
+
return harness;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function validateSession(session, internalSessionId) {
|
|
63
|
+
if (session === null || typeof session !== 'object' || session.id !== internalSessionId) {
|
|
64
|
+
throw registryError(
|
|
65
|
+
'AGENT_API_SESSION_SCOPE_MISMATCH',
|
|
66
|
+
'SDK session does not match the isolated session scope',
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
return session;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function normalizeSessionOptions(value) {
|
|
73
|
+
if (value === undefined) return {};
|
|
74
|
+
if (value === null || typeof value !== 'object' || Array.isArray(value)) {
|
|
75
|
+
throw new TypeError('sessionOptions must resolve to an object');
|
|
76
|
+
}
|
|
77
|
+
return { ...value };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function createAgentApiSessionRegistry(options = {}) {
|
|
81
|
+
const rootDir = requireRootDir(options.rootDir);
|
|
82
|
+
const isolationSecret = validateIsolationSecret(options.isolationSecret);
|
|
83
|
+
if (typeof options.createHarness !== 'function') {
|
|
84
|
+
throw new TypeError('createHarness must be a function');
|
|
85
|
+
}
|
|
86
|
+
if (options.prepareHarness !== undefined && typeof options.prepareHarness !== 'function') {
|
|
87
|
+
throw new TypeError('prepareHarness must be a function');
|
|
88
|
+
}
|
|
89
|
+
if (options.sessionOptions !== undefined
|
|
90
|
+
&& typeof options.sessionOptions !== 'function'
|
|
91
|
+
&& (options.sessionOptions === null
|
|
92
|
+
|| typeof options.sessionOptions !== 'object'
|
|
93
|
+
|| Array.isArray(options.sessionOptions))) {
|
|
94
|
+
throw new TypeError('sessionOptions must be an object or function');
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const maxSessionsPerPrincipal = requireLimit(
|
|
98
|
+
options.maxSessionsPerPrincipal ?? DEFAULT_MAX_SESSIONS_PER_PRINCIPAL,
|
|
99
|
+
);
|
|
100
|
+
const maxPrincipals = requireLimit(options.maxPrincipals ?? DEFAULT_MAX_PRINCIPALS);
|
|
101
|
+
const principals = new Map();
|
|
102
|
+
let closed = false;
|
|
103
|
+
|
|
104
|
+
function ensureOpen() {
|
|
105
|
+
if (closed) {
|
|
106
|
+
throw registryError('AGENT_API_SESSION_REGISTRY_CLOSED', 'session registry is closed');
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function createPrincipalRecord(principalId) {
|
|
111
|
+
const principalKey = opaqueHash(isolationSecret, 'principal', principalId).slice(0, 40);
|
|
112
|
+
const principalRoot = path.join(rootDir, 'principals', principalKey);
|
|
113
|
+
const homeDir = path.join(principalRoot, 'home');
|
|
114
|
+
const workRoot = path.join(principalRoot, 'work');
|
|
115
|
+
const record = {
|
|
116
|
+
harnessReady: false,
|
|
117
|
+
harnessPromise: undefined,
|
|
118
|
+
homeDir,
|
|
119
|
+
principalKey,
|
|
120
|
+
principalRoot,
|
|
121
|
+
sessions: new Map(),
|
|
122
|
+
workRoot,
|
|
123
|
+
};
|
|
124
|
+
record.harnessPromise = (async () => {
|
|
125
|
+
await mkdir(homeDir, { recursive: true });
|
|
126
|
+
await mkdir(workRoot, { recursive: true });
|
|
127
|
+
const harness = validateHarness(await options.createHarness({
|
|
128
|
+
homeDir,
|
|
129
|
+
principalKey,
|
|
130
|
+
uiMode: 'api',
|
|
131
|
+
}));
|
|
132
|
+
try {
|
|
133
|
+
if (typeof harness.ensureConfigFile === 'function') {
|
|
134
|
+
await harness.ensureConfigFile();
|
|
135
|
+
}
|
|
136
|
+
if (options.prepareHarness !== undefined) {
|
|
137
|
+
await options.prepareHarness({ harness, homeDir, principalKey });
|
|
138
|
+
}
|
|
139
|
+
record.harnessReady = true;
|
|
140
|
+
} catch (error) {
|
|
141
|
+
try {
|
|
142
|
+
await harness.close?.();
|
|
143
|
+
} catch {
|
|
144
|
+
// Preserve the preparation error.
|
|
145
|
+
}
|
|
146
|
+
throw error;
|
|
147
|
+
}
|
|
148
|
+
return harness;
|
|
149
|
+
})();
|
|
150
|
+
principals.set(principalId, record);
|
|
151
|
+
return record;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function getOrCreatePrincipal(principalId) {
|
|
155
|
+
const existing = principals.get(principalId);
|
|
156
|
+
if (existing !== undefined) return existing;
|
|
157
|
+
if (principals.size >= maxPrincipals) {
|
|
158
|
+
throw registryError(
|
|
159
|
+
'AGENT_API_PRINCIPAL_LIMIT',
|
|
160
|
+
'Agent API active customer limit reached',
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
return createPrincipalRecord(principalId);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
async function sessionOptionsFor(input) {
|
|
167
|
+
const configured = typeof options.sessionOptions === 'function'
|
|
168
|
+
? await options.sessionOptions(input)
|
|
169
|
+
: options.sessionOptions;
|
|
170
|
+
return normalizeSessionOptions(configured);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
async function createSession(record, principalId, publicSessionId) {
|
|
174
|
+
const sessionHash = opaqueHash(
|
|
175
|
+
isolationSecret,
|
|
176
|
+
'session',
|
|
177
|
+
principalId,
|
|
178
|
+
publicSessionId,
|
|
179
|
+
);
|
|
180
|
+
const internalSessionId = `agent_api_${sessionHash.slice(0, 48)}`;
|
|
181
|
+
const workDir = path.join(record.workRoot, sessionHash.slice(0, 48));
|
|
182
|
+
await mkdir(workDir, { recursive: true });
|
|
183
|
+
const harness = await record.harnessPromise;
|
|
184
|
+
const configured = await sessionOptionsFor({
|
|
185
|
+
internalSessionId,
|
|
186
|
+
principalKey: record.principalKey,
|
|
187
|
+
publicSessionId,
|
|
188
|
+
workDir,
|
|
189
|
+
});
|
|
190
|
+
const session = await harness.createSession({
|
|
191
|
+
...configured,
|
|
192
|
+
id: internalSessionId,
|
|
193
|
+
model: configured.model ?? 'king',
|
|
194
|
+
permission: configured.permission ?? 'auto',
|
|
195
|
+
workDir,
|
|
196
|
+
});
|
|
197
|
+
return validateSession(session, internalSessionId);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
async function resolveSession(input) {
|
|
201
|
+
ensureOpen();
|
|
202
|
+
if (input === null || typeof input !== 'object') {
|
|
203
|
+
throw registryError('AGENT_API_INVALID_SCOPE', 'session scope is required');
|
|
204
|
+
}
|
|
205
|
+
const principalId = requirePattern(input.principalId, PRINCIPAL_PATTERN, 'principalId');
|
|
206
|
+
const publicSessionId = requirePattern(
|
|
207
|
+
input.sessionId,
|
|
208
|
+
PUBLIC_SESSION_PATTERN,
|
|
209
|
+
'sessionId',
|
|
210
|
+
);
|
|
211
|
+
let record = principals.get(principalId);
|
|
212
|
+
const existing = record?.sessions.get(publicSessionId);
|
|
213
|
+
if (existing !== undefined) return existing;
|
|
214
|
+
if (input.create !== true) {
|
|
215
|
+
throw registryError('AGENT_API_SESSION_NOT_FOUND', 'Agent API session was not found');
|
|
216
|
+
}
|
|
217
|
+
record = getOrCreatePrincipal(principalId);
|
|
218
|
+
if (record.sessions.size >= maxSessionsPerPrincipal) {
|
|
219
|
+
throw registryError(
|
|
220
|
+
'AGENT_API_SESSION_LIMIT',
|
|
221
|
+
'Agent API session limit reached for this customer',
|
|
222
|
+
);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const pending = createSession(record, principalId, publicSessionId);
|
|
226
|
+
record.sessions.set(publicSessionId, pending);
|
|
227
|
+
try {
|
|
228
|
+
return await pending;
|
|
229
|
+
} catch (error) {
|
|
230
|
+
if (record.sessions.get(publicSessionId) === pending) {
|
|
231
|
+
record.sessions.delete(publicSessionId);
|
|
232
|
+
}
|
|
233
|
+
if (!record.harnessReady
|
|
234
|
+
&& record.sessions.size === 0
|
|
235
|
+
&& principals.get(principalId) === record) {
|
|
236
|
+
principals.delete(principalId);
|
|
237
|
+
}
|
|
238
|
+
throw error;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
async function closeSession(input) {
|
|
243
|
+
ensureOpen();
|
|
244
|
+
if (input === null || typeof input !== 'object') {
|
|
245
|
+
throw registryError('AGENT_API_INVALID_SCOPE', 'session scope is required');
|
|
246
|
+
}
|
|
247
|
+
const principalId = requirePattern(input.principalId, PRINCIPAL_PATTERN, 'principalId');
|
|
248
|
+
const publicSessionId = requirePattern(
|
|
249
|
+
input.sessionId,
|
|
250
|
+
PUBLIC_SESSION_PATTERN,
|
|
251
|
+
'sessionId',
|
|
252
|
+
);
|
|
253
|
+
const record = principals.get(principalId);
|
|
254
|
+
const pending = record?.sessions.get(publicSessionId);
|
|
255
|
+
if (pending === undefined) return false;
|
|
256
|
+
record.sessions.delete(publicSessionId);
|
|
257
|
+
const session = await pending;
|
|
258
|
+
await session.close?.();
|
|
259
|
+
if (record.sessions.size === 0 && principals.get(principalId) === record) {
|
|
260
|
+
principals.delete(principalId);
|
|
261
|
+
const harness = await record.harnessPromise.catch(() => null);
|
|
262
|
+
await harness?.close?.();
|
|
263
|
+
}
|
|
264
|
+
return true;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
async function close() {
|
|
268
|
+
if (closed) return;
|
|
269
|
+
closed = true;
|
|
270
|
+
const records = [...principals.values()];
|
|
271
|
+
principals.clear();
|
|
272
|
+
for (const record of records) {
|
|
273
|
+
const sessions = await Promise.allSettled([...record.sessions.values()]);
|
|
274
|
+
record.sessions.clear();
|
|
275
|
+
for (const outcome of sessions) {
|
|
276
|
+
if (outcome.status === 'fulfilled') await outcome.value.close?.();
|
|
277
|
+
}
|
|
278
|
+
const harness = await record.harnessPromise.catch(() => null);
|
|
279
|
+
await harness?.close?.();
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
return Object.freeze({ close, closeSession, resolveSession });
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
export {
|
|
287
|
+
AgentApiSessionRegistryError,
|
|
288
|
+
DEFAULT_MAX_PRINCIPALS,
|
|
289
|
+
DEFAULT_MAX_SESSIONS_PER_PRINCIPAL,
|
|
290
|
+
createAgentApiSessionRegistry,
|
|
291
|
+
};
|
|
@@ -123,7 +123,7 @@ function openAiEnvelope(turn, message, finishReason) {
|
|
|
123
123
|
blun: {
|
|
124
124
|
mode: 'agent',
|
|
125
125
|
task_id: turn.taskId,
|
|
126
|
-
session_id: turn.
|
|
126
|
+
session_id: turn.publicSessionId,
|
|
127
127
|
request_id: turn.requestId,
|
|
128
128
|
},
|
|
129
129
|
};
|
|
@@ -337,6 +337,9 @@ function createAgentApiTurnController(options = {}) {
|
|
|
337
337
|
throw controllerError('AGENT_API_INVALID_REQUEST', 'turn input must be an object');
|
|
338
338
|
}
|
|
339
339
|
const sessionId = validateSession(input.session);
|
|
340
|
+
const publicSessionId = input.publicSessionId === undefined
|
|
341
|
+
? sessionId
|
|
342
|
+
: requireId(input.publicSessionId, 'publicSessionId');
|
|
340
343
|
const requestId = requireId(input.requestId, 'requestId');
|
|
341
344
|
if (activeTurns.has(sessionId)) {
|
|
342
345
|
throw controllerError(
|
|
@@ -360,6 +363,7 @@ function createAgentApiTurnController(options = {}) {
|
|
|
360
363
|
closed: false,
|
|
361
364
|
created: Math.floor(Number(now()) / 1000),
|
|
362
365
|
publicModel,
|
|
366
|
+
publicSessionId,
|
|
363
367
|
requestId,
|
|
364
368
|
session: input.session,
|
|
365
369
|
sessionId,
|