@myagentroam/node 0.9.135 → 0.9.136
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/dist/codex-app-server.js +14 -0
- package/dist/connector.js +4 -2
- package/dist/node-execution-environment.js +8 -0
- package/dist/runner/abstract-runner.js +35 -4
- package/dist/runner/claude/managed-run-controller.js +8 -1
- package/dist/runner/codex/conversation-parser.js +194 -13
- package/dist/runner/codex/managed-run-controller.js +2 -1
- package/dist/runner/codex-runner.js +113 -10
- package/dist/runner/mar-agent/computer-use.js +1 -0
- package/dist/runner/mar-agent/conversation-normalizer.js +8 -6
- package/dist/runner/mar-agent/hosted-conversation-runtime.js +3 -2
- package/dist/runner/mar-agent/managed-run-controller.js +46 -8
- package/dist/runner/mar-agent/mcp-client-pool.js +253 -29
- package/dist/runner/mar-agent/runtime-model-factory.js +1 -1
- package/dist/runner/mar-agent-runner.js +3 -0
- package/dist/runner/opencode/managed-run-controller.js +2 -1
- package/dist/service/continuous-run-coordinator.js +3 -1
- package/dist/service/conversation-history-service.js +29 -40
- package/dist/service/hosted-conversation-node-service.js +1 -1
- package/dist/service/native-session-watch-service.js +13 -4
- package/dist/service/run-event-service.js +13 -0
- package/dist/service/session-catalog-service.js +3 -4
- package/dist/service/session-message-service.js +4 -2
- package/dist/util/node-operation-parsers.js +1 -3
- package/package.json +6 -5
|
@@ -1,7 +1,9 @@
|
|
|
1
|
-
import { createHash } from 'node:crypto';
|
|
1
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
2
2
|
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
|
3
3
|
import { getDefaultEnvironment, StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
|
|
4
4
|
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
|
|
5
|
+
import { ToolListChangedNotificationSchema } from '@modelcontextprotocol/sdk/types.js';
|
|
6
|
+
import { assertMcpToolDefinition, assertMcpToolCatalog, MarAgentError } from '@myagentroam/agent';
|
|
5
7
|
import { ExecutorStdioClientTransport } from './executor-stdio-client-transport.js';
|
|
6
8
|
export class MarAgentMcpClientCache {
|
|
7
9
|
#entries = new Map();
|
|
@@ -21,16 +23,16 @@ export class MarAgentMcpClientCache {
|
|
|
21
23
|
const pool = await MarAgentMcpClientPool.connect(servers, cwd, options);
|
|
22
24
|
return singleUseLease(pool);
|
|
23
25
|
}
|
|
24
|
-
const key = mcpConfigurationFingerprint(servers, cwd);
|
|
26
|
+
const key = mcpConfigurationFingerprint(servers, cwd, options.authorizationScope);
|
|
25
27
|
let entry = this.#entries.get(key);
|
|
26
28
|
if (entry === undefined) {
|
|
27
29
|
await this.#evictIdleEntry();
|
|
28
30
|
if (this.#entries.size >= this.#maxEntries) {
|
|
29
|
-
const pool = await MarAgentMcpClientPool.connect(servers, cwd);
|
|
31
|
+
const pool = await MarAgentMcpClientPool.connect(servers, cwd, options);
|
|
30
32
|
return singleUseLease(pool);
|
|
31
33
|
}
|
|
32
34
|
entry = {
|
|
33
|
-
promise: MarAgentMcpClientPool.connect(servers, cwd),
|
|
35
|
+
promise: MarAgentMcpClientPool.connect(servers, cwd, options),
|
|
34
36
|
references: 0,
|
|
35
37
|
lastReleasedAt: 0
|
|
36
38
|
};
|
|
@@ -103,37 +105,87 @@ export class MarAgentMcpClientCache {
|
|
|
103
105
|
await (await entry.promise).close().catch(() => undefined);
|
|
104
106
|
}
|
|
105
107
|
}
|
|
108
|
+
export const MCP_CATALOG_LIMITS = Object.freeze({
|
|
109
|
+
httpConcurrency: 4,
|
|
110
|
+
retryMs: 30_000,
|
|
111
|
+
timeoutMs: 30_000,
|
|
112
|
+
maximumPages: 256,
|
|
113
|
+
maximumTools: 10_000,
|
|
114
|
+
maximumBytes: 16 * 1024 * 1024
|
|
115
|
+
});
|
|
106
116
|
export class MarAgentMcpClientPool {
|
|
117
|
+
#unknownPrincipal = randomUUID();
|
|
107
118
|
#clients = [];
|
|
119
|
+
#servers = new Map();
|
|
108
120
|
#tools = new Map();
|
|
121
|
+
#snapshot = { revision: '', servers: [], tools: [] };
|
|
122
|
+
#controller = new AbortController();
|
|
123
|
+
#closed = false;
|
|
124
|
+
#ready = false;
|
|
125
|
+
#initialCatalogAvailable = true;
|
|
126
|
+
#httpRefreshActive = 0;
|
|
127
|
+
#httpWaiters = [];
|
|
109
128
|
static async connect(servers, cwd, options = {}) {
|
|
110
129
|
const pool = new MarAgentMcpClientPool();
|
|
111
|
-
|
|
112
|
-
|
|
130
|
+
const http = servers.filter((server) => server.transport === 'HTTP');
|
|
131
|
+
let next = 0;
|
|
132
|
+
const jobs = Array.from({ length: Math.min(MCP_CATALOG_LIMITS.httpConcurrency, http.length) }, async () => {
|
|
133
|
+
while (next < http.length)
|
|
134
|
+
await pool.#connect(http[next++], cwd, options);
|
|
135
|
+
});
|
|
136
|
+
jobs.push((async () => {
|
|
137
|
+
for (const server of servers.filter((server) => server.transport === 'STDIO'))
|
|
113
138
|
await pool.#connect(server, cwd, options);
|
|
139
|
+
})());
|
|
140
|
+
try {
|
|
141
|
+
await Promise.all(jobs);
|
|
142
|
+
pool.#publish();
|
|
143
|
+
pool.#ready = true;
|
|
144
|
+
for (const state of pool.#servers.values())
|
|
145
|
+
if (state.dirty)
|
|
146
|
+
void pool.#refresh(state).catch(() => undefined);
|
|
114
147
|
return pool;
|
|
115
148
|
}
|
|
116
149
|
catch (error) {
|
|
150
|
+
pool.#controller.abort();
|
|
151
|
+
await Promise.allSettled(jobs);
|
|
117
152
|
await pool.close();
|
|
118
153
|
throw error;
|
|
119
154
|
}
|
|
120
155
|
}
|
|
121
156
|
definitions() {
|
|
122
|
-
return
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
}));
|
|
157
|
+
return this.#snapshot.tools.map((tool) => tool.definition);
|
|
158
|
+
}
|
|
159
|
+
catalog() {
|
|
160
|
+
return this.#snapshot;
|
|
127
161
|
}
|
|
128
|
-
async
|
|
162
|
+
async refreshForExecution() {
|
|
163
|
+
if (this.#initialCatalogAvailable) {
|
|
164
|
+
this.#initialCatalogAvailable = false;
|
|
165
|
+
await Promise.all([...this.#servers.values()].flatMap((state) => state.refreshing ? [state.refreshing.catch(() => undefined)] : []));
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
await Promise.all([...this.#servers.values()].map((state) => this.#refresh(state).catch(() => undefined)));
|
|
169
|
+
}
|
|
170
|
+
async call(name, arguments_, signal, expectedTool) {
|
|
129
171
|
const binding = this.#tools.get(name);
|
|
130
|
-
if (!binding)
|
|
131
|
-
throw new
|
|
172
|
+
if (!binding || this.#closed)
|
|
173
|
+
throw new MarAgentError('MAR_AGENT_MCP_TOOL_NOT_FOUND', 'MCP tool is no longer available.');
|
|
174
|
+
if (expectedTool &&
|
|
175
|
+
(binding.metadata.sourceIdentity !== expectedTool.sourceIdentity ||
|
|
176
|
+
binding.metadata.definitionRevision !== expectedTool.definitionRevision))
|
|
177
|
+
throw new MarAgentError('MAR_AGENT_MCP_TOOL_DEFINITION_CHANGED', 'MCP tool binding or definition changed.');
|
|
178
|
+
signal?.throwIfAborted();
|
|
132
179
|
const result = await binding.client.callTool({
|
|
133
180
|
name: binding.nativeName,
|
|
134
181
|
arguments: arguments_ !== null && typeof arguments_ === 'object' && !Array.isArray(arguments_)
|
|
135
182
|
? arguments_
|
|
136
183
|
: {}
|
|
184
|
+
}, undefined, {
|
|
185
|
+
timeout: MCP_CATALOG_LIMITS.timeoutMs,
|
|
186
|
+
signal: signal
|
|
187
|
+
? AbortSignal.any([signal, this.#controller.signal])
|
|
188
|
+
: this.#controller.signal
|
|
137
189
|
});
|
|
138
190
|
const items = Array.isArray(result.content) ? result.content : [];
|
|
139
191
|
const content = items
|
|
@@ -149,12 +201,156 @@ export class MarAgentMcpClientPool {
|
|
|
149
201
|
return { content, data: result.structuredContent ?? items };
|
|
150
202
|
}
|
|
151
203
|
async close() {
|
|
204
|
+
if (this.#closed)
|
|
205
|
+
return;
|
|
206
|
+
this.#closed = true;
|
|
207
|
+
this.#controller.abort();
|
|
208
|
+
await Promise.allSettled([...this.#servers.values()].flatMap((state) => (state.refreshing ? [state.refreshing] : [])));
|
|
152
209
|
await Promise.all(this.#clients.map((client) => client.close().catch(() => undefined)));
|
|
153
210
|
this.#clients.length = 0;
|
|
211
|
+
this.#servers.clear();
|
|
154
212
|
this.#tools.clear();
|
|
213
|
+
this.#snapshot = { revision: '', servers: [], tools: [] };
|
|
214
|
+
}
|
|
215
|
+
#publish(replacement) {
|
|
216
|
+
const combined = new Map();
|
|
217
|
+
for (const state of this.#servers.values())
|
|
218
|
+
for (const [name, binding] of replacement?.state === state
|
|
219
|
+
? replacement.tools
|
|
220
|
+
: state.tools) {
|
|
221
|
+
if (combined.has(name))
|
|
222
|
+
throw new Error('MAR_AGENT_MCP_TOOL_NAME_CONFLICT');
|
|
223
|
+
combined.set(name, binding);
|
|
224
|
+
}
|
|
225
|
+
const tools = [...combined]
|
|
226
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
227
|
+
.map(([, binding]) => binding.metadata);
|
|
228
|
+
const servers = [...this.#servers.values()]
|
|
229
|
+
.map((state) => state.metadata)
|
|
230
|
+
.sort((a, b) => a.id.localeCompare(b.id));
|
|
231
|
+
const revision = fingerprint({ servers, tools });
|
|
232
|
+
assertMcpToolCatalog({ revision, servers, tools });
|
|
233
|
+
if (replacement)
|
|
234
|
+
replacement.state.tools = replacement.tools;
|
|
235
|
+
this.#tools = combined;
|
|
236
|
+
if (revision !== this.#snapshot.revision)
|
|
237
|
+
this.#snapshot = { revision, servers, tools };
|
|
238
|
+
}
|
|
239
|
+
#refresh(state) {
|
|
240
|
+
if (state.refreshing)
|
|
241
|
+
return state.refreshing;
|
|
242
|
+
if (this.#closed || Date.now() < state.retryAt)
|
|
243
|
+
return Promise.resolve();
|
|
244
|
+
const job = (async () => {
|
|
245
|
+
do {
|
|
246
|
+
state.dirty = false;
|
|
247
|
+
const tools = await this.#enumerate(state);
|
|
248
|
+
if (this.#closed)
|
|
249
|
+
return;
|
|
250
|
+
this.#publish({ state, tools });
|
|
251
|
+
state.retryAt = 0;
|
|
252
|
+
} while (state.dirty && !this.#closed);
|
|
253
|
+
})()
|
|
254
|
+
.catch((error) => {
|
|
255
|
+
state.retryAt = Date.now() + MCP_CATALOG_LIMITS.retryMs;
|
|
256
|
+
throw error;
|
|
257
|
+
})
|
|
258
|
+
.finally(() => {
|
|
259
|
+
delete state.refreshing;
|
|
260
|
+
});
|
|
261
|
+
state.refreshing = job;
|
|
262
|
+
return job;
|
|
263
|
+
}
|
|
264
|
+
async #enumerate(state) {
|
|
265
|
+
if (state.server.transport !== 'HTTP')
|
|
266
|
+
return this.#enumeratePages(state);
|
|
267
|
+
if (this.#httpRefreshActive < MCP_CATALOG_LIMITS.httpConcurrency)
|
|
268
|
+
this.#httpRefreshActive++;
|
|
269
|
+
else
|
|
270
|
+
await new Promise((resolve) => this.#httpWaiters.push(resolve));
|
|
271
|
+
try {
|
|
272
|
+
this.#controller.signal.throwIfAborted();
|
|
273
|
+
return await this.#enumeratePages(state);
|
|
274
|
+
}
|
|
275
|
+
finally {
|
|
276
|
+
const next = this.#httpWaiters.shift();
|
|
277
|
+
if (next)
|
|
278
|
+
next();
|
|
279
|
+
else
|
|
280
|
+
this.#httpRefreshActive--;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
async #enumeratePages(state) {
|
|
284
|
+
const tools = new Map();
|
|
285
|
+
const cursors = new Set();
|
|
286
|
+
let cursor;
|
|
287
|
+
let bytes = 0;
|
|
288
|
+
for (let page = 0; page < MCP_CATALOG_LIMITS.maximumPages; page++) {
|
|
289
|
+
const listed = await state.client.listTools(cursor === undefined ? {} : { cursor }, {
|
|
290
|
+
signal: this.#controller.signal,
|
|
291
|
+
timeout: MCP_CATALOG_LIMITS.timeoutMs
|
|
292
|
+
});
|
|
293
|
+
bytes += Buffer.byteLength(JSON.stringify(listed));
|
|
294
|
+
if (bytes > MCP_CATALOG_LIMITS.maximumBytes)
|
|
295
|
+
throw new Error('MAR_AGENT_MCP_CATALOG_LIMIT');
|
|
296
|
+
for (const tool of listed.tools) {
|
|
297
|
+
const name = `mcp__${safeName(state.server.name)}__${safeName(tool.name)}`;
|
|
298
|
+
if (tools.has(name))
|
|
299
|
+
throw new Error('MAR_AGENT_MCP_TOOL_NAME_CONFLICT');
|
|
300
|
+
const definition = {
|
|
301
|
+
name,
|
|
302
|
+
description: tool.description ?? `MCP tool ${tool.name}.`,
|
|
303
|
+
allowedCallers: [
|
|
304
|
+
...(state.server.allowedToolCallers?.[tool.name] ??
|
|
305
|
+
(state.server.origin === 'BUILTIN' ? ['direct'] : ['direct', 'programmatic']))
|
|
306
|
+
],
|
|
307
|
+
inputSchema: tool.inputSchema,
|
|
308
|
+
...(tool.outputSchema
|
|
309
|
+
? { outputSchema: tool.outputSchema }
|
|
310
|
+
: {})
|
|
311
|
+
};
|
|
312
|
+
assertMcpToolDefinition(definition);
|
|
313
|
+
const metadata = {
|
|
314
|
+
definition,
|
|
315
|
+
serverId: state.metadata.id,
|
|
316
|
+
rawName: tool.name,
|
|
317
|
+
sourceIdentity: state.sourceIdentity,
|
|
318
|
+
definitionRevision: fingerprint(definition),
|
|
319
|
+
lifetime: state.server.lifecycle === 'EXECUTION' ? 'execution' : 'session',
|
|
320
|
+
exposure: state.server.origin === 'BUILTIN' ? 'direct' : 'deferred',
|
|
321
|
+
...(tool.annotations ? { annotations: tool.annotations } : {})
|
|
322
|
+
};
|
|
323
|
+
tools.set(name, {
|
|
324
|
+
client: state.client,
|
|
325
|
+
nativeName: tool.name,
|
|
326
|
+
metadata
|
|
327
|
+
});
|
|
328
|
+
if (tools.size > MCP_CATALOG_LIMITS.maximumTools)
|
|
329
|
+
throw new Error('MAR_AGENT_MCP_CATALOG_LIMIT');
|
|
330
|
+
}
|
|
331
|
+
cursor = listed.nextCursor;
|
|
332
|
+
if (cursor === undefined)
|
|
333
|
+
return tools;
|
|
334
|
+
if (cursors.has(cursor))
|
|
335
|
+
throw new Error('MAR_AGENT_MCP_CATALOG_CURSOR_INVALID');
|
|
336
|
+
cursors.add(cursor);
|
|
337
|
+
}
|
|
338
|
+
throw new Error('MAR_AGENT_MCP_CATALOG_LIMIT');
|
|
155
339
|
}
|
|
156
340
|
async #connect(server, cwd, options) {
|
|
157
341
|
const client = new Client({ name: 'maragent', version: '1.0.0' });
|
|
342
|
+
this.#clients.push(client);
|
|
343
|
+
let state;
|
|
344
|
+
let initialDirty = false;
|
|
345
|
+
client.setNotificationHandler(ToolListChangedNotificationSchema, () => {
|
|
346
|
+
if (!state) {
|
|
347
|
+
initialDirty = true;
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
state.dirty = true;
|
|
351
|
+
if (this.#ready)
|
|
352
|
+
void this.#refresh(state).catch(() => undefined);
|
|
353
|
+
});
|
|
158
354
|
const environment = { ...getDefaultEnvironment(), ...(server.environment ?? {}) };
|
|
159
355
|
const transport = server.transport === 'STDIO'
|
|
160
356
|
? options.processExecutor === undefined
|
|
@@ -177,22 +373,49 @@ export class MarAgentMcpClientPool {
|
|
|
177
373
|
: new StreamableHTTPClientTransport(new URL(server.url), {
|
|
178
374
|
requestInit: { headers: { ...(server.headers ?? {}) } }
|
|
179
375
|
});
|
|
180
|
-
await client.connect(transport
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
376
|
+
await client.connect(transport, {
|
|
377
|
+
signal: this.#controller.signal,
|
|
378
|
+
timeout: MCP_CATALOG_LIMITS.timeoutMs
|
|
379
|
+
});
|
|
380
|
+
const sourceIdentity = fingerprint({
|
|
381
|
+
source: server.sourceIdentity ?? {
|
|
382
|
+
name: server.name,
|
|
383
|
+
transport: server.transport,
|
|
384
|
+
url: server.url,
|
|
385
|
+
command: server.command,
|
|
386
|
+
args: server.args
|
|
387
|
+
},
|
|
388
|
+
cwd,
|
|
389
|
+
allowedToolCallers: server.allowedToolCallers,
|
|
390
|
+
authorizationScope: options.authorizationScope ?? cwd,
|
|
391
|
+
principal: server.authorizationIdentity ?? this.#unknownPrincipal
|
|
392
|
+
});
|
|
393
|
+
const version = client.getServerVersion();
|
|
394
|
+
state = {
|
|
395
|
+
server,
|
|
396
|
+
client,
|
|
397
|
+
sourceIdentity,
|
|
398
|
+
retryAt: 0,
|
|
399
|
+
dirty: initialDirty,
|
|
400
|
+
tools: new Map(),
|
|
401
|
+
metadata: {
|
|
402
|
+
id: sourceIdentity,
|
|
403
|
+
name: server.name,
|
|
404
|
+
...(version?.title ? { description: version.title } : {}),
|
|
405
|
+
...(client.getInstructions() ? { instructions: client.getInstructions() } : {})
|
|
406
|
+
}
|
|
407
|
+
};
|
|
408
|
+
state.tools = await this.#enumerate(state);
|
|
409
|
+
if (this.#servers.has(state.metadata.id))
|
|
410
|
+
throw new Error('MAR_AGENT_MCP_SERVER_NAME_CONFLICT');
|
|
411
|
+
this.#servers.set(state.metadata.id, state);
|
|
194
412
|
}
|
|
195
413
|
}
|
|
414
|
+
function fingerprint(value) {
|
|
415
|
+
return createHash('sha256')
|
|
416
|
+
.update(JSON.stringify(canonicalValue(value)))
|
|
417
|
+
.digest('hex');
|
|
418
|
+
}
|
|
196
419
|
function singleUseLease(pool) {
|
|
197
420
|
let released = false;
|
|
198
421
|
return {
|
|
@@ -205,10 +428,11 @@ function singleUseLease(pool) {
|
|
|
205
428
|
}
|
|
206
429
|
};
|
|
207
430
|
}
|
|
208
|
-
function mcpConfigurationFingerprint(servers, cwd) {
|
|
431
|
+
function mcpConfigurationFingerprint(servers, cwd, authorizationScope) {
|
|
209
432
|
return createHash('sha256')
|
|
210
433
|
.update(JSON.stringify(canonicalValue({
|
|
211
434
|
cwd,
|
|
435
|
+
authorizationScope,
|
|
212
436
|
inheritedEnvironment: getDefaultEnvironment(),
|
|
213
437
|
servers
|
|
214
438
|
})))
|
|
@@ -115,7 +115,7 @@ export class MarAgentRuntimeModelFactory {
|
|
|
115
115
|
}
|
|
116
116
|
: {}),
|
|
117
117
|
responsesEncoding: configuration.responsesEncoding ?? 'STANDARD',
|
|
118
|
-
|
|
118
|
+
responsesConfigurationUpdates: configuration.responsesConfigurationUpdates === true,
|
|
119
119
|
credentialProvider: provider
|
|
120
120
|
};
|
|
121
121
|
});
|
|
@@ -296,6 +296,9 @@ export class MarAgentRunner extends AbstractRunner {
|
|
|
296
296
|
await catalog.close().catch(() => undefined);
|
|
297
297
|
}
|
|
298
298
|
}
|
|
299
|
+
supportsRollback(session) {
|
|
300
|
+
return session.externalSessionId !== null;
|
|
301
|
+
}
|
|
299
302
|
async clearNative(session) {
|
|
300
303
|
if (session.externalSessionId === null)
|
|
301
304
|
throw new Error('SESSION_CLEAR_UNAVAILABLE');
|
|
@@ -116,7 +116,8 @@ export class OpenCodeManagedRunController {
|
|
|
116
116
|
mcpSecretEnvironment = parseSecretEnvironment(payload.mcpSecretEnvironment);
|
|
117
117
|
mcp = this.runner.mcpConfiguration(payload.mcpInstallations, {
|
|
118
118
|
...environment,
|
|
119
|
-
...mcpSecretEnvironment
|
|
119
|
+
...mcpSecretEnvironment,
|
|
120
|
+
MAR_USERNAME: environment['MAR_USERNAME'] ?? ''
|
|
120
121
|
});
|
|
121
122
|
attachments = this.host.parseAttachments(payload.attachments);
|
|
122
123
|
}
|
|
@@ -384,8 +384,10 @@ export class ContinuousRunCoordinator {
|
|
|
384
384
|
if (lease !== undefined)
|
|
385
385
|
this.environmentLeases.set(nextId, lease);
|
|
386
386
|
}
|
|
387
|
-
removeGoalQueue(sessionId, preservedGoal) {
|
|
387
|
+
removeGoalQueue(sessionId, preservedGoal, preservedSubmissionId) {
|
|
388
388
|
for (const [id, pending] of this.preparedInputs) {
|
|
389
|
+
if (id === preservedSubmissionId)
|
|
390
|
+
continue;
|
|
389
391
|
if (pending.sessionId !== sessionId || pending.goal === undefined)
|
|
390
392
|
continue;
|
|
391
393
|
if (pending.goal.id === preservedGoal?.id && pending.goal.revision === preservedGoal.revision)
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { discoverNativeSessions, readNativeSession } from '../native-session-history.js';
|
|
2
|
-
import { coalesceImageGenerationItems } from '../runner/codex/conversation-parser.js';
|
|
3
2
|
import { nativeConversationPage } from '../util/runner-native-session-parsers.js';
|
|
4
3
|
import { isPlainRecord } from '../util/node-operation-parsers.js';
|
|
5
4
|
import { nodeLog } from '../operational.js';
|
|
@@ -152,7 +151,28 @@ export class ConversationHistoryService {
|
|
|
152
151
|
0);
|
|
153
152
|
}
|
|
154
153
|
async readHistory(session, input, snapshotSequence, readAt) {
|
|
155
|
-
|
|
154
|
+
let selected;
|
|
155
|
+
try {
|
|
156
|
+
selected = await this.readRunnerHistory(session, input);
|
|
157
|
+
}
|
|
158
|
+
catch (error) {
|
|
159
|
+
// An unavailable persistent source must not erase accepted live output.
|
|
160
|
+
// Older-page and external-session errors remain visible to the caller.
|
|
161
|
+
if (input.cursor !== undefined ||
|
|
162
|
+
this.options.runtime.getAgentSession(session.id) === undefined)
|
|
163
|
+
throw error;
|
|
164
|
+
const runtimePage = this.options.runtime.listConversationTurns({
|
|
165
|
+
sessionId: session.id,
|
|
166
|
+
...input
|
|
167
|
+
});
|
|
168
|
+
if (runtimePage.turns.length === 0)
|
|
169
|
+
throw error;
|
|
170
|
+
return this.page(runtimePage.nextCursor, runtimePage.turns, {
|
|
171
|
+
snapshotSequence,
|
|
172
|
+
source: 'runtime',
|
|
173
|
+
readAt
|
|
174
|
+
});
|
|
175
|
+
}
|
|
156
176
|
if (selected.page?.source === 'native')
|
|
157
177
|
return this.page(selected.page.nextCursor, selected.page.turns, {
|
|
158
178
|
snapshotSequence,
|
|
@@ -179,7 +199,7 @@ export class ConversationHistoryService {
|
|
|
179
199
|
if (narrowed !== undefined)
|
|
180
200
|
official = { ...narrowed, source: 'official' };
|
|
181
201
|
}
|
|
182
|
-
const officialTurns =
|
|
202
|
+
const officialTurns = official.turns;
|
|
183
203
|
const runtimeTurnsByOfficialId = new Map();
|
|
184
204
|
const runtimeSupplementalItemsByOfficialId = new Map();
|
|
185
205
|
for (const officialTurn of officialTurns) {
|
|
@@ -261,7 +281,9 @@ export class ConversationHistoryService {
|
|
|
261
281
|
readAt
|
|
262
282
|
});
|
|
263
283
|
}
|
|
264
|
-
const page = this.options.runtime.
|
|
284
|
+
const page = this.options.runtime.getAgentSession(session.id) === undefined
|
|
285
|
+
? { turns: [], nextCursor: null }
|
|
286
|
+
: this.options.runtime.listConversationTurns({ sessionId: session.id, ...input });
|
|
265
287
|
if (session.externalSessionId !== null &&
|
|
266
288
|
session.nativeControl === 'EXTERNAL' &&
|
|
267
289
|
page.turns.length === 0)
|
|
@@ -308,6 +330,7 @@ export class ConversationHistoryService {
|
|
|
308
330
|
: input, runtimeTurns, (images) => this.options.attachments.registerNative(session, images));
|
|
309
331
|
return native;
|
|
310
332
|
},
|
|
333
|
+
session,
|
|
311
334
|
official: () => this.readOfficial(session, input)
|
|
312
335
|
});
|
|
313
336
|
return { page, incompleteNative };
|
|
@@ -327,9 +350,7 @@ export class ConversationHistoryService {
|
|
|
327
350
|
const history = selected.page;
|
|
328
351
|
if (history === undefined)
|
|
329
352
|
return this.page(null, [], { snapshotSequence, source: 'runtime', readAt });
|
|
330
|
-
const hydratedTurns = history.
|
|
331
|
-
? await this.attachNativeImages(session, history.turns)
|
|
332
|
-
: history.turns;
|
|
353
|
+
const hydratedTurns = history.turns;
|
|
333
354
|
const turns = hydratedTurns.filter((turn) => latestTurnActivityAt(turn) < beforeAt);
|
|
334
355
|
return this.page(history.nextCursor === null
|
|
335
356
|
? null
|
|
@@ -421,45 +442,13 @@ export class ConversationHistoryService {
|
|
|
421
442
|
await discoverNativeSessions(session.runner, session.cwd);
|
|
422
443
|
return readNativeSession(session.runner, session.cwd, session.externalSessionId, page, limit);
|
|
423
444
|
}
|
|
424
|
-
async attachNativeImages(session, turns) {
|
|
425
|
-
const startedAt = performance.now();
|
|
426
|
-
const history = await this.readNative(session);
|
|
427
|
-
const readDurationMs = Math.round(performance.now() - startedAt);
|
|
428
|
-
if (readDurationMs >= 250)
|
|
429
|
-
nodeLog('native.session.history.attachment-source-read.completed', {
|
|
430
|
-
runner: session.runner,
|
|
431
|
-
sessionId: session.id,
|
|
432
|
-
durationMs: readDurationMs
|
|
433
|
-
});
|
|
434
|
-
if (history === undefined)
|
|
435
|
-
return turns;
|
|
436
|
-
const attachments = new Map();
|
|
437
|
-
for (const item of history.items) {
|
|
438
|
-
if (item.kind !== 'tool_call' || item.imageAttachments === undefined)
|
|
439
|
-
continue;
|
|
440
|
-
const registered = this.options.attachments.registerNative(session, item.imageAttachments);
|
|
441
|
-
if (registered.length > 0)
|
|
442
|
-
attachments.set(item.toolUseId, registered);
|
|
443
|
-
}
|
|
444
|
-
if (attachments.size === 0)
|
|
445
|
-
return turns;
|
|
446
|
-
return turns.map((turn) => ({
|
|
447
|
-
...turn,
|
|
448
|
-
items: coalesceImageGenerationItems(turn.items.map((item) => {
|
|
449
|
-
const registered = attachments.get(item.id.slice(item.id.lastIndexOf(':') + 1));
|
|
450
|
-
if (registered === undefined || !isPlainRecord(item.payload))
|
|
451
|
-
return item;
|
|
452
|
-
return { ...item, payload: { ...item.payload, attachments: registered } };
|
|
453
|
-
}))
|
|
454
|
-
}));
|
|
455
|
-
}
|
|
456
445
|
async readOfficial(session, input) {
|
|
457
446
|
const runner = this.options.runners.require(session.runner);
|
|
458
447
|
if (session.externalSessionId === null || !runner.available(this.options.capabilities()))
|
|
459
448
|
return undefined;
|
|
460
449
|
const startedAt = performance.now();
|
|
461
450
|
try {
|
|
462
|
-
return await runner.readOfficialConversation(session, input, (nativeTurnId) => this.managedRunnerTurn(session, nativeTurnId));
|
|
451
|
+
return await runner.readOfficialConversation(session, input, (nativeTurnId) => this.managedRunnerTurn(session, nativeTurnId), (images) => this.options.attachments.registerNative(session, images));
|
|
463
452
|
}
|
|
464
453
|
finally {
|
|
465
454
|
const durationMs = Math.round(performance.now() - startedAt);
|
|
@@ -519,7 +519,7 @@ export class HostedConversationNodeService {
|
|
|
519
519
|
temporaryDirectory,
|
|
520
520
|
pathMounts,
|
|
521
521
|
processExecutor: this.processExecutor(paths, version, temporaryDirectory, generatedImagesDirectory, generatedImagesLogicalPath),
|
|
522
|
-
mcpServers: mcpServers ?? resolveMcpServers(version, this.options.secretEnvironment ?? process.env),
|
|
522
|
+
mcpServers: (mcpServers ?? resolveMcpServers(version, this.options.secretEnvironment ?? process.env)).map((server) => ({ ...server, authorizationIdentity: `user:${payload.userId}` })),
|
|
523
523
|
webAccess: version.draft.networkMode !== 'NONE',
|
|
524
524
|
onEvent: (event) => {
|
|
525
525
|
if (runtimeEventsAccepted)
|
|
@@ -155,8 +155,13 @@ export class NativeSessionWatchService {
|
|
|
155
155
|
setTimeout(() => void this.convergeHistory(watch, generation), 0);
|
|
156
156
|
}
|
|
157
157
|
for (const lease of leases) {
|
|
158
|
-
if (!lease.ready)
|
|
159
|
-
|
|
158
|
+
if (!lease.ready) {
|
|
159
|
+
// Observations are useful only to an already watching page, never after a snapshot.
|
|
160
|
+
if (event.category !== 'conversation' ||
|
|
161
|
+
!isPlainRecord(event.payload) ||
|
|
162
|
+
event.payload.performance === undefined)
|
|
163
|
+
lease.pendingEvents.push(event);
|
|
164
|
+
}
|
|
160
165
|
else
|
|
161
166
|
this.emitLeaseEvent(lease, event);
|
|
162
167
|
}
|
|
@@ -301,8 +306,12 @@ export class NativeSessionWatchService {
|
|
|
301
306
|
if (changed || watch.page === undefined)
|
|
302
307
|
watch.page = accepted;
|
|
303
308
|
stage = 'emit_page';
|
|
304
|
-
if (completingConvergence && accepted.source === 'runtime')
|
|
305
|
-
|
|
309
|
+
if (completingConvergence && accepted.source === 'runtime') {
|
|
310
|
+
if (changed)
|
|
311
|
+
this.emitSnapshotToLeases(session, accepted, true);
|
|
312
|
+
else
|
|
313
|
+
this.releasePendingEvents(session.id);
|
|
314
|
+
}
|
|
306
315
|
else if (completingConvergence)
|
|
307
316
|
this.emitSnapshotToLeases(session, accepted, true);
|
|
308
317
|
else if (changed)
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { runPerformanceSchema } from '@myagentroam/protocol';
|
|
1
2
|
import { nodeLog } from '../operational.js';
|
|
2
3
|
import { isPlainRecord, isTerminalRunStatus } from '../util/node-operation-parsers.js';
|
|
3
4
|
const COALESCE_MS = 200;
|
|
@@ -9,6 +10,18 @@ export class RunEventService {
|
|
|
9
10
|
this.options = options;
|
|
10
11
|
}
|
|
11
12
|
emit(runId, eventType, payload, status) {
|
|
13
|
+
if (eventType === 'run.performance') {
|
|
14
|
+
const run = this.options.runtime.getRun(runId);
|
|
15
|
+
if (run === undefined || !isPlainRecord(payload))
|
|
16
|
+
return;
|
|
17
|
+
const parsed = runPerformanceSchema.safeParse({ ...payload, runId: run.id });
|
|
18
|
+
if (parsed.success)
|
|
19
|
+
this.options.emitWorkbench('conversation', {
|
|
20
|
+
sessionId: run.sessionId,
|
|
21
|
+
performance: parsed.data
|
|
22
|
+
});
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
12
25
|
if (eventType === 'text.delta' &&
|
|
13
26
|
status === undefined &&
|
|
14
27
|
isPlainRecord(payload) &&
|
|
@@ -115,7 +115,8 @@ export class SessionCatalogService {
|
|
|
115
115
|
(await this.options.projection.resolve(input.sessionId));
|
|
116
116
|
if (source === undefined)
|
|
117
117
|
throw new Error('SESSION_NOT_FOUND');
|
|
118
|
-
|
|
118
|
+
const runner = this.options.runners.require(source.runner);
|
|
119
|
+
if (!runner.supportsRollback(source))
|
|
119
120
|
throw new Error('SESSION_ROLLBACK_UNAVAILABLE');
|
|
120
121
|
if (this.options.runtime
|
|
121
122
|
.listRunsForSession(source.id)
|
|
@@ -132,9 +133,7 @@ export class SessionCatalogService {
|
|
|
132
133
|
throw new Error('SESSION_ROLLBACK_TARGET_INVALID');
|
|
133
134
|
const releaseMutation = this.options.runtime.beginSessionHistoryMutation(source.id);
|
|
134
135
|
try {
|
|
135
|
-
const lastActivityAt = await
|
|
136
|
-
.require(source.runner)
|
|
137
|
-
.rollbackNative(source, input.boundary);
|
|
136
|
+
const lastActivityAt = await runner.rollbackNative(source, input.boundary, turn.rewind?.after);
|
|
138
137
|
const runtimeSession = this.options.runtime.getAgentSession(source.id);
|
|
139
138
|
const session = runtimeSession === undefined
|
|
140
139
|
? { ...source, lastActivityAt }
|
|
@@ -100,10 +100,12 @@ export class SessionMessageService {
|
|
|
100
100
|
idempotent: true,
|
|
101
101
|
auditReceipt: auditReceipt(session, existing.kind === 'queue' ? 'QUEUE' : 'SEND', existing.run.id, existing.kind === 'queue' ? existing.item.id : undefined)
|
|
102
102
|
};
|
|
103
|
-
const goalMessage = internalGoal || internalSchedule
|
|
103
|
+
const goalMessage = internalGoal || internalSchedule
|
|
104
|
+
? undefined
|
|
105
|
+
: this.options.goalMessage(session, content, requestedIntent);
|
|
104
106
|
const runnerContent = goalMessage?.runnerContent ?? internalRunnerContent;
|
|
105
107
|
const publicContent = goalMessage?.activated === true ? publicGoalStartText(content) : content;
|
|
106
|
-
const intent = goalMessage ===
|
|
108
|
+
const intent = goalMessage?.activated === true ? 'REPLACE_CURRENT' : requestedIntent;
|
|
107
109
|
const operation = (async () => {
|
|
108
110
|
const acceptedSession = accepted.session;
|
|
109
111
|
const session = this.options.runtime.getAgentSession(acceptedSession.id) ?? acceptedSession;
|
|
@@ -330,9 +330,7 @@ export function withNonImageAttachmentPrompt(input, paths) {
|
|
|
330
330
|
.join('\n')}`;
|
|
331
331
|
}
|
|
332
332
|
export function isCanonicalBase64(value) {
|
|
333
|
-
return
|
|
334
|
-
value.length % 4 === 0 &&
|
|
335
|
-
/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/.test(value));
|
|
333
|
+
return value.length > 0 && value.length % 4 === 0 && /^[A-Za-z0-9+/]+={0,2}$/.test(value);
|
|
336
334
|
}
|
|
337
335
|
export function validatedCapabilities(value) {
|
|
338
336
|
return nodeCapabilitiesSchema.parse(value);
|