@myagentroam/node 0.10.6 → 0.10.8
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/connector.js +60 -5
- package/dist/database.js +3 -1
- package/dist/migrations/v017.js +46 -0
- package/dist/native-session-history.js +7 -15
- package/dist/runner/abstract-runner.js +4 -0
- package/dist/runner/claude/managed-run-controller.js +15 -3
- package/dist/runner/claude/tool-normalizer.js +100 -6
- package/dist/runner/codex/conversation-parser.js +41 -30
- package/dist/runner/codex/managed-run-controller.js +12 -1
- package/dist/runner/codex/tool-normalizer.js +5 -2
- package/dist/runner/mar-agent/compact-extensions.js +1 -0
- package/dist/runner/mar-agent/conversation-normalizer.js +43 -78
- package/dist/runner/mar-agent/hosted-conversation-runtime.js +27 -0
- package/dist/runner/mar-agent/managed-run-controller.js +13 -1
- package/dist/runner/mar-agent-runner.js +15 -0
- package/dist/runner/opencode/conversation-parser.js +49 -0
- package/dist/runner/opencode/managed-run-controller.js +12 -0
- package/dist/service/continuous-run-coordinator.js +1 -0
- package/dist/service/hosted-conversation-node-service.js +29 -2
- package/dist/service/model-usage-outbox.js +167 -0
- package/dist/service/model-usage-service.js +61 -0
- package/dist/service/node-control-message-service.js +7 -2
- package/dist/service/node-request-service.js +3 -1
- package/dist/service/node-run-event-bridge.js +6 -0
- package/dist/service/runner-usage-tracker.js +62 -0
- package/dist/service/session-command-service.js +4 -0
- package/dist/service/session-message-service.js +6 -0
- package/dist/service/session-query-service.js +16 -0
- package/dist/service/workspace-change-service.js +2 -0
- package/dist/util/node-operation-parsers.js +0 -21
- package/dist/util/runner-native-session-parsers.js +9 -4
- package/dist/workspace.js +20 -1
- package/package.json +5 -5
package/dist/connector.js
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { normalizeMarAgentUsage } from './runner/mar-agent/conversation-normalizer.js';
|
|
2
|
+
import { ModelUsageOutbox } from './service/model-usage-outbox.js';
|
|
3
|
+
import { NodeModelUsageService } from './service/model-usage-service.js';
|
|
1
4
|
import { dirname, resolve } from 'node:path';
|
|
2
5
|
import { createEnvelope, composerVisibleText, directNodeCommandSchema } from '@myagentroam/protocol';
|
|
3
6
|
import { hostedConversationStorePath, loadNodeConfig, computerUseRuntimeExecutable, directTransferUdpPortRange, nodeDataDirectory, nodeDatabasePath } from './config.js';
|
|
@@ -125,6 +128,7 @@ const HOSTED_CONVERSATION_OPERATIONS = [
|
|
|
125
128
|
'conversation.hosted.config.update',
|
|
126
129
|
'conversation.hosted.metadata.update',
|
|
127
130
|
'conversation.hosted.image.read',
|
|
131
|
+
'conversation.hosted.tool-resource.read',
|
|
128
132
|
'conversation.hosted.files.list',
|
|
129
133
|
'conversation.hosted.file.read',
|
|
130
134
|
'conversation.hosted.file.delete',
|
|
@@ -157,6 +161,8 @@ function upgradeMetadata(metadata, status) {
|
|
|
157
161
|
return next;
|
|
158
162
|
}
|
|
159
163
|
export class NodeConnector {
|
|
164
|
+
usageOutbox;
|
|
165
|
+
modelUsage = new NodeModelUsageService(() => this.usageOutbox, () => this.config?.nodeId);
|
|
160
166
|
nodeGeneration = crypto.randomUUID().replace(/-/g, '');
|
|
161
167
|
controlChannel;
|
|
162
168
|
controlMessages;
|
|
@@ -417,6 +423,7 @@ export class NodeConnector {
|
|
|
417
423
|
void this.enqueueGoalContinuation({
|
|
418
424
|
sessionId: retry.sessionId,
|
|
419
425
|
goal: pending.goal,
|
|
426
|
+
...(pending.usageGrant === undefined ? {} : { usageGrant: pending.usageGrant }),
|
|
420
427
|
secretEnvironment: pending.secretEnvironment,
|
|
421
428
|
...(pending.personalInstructions === undefined
|
|
422
429
|
? {}
|
|
@@ -442,6 +449,7 @@ export class NodeConnector {
|
|
|
442
449
|
retry.timer.unref();
|
|
443
450
|
},
|
|
444
451
|
releaseExecution: (executionId) => {
|
|
452
|
+
this.modelUsage.release(executionId);
|
|
445
453
|
this.goalExecutionProgress.delete(executionId);
|
|
446
454
|
this.goalMcpServer.revoke(executionId);
|
|
447
455
|
this.scheduledTaskMcpServer.revoke(executionId);
|
|
@@ -797,6 +805,7 @@ export class NodeConnector {
|
|
|
797
805
|
...(run?.initiatedByUserId === undefined
|
|
798
806
|
? {}
|
|
799
807
|
: { initiatedByUserId: run.initiatedByUserId }),
|
|
808
|
+
...(input.usageGrant === undefined ? {} : { usageGrant: input.usageGrant }),
|
|
800
809
|
secretEnvironment: input.secretEnvironment,
|
|
801
810
|
...(input.personalInstructions === undefined
|
|
802
811
|
? {}
|
|
@@ -986,7 +995,7 @@ export class NodeConnector {
|
|
|
986
995
|
emitSession: (session) => this.emitSessionWorkbenchEvent({
|
|
987
996
|
session: this.sessionPresentationService.present(session)
|
|
988
997
|
}),
|
|
989
|
-
execute: (operation, data) => this.executeNodeOperation(operation, data),
|
|
998
|
+
execute: (operation, data) => this.modelUsage.withGrant(isPlainRecord(data) ? data.__usageGrant : undefined, () => this.executeNodeOperation(operation, data)),
|
|
990
999
|
interceptedSessionId: (clientMessageId) => this.fakeRunner === undefined ? undefined : `fake:${clientMessageId}`
|
|
991
1000
|
});
|
|
992
1001
|
this.externalSessionResumeService = new ExternalSessionResumeService({
|
|
@@ -1044,7 +1053,7 @@ export class NodeConnector {
|
|
|
1044
1053
|
goalAccessChanged: prepared.changed
|
|
1045
1054
|
};
|
|
1046
1055
|
},
|
|
1047
|
-
goal: async ({ session, objective, initiatedByUserId, secretEnvironment, personalInstructions, marAgentModel, marAgentModels, marAgentCompact, marAgentRuntimeScopeId }) => {
|
|
1056
|
+
goal: async ({ session, objective, initiatedByUserId, secretEnvironment, personalInstructions, marAgentModel, marAgentModels, marAgentCompact, marAgentRuntimeScopeId, usageGrant }) => {
|
|
1048
1057
|
this.invalidateGoalContinuation(session.id);
|
|
1049
1058
|
const prepared = this.prepareGoalSession(session);
|
|
1050
1059
|
const goalSession = prepared.session;
|
|
@@ -1064,6 +1073,7 @@ export class NodeConnector {
|
|
|
1064
1073
|
objective: goal.objective,
|
|
1065
1074
|
deliveryIntent: replacing ? 'REPLACE_CURRENT' : 'SEND',
|
|
1066
1075
|
goal: goalRef,
|
|
1076
|
+
...(usageGrant === undefined ? {} : { usageGrant }),
|
|
1067
1077
|
...(initiatedByUserId === undefined ? {} : { initiatedByUserId }),
|
|
1068
1078
|
secretEnvironment,
|
|
1069
1079
|
...(personalInstructions === undefined ? {} : { personalInstructions }),
|
|
@@ -1283,6 +1293,7 @@ export class NodeConnector {
|
|
|
1283
1293
|
onReconnectScheduled: () => this.metrics.reconnectScheduled(),
|
|
1284
1294
|
onMessage: (raw) => this.handleMessage(raw),
|
|
1285
1295
|
onClose: (code, reason) => {
|
|
1296
|
+
this.usageOutbox?.disconnected();
|
|
1286
1297
|
this.directTransferService?.close();
|
|
1287
1298
|
this.runtimeCredentials.disconnected();
|
|
1288
1299
|
this.chatgptLogins.close();
|
|
@@ -1300,8 +1311,24 @@ export class NodeConnector {
|
|
|
1300
1311
|
});
|
|
1301
1312
|
this.controlMessages = new NodeControlMessageService({
|
|
1302
1313
|
config: () => this.config,
|
|
1303
|
-
register: (nodeId, credential) =>
|
|
1304
|
-
|
|
1314
|
+
register: async (nodeId, credential, payload) => {
|
|
1315
|
+
await this.connectionLifecycle.register(nodeId, credential);
|
|
1316
|
+
this.usageOutbox?.connected(payload);
|
|
1317
|
+
},
|
|
1318
|
+
usageAck: (payload) => this.usageOutbox?.handle(createEnvelope('usage.ack', payload)),
|
|
1319
|
+
bindUsage: (payload) => {
|
|
1320
|
+
if (!isPlainRecord(payload) ||
|
|
1321
|
+
typeof payload.runId !== 'string' ||
|
|
1322
|
+
typeof payload.sessionId !== 'string')
|
|
1323
|
+
return;
|
|
1324
|
+
const runner = this.runtime.getAgentSession(payload.sessionId)?.runner;
|
|
1325
|
+
if (runner)
|
|
1326
|
+
this.modelUsage.bind(payload.runId, payload.usageGrant, payload.sessionId, runner);
|
|
1327
|
+
},
|
|
1328
|
+
connected: (payload) => {
|
|
1329
|
+
this.usageOutbox?.connected(payload);
|
|
1330
|
+
this.connectionLifecycle.connected();
|
|
1331
|
+
},
|
|
1305
1332
|
close: (code, reason) => this.controlChannel.close(code, reason),
|
|
1306
1333
|
inspectWorkspace: (envelope) => this.workspaceCoordinator.inspect(envelope),
|
|
1307
1334
|
handleNodeRequest: (envelope) => this.nodeRequests.handle(envelope),
|
|
@@ -1323,7 +1350,7 @@ export class NodeConnector {
|
|
|
1323
1350
|
this.nodeRequests = new NodeRequestService({
|
|
1324
1351
|
nodeId: () => this.config?.nodeId,
|
|
1325
1352
|
databaseAvailable: () => this.database !== undefined,
|
|
1326
|
-
execute: (operation, data) => this.executeNodeOperation(operation, data),
|
|
1353
|
+
execute: (operation, data) => this.modelUsage.withGrant(isPlainRecord(data) ? data.__usageGrant : undefined, () => this.executeNodeOperation(operation, data)),
|
|
1327
1354
|
send: (type, payload, replyTo) => this.send(type, payload, replyTo)
|
|
1328
1355
|
});
|
|
1329
1356
|
this.workspaceCoordinator = new NodeWorkspaceCoordinator({
|
|
@@ -1336,6 +1363,13 @@ export class NodeConnector {
|
|
|
1336
1363
|
send: (type, payload, replyTo) => this.send(type, payload, replyTo)
|
|
1337
1364
|
});
|
|
1338
1365
|
this.runEventBridge = new NodeRunEventBridge({
|
|
1366
|
+
bindModelUsage: (runId) => {
|
|
1367
|
+
const run = this.runtime.getRun(runId);
|
|
1368
|
+
const session = run ? this.runtime.getAgentSession(run.sessionId) : undefined;
|
|
1369
|
+
if (session)
|
|
1370
|
+
this.modelUsage.bindInherited(runId, session.id, session.runner);
|
|
1371
|
+
},
|
|
1372
|
+
modelUsage: (runId, payload) => this.modelUsage.record(runId, payload),
|
|
1339
1373
|
runtime: this.runtime,
|
|
1340
1374
|
state: this.runState,
|
|
1341
1375
|
events: this.runEventService,
|
|
@@ -1414,6 +1448,7 @@ export class NodeConnector {
|
|
|
1414
1448
|
marAgentModel: context.marAgentModel,
|
|
1415
1449
|
marAgentModels: context.marAgentModels,
|
|
1416
1450
|
marAgentCompact: context.marAgentCompact,
|
|
1451
|
+
...(context.usageGrant === undefined ? {} : { usageGrant: context.usageGrant }),
|
|
1417
1452
|
marAgentRuntimeScopeId: context.marAgentRuntimeScopeId
|
|
1418
1453
|
}));
|
|
1419
1454
|
return { runId: result.run.id };
|
|
@@ -1593,6 +1628,12 @@ export class NodeConnector {
|
|
|
1593
1628
|
})
|
|
1594
1629
|
});
|
|
1595
1630
|
hostedService = new HostedConversationNodeService({
|
|
1631
|
+
modelUsage: (grant, usage) => {
|
|
1632
|
+
const runId = usage.executionId ?? usage.requestId;
|
|
1633
|
+
this.modelUsage.bind(runId, grant, usage.sessionId, 'mar-agent');
|
|
1634
|
+
this.modelUsage.record(runId, normalizeMarAgentUsage(usage));
|
|
1635
|
+
this.modelUsage.release(runId);
|
|
1636
|
+
},
|
|
1596
1637
|
database: this.database,
|
|
1597
1638
|
store,
|
|
1598
1639
|
runtime,
|
|
@@ -1684,6 +1725,16 @@ export class NodeConnector {
|
|
|
1684
1725
|
await this.upgradeCoordinator.loadResult(resolve(dirname(nodeDatabasePath(this.config, this.configPath)), 'upgrade-result.json'));
|
|
1685
1726
|
await assertLocalSqlitePath(nodeDatabasePath(this.config, this.configPath));
|
|
1686
1727
|
this.database ??= new NodeDatabase(nodeDatabasePath(this.config, this.configPath), () => this.config?.nodeId ?? 'unregistered');
|
|
1728
|
+
this.usageOutbox?.disconnected();
|
|
1729
|
+
this.usageOutbox = new ModelUsageOutbox({
|
|
1730
|
+
database: () => this.database?.raw,
|
|
1731
|
+
namespace: () => this.config?.nodeId
|
|
1732
|
+
? JSON.stringify([this.config.serverUrl, this.config.nodeId])
|
|
1733
|
+
: undefined,
|
|
1734
|
+
send: (type, payload) => this.send(type, payload),
|
|
1735
|
+
diagnose: (code) => nodeLog('model.usage.diagnostic', { code })
|
|
1736
|
+
});
|
|
1737
|
+
this.usageOutbox.recoverInterrupted();
|
|
1687
1738
|
this.database.closeRunningTerminalSessions();
|
|
1688
1739
|
this.scheduledSessions.start();
|
|
1689
1740
|
await this.initializeConversationHosting();
|
|
@@ -1872,6 +1923,7 @@ export class NodeConnector {
|
|
|
1872
1923
|
...(input.initiatedByUserId === undefined
|
|
1873
1924
|
? {}
|
|
1874
1925
|
: { initiatedByUserId: input.initiatedByUserId }),
|
|
1926
|
+
...(input.usageGrant === undefined ? {} : { usageGrant: input.usageGrant }),
|
|
1875
1927
|
secretEnvironment: input.secretEnvironment,
|
|
1876
1928
|
...(input.personalInstructions === undefined
|
|
1877
1929
|
? {}
|
|
@@ -2095,6 +2147,9 @@ export class NodeConnector {
|
|
|
2095
2147
|
await hostedConversationRuntime?.dispose().catch(() => undefined);
|
|
2096
2148
|
await drains;
|
|
2097
2149
|
await this.gitExecutionService?.close();
|
|
2150
|
+
this.usageOutbox?.disconnected();
|
|
2151
|
+
this.usageOutbox = undefined;
|
|
2152
|
+
this.modelUsage.clear();
|
|
2098
2153
|
database?.close();
|
|
2099
2154
|
}
|
|
2100
2155
|
})();
|
package/dist/database.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { NODE_SCHEMA_MIGRATION_V17 } from './migrations/v017.js';
|
|
1
2
|
import { randomUUID } from 'node:crypto';
|
|
2
3
|
import { closeSync, existsSync, mkdirSync, openSync, rmSync } from 'node:fs';
|
|
3
4
|
import { dirname } from 'node:path';
|
|
@@ -14,7 +15,8 @@ import { assertNodeNoForeignKeys, assertNodeRelations } from './database-relatio
|
|
|
14
15
|
export const NODE_SCHEMA_MIGRATIONS = [
|
|
15
16
|
NODE_SCHEMA_MIGRATION_V14,
|
|
16
17
|
NODE_SCHEMA_MIGRATION_V15,
|
|
17
|
-
NODE_SCHEMA_MIGRATION_V16
|
|
18
|
+
NODE_SCHEMA_MIGRATION_V16,
|
|
19
|
+
NODE_SCHEMA_MIGRATION_V17
|
|
18
20
|
];
|
|
19
21
|
const LEGACY_NODE_SCHEMA_V12 = NODE_SCHEMA_VERSION_V13 - 1;
|
|
20
22
|
const OBSOLETE_NODE_SCHEMA_V16 = 16;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { DatabaseSync } from 'node:sqlite';
|
|
2
|
+
import { NODE_SCHEMA_V13 } from './v013.js';
|
|
3
|
+
import { NODE_SCHEMA_MIGRATION_V14 } from './v014.js';
|
|
4
|
+
import { NODE_SCHEMA_MIGRATION_V15 } from './v015.js';
|
|
5
|
+
import { NODE_SCHEMA_MIGRATION_V16 } from './v016.js';
|
|
6
|
+
const sql = `CREATE TABLE model_usage_outbox (
|
|
7
|
+
namespace TEXT NOT NULL,
|
|
8
|
+
record_id TEXT NOT NULL,
|
|
9
|
+
revision INTEGER NOT NULL,
|
|
10
|
+
payload TEXT NOT NULL,
|
|
11
|
+
bytes INTEGER NOT NULL,
|
|
12
|
+
updated_at INTEGER NOT NULL,
|
|
13
|
+
ended_at INTEGER,
|
|
14
|
+
PRIMARY KEY(namespace, record_id)
|
|
15
|
+
) STRICT;
|
|
16
|
+
CREATE INDEX model_usage_outbox_updated ON model_usage_outbox(updated_at);`;
|
|
17
|
+
export const NODE_SCHEMA_MIGRATION_V17 = {
|
|
18
|
+
version: 17,
|
|
19
|
+
sql,
|
|
20
|
+
preflight: (database) => NODE_SCHEMA_MIGRATION_V16.assert(database),
|
|
21
|
+
assert(database) {
|
|
22
|
+
const reference = new DatabaseSync(':memory:');
|
|
23
|
+
try {
|
|
24
|
+
reference.exec(NODE_SCHEMA_V13);
|
|
25
|
+
reference.exec(NODE_SCHEMA_MIGRATION_V14.sql);
|
|
26
|
+
reference.exec(NODE_SCHEMA_MIGRATION_V15.sql);
|
|
27
|
+
reference.exec(NODE_SCHEMA_MIGRATION_V16.sql);
|
|
28
|
+
reference.exec(sql);
|
|
29
|
+
const schema = (db) => db
|
|
30
|
+
.prepare("SELECT type,name,sql FROM sqlite_master WHERE sql IS NOT NULL AND name NOT LIKE 'sqlite_%' ORDER BY type,name")
|
|
31
|
+
.all()
|
|
32
|
+
.map((row) => [
|
|
33
|
+
row.type,
|
|
34
|
+
row.name,
|
|
35
|
+
row.name === 'node_schema_migrations'
|
|
36
|
+
? String(row.sql).replace(/\s+/g, '')
|
|
37
|
+
: String(row.sql).replace(/\s+/g, ' ').trim()
|
|
38
|
+
]);
|
|
39
|
+
if (JSON.stringify(schema(reference)) !== JSON.stringify(schema(database)))
|
|
40
|
+
throw new Error('NODE_DATABASE_SCHEMA_INVALID:model_usage');
|
|
41
|
+
}
|
|
42
|
+
finally {
|
|
43
|
+
reference.close();
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
};
|
|
@@ -7,6 +7,7 @@ import { nodeLog } from './operational.js';
|
|
|
7
7
|
import { stripClaudePlanTag } from './runner-command-engine.js';
|
|
8
8
|
import { stripGoalStartPrompt } from './service/goal-runtime.js';
|
|
9
9
|
import { parseCodexComposerImageMarkers } from './runner/codex/composer-image-markers.js';
|
|
10
|
+
import { extractClaudeToolResults } from './runner/claude/tool-normalizer.js';
|
|
10
11
|
const DISCOVERY_READ_BYTES = 32 * 1024;
|
|
11
12
|
const TRANSCRIPT_HEADER_READ_BYTES = 32 * 1024;
|
|
12
13
|
const TRANSCRIPT_READ_MAX_BYTES = 16 * 1024 * 1024;
|
|
@@ -740,21 +741,11 @@ function toolUsesOf(record, createdAt) {
|
|
|
740
741
|
});
|
|
741
742
|
}
|
|
742
743
|
function toolResultsOf(record, createdAt) {
|
|
743
|
-
return
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
{
|
|
749
|
-
kind: 'tool_result',
|
|
750
|
-
toolUseId: block['tool_use_id'],
|
|
751
|
-
output: output.text,
|
|
752
|
-
failed: block['is_error'] === true,
|
|
753
|
-
truncated: output.truncated,
|
|
754
|
-
createdAt
|
|
755
|
-
}
|
|
756
|
-
];
|
|
757
|
-
});
|
|
744
|
+
return extractClaudeToolResults(record).map((result) => ({
|
|
745
|
+
...result,
|
|
746
|
+
kind: 'tool_result',
|
|
747
|
+
createdAt
|
|
748
|
+
}));
|
|
758
749
|
}
|
|
759
750
|
/**
|
|
760
751
|
* Codex persists custom and standard function tools as paired `response_item`
|
|
@@ -973,6 +964,7 @@ function withToolResult(call, result) {
|
|
|
973
964
|
...call,
|
|
974
965
|
outputSummary: result.output,
|
|
975
966
|
outputTruncated: result.truncated,
|
|
967
|
+
...(result.claudeResult === undefined ? {} : { claudeResult: result.claudeResult }),
|
|
976
968
|
...(imageAttachments === undefined ? {} : { imageAttachments }),
|
|
977
969
|
failed: result.failed
|
|
978
970
|
};
|
|
@@ -126,6 +126,10 @@ export class AbstractRunner {
|
|
|
126
126
|
void session;
|
|
127
127
|
return undefined;
|
|
128
128
|
}
|
|
129
|
+
async readToolResultResource(session, input) {
|
|
130
|
+
void [session, input];
|
|
131
|
+
throw new Error('TOOL_RESOURCE_UNSUPPORTED');
|
|
132
|
+
}
|
|
129
133
|
acceptsTruncatedNativeHistory() {
|
|
130
134
|
return true;
|
|
131
135
|
}
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
+
import { ClaudeUsageNormalizer } from './tool-normalizer.js';
|
|
1
2
|
import { createEnvelope } from '@myagentroam/protocol';
|
|
2
3
|
import { ClaudeChannelGate } from '../../claude-channel.js';
|
|
3
4
|
import { nodeExecutionEnvironment } from '../../node-execution-environment.js';
|
|
4
|
-
import { boundedConversationItemId, claudePlanText, compactRunnerText, extractClaudeText, extractClaudeThinkingSummaries,
|
|
5
|
-
import { normalizeClaudeTool } from './tool-normalizer.js';
|
|
5
|
+
import { boundedConversationItemId, claudePlanText, compactRunnerText, extractClaudeText, extractClaudeThinkingSummaries, extractClaudeToolUses } from '../codex/conversation-parser.js';
|
|
6
|
+
import { normalizeClaudeTool, extractClaudeToolResults } from './tool-normalizer.js';
|
|
6
7
|
import { claudeContentBlocks } from '../../util/node-operation-parsers.js';
|
|
7
8
|
import { parseSecretEnvironment } from '../../util/secret-environment.js';
|
|
8
9
|
import { safeErrorCode } from '../../util/safe-error.js';
|
|
@@ -134,6 +135,9 @@ export class ClaudeManagedRunController {
|
|
|
134
135
|
let terminal = false;
|
|
135
136
|
let handle;
|
|
136
137
|
const toolKinds = new Map();
|
|
138
|
+
const toolInputs = new Map();
|
|
139
|
+
const knownSubagents = new Map();
|
|
140
|
+
const usage = new ClaudeUsageNormalizer(runId, (value) => this.host.emit(runId, 'run.usage', value));
|
|
137
141
|
const finish = (status, eventType, eventPayload, failure, fallbackCode = 'CLAUDE_RUN_FAILED') => {
|
|
138
142
|
if (terminal)
|
|
139
143
|
return;
|
|
@@ -157,6 +161,7 @@ export class ClaudeManagedRunController {
|
|
|
157
161
|
payload: { phase: 'COMPLETED', summary: null },
|
|
158
162
|
merge: true
|
|
159
163
|
});
|
|
164
|
+
usage.finish(status === 'SUCCEEDED' ? 'succeeded' : status === 'FAILED' ? 'failed' : 'cancelled');
|
|
160
165
|
this.host.emit(runId, eventType, projected ?? eventPayload, status);
|
|
161
166
|
this.runner.execution.runs.delete(runId);
|
|
162
167
|
handle?.cancel();
|
|
@@ -233,6 +238,7 @@ export class ClaudeManagedRunController {
|
|
|
233
238
|
onMessage: (message) => {
|
|
234
239
|
if (terminal)
|
|
235
240
|
return;
|
|
241
|
+
usage.observe(message);
|
|
236
242
|
const receiptAt = generationTiming.timestamp();
|
|
237
243
|
const record = message;
|
|
238
244
|
const nativeSessionId = typeof record.session_id === 'string' ? record.session_id : undefined;
|
|
@@ -373,7 +379,8 @@ export class ClaudeManagedRunController {
|
|
|
373
379
|
continue;
|
|
374
380
|
}
|
|
375
381
|
const itemId = boundedConversationItemId('claude-tool', tool.id);
|
|
376
|
-
|
|
382
|
+
toolInputs.set(tool.id, tool);
|
|
383
|
+
const normalized = normalizeClaudeTool(tool.name, tool.input, null, false, tool.inputTruncated, knownSubagents);
|
|
377
384
|
const kind = normalized.kind;
|
|
378
385
|
toolKinds.set(tool.id, kind);
|
|
379
386
|
this.host.emitItem(runId, {
|
|
@@ -436,6 +443,10 @@ export class ClaudeManagedRunController {
|
|
|
436
443
|
const kind = toolKinds.get(result.toolUseId) ?? 'tool_call';
|
|
437
444
|
if (kind === 'user_input_request')
|
|
438
445
|
continue;
|
|
446
|
+
const tool = toolInputs.get(result.toolUseId);
|
|
447
|
+
if (tool === undefined)
|
|
448
|
+
continue;
|
|
449
|
+
const normalized = normalizeClaudeTool(tool.name, tool.input, result.output, result.failed, tool.inputTruncated || result.truncated, knownSubagents, result.claudeResult);
|
|
439
450
|
this.host.emitItem(runId, {
|
|
440
451
|
itemId: boundedConversationItemId('claude-tool', result.toolUseId),
|
|
441
452
|
kind,
|
|
@@ -450,6 +461,7 @@ export class ClaudeManagedRunController {
|
|
|
450
461
|
: kind === 'file_change'
|
|
451
462
|
? {}
|
|
452
463
|
: {
|
|
464
|
+
...normalized.payload,
|
|
453
465
|
outputSummary: result.output,
|
|
454
466
|
progressLabel: null,
|
|
455
467
|
errorCode: result.failed ? 'TOOL_EXECUTION_FAILED' : null,
|
|
@@ -1,10 +1,90 @@
|
|
|
1
|
-
import { compactRunnerText, compactRunnerValue } from '../../util/node-operation-parsers.js';
|
|
2
|
-
|
|
1
|
+
import { claudeContentBlocks, compactRunnerText, compactRunnerValue } from '../../util/node-operation-parsers.js';
|
|
2
|
+
import { RunnerUsageTracker, usageToken, usageTokenSum } from '../../service/runner-usage-tracker.js';
|
|
3
|
+
export class ClaudeUsageNormalizer {
|
|
4
|
+
tracker;
|
|
5
|
+
messages = new Map();
|
|
6
|
+
constructor(identity, publish) {
|
|
7
|
+
this.tracker = new RunnerUsageTracker(identity, publish);
|
|
8
|
+
}
|
|
9
|
+
observe(value) {
|
|
10
|
+
const envelope = record(value);
|
|
11
|
+
if (!envelope)
|
|
12
|
+
return;
|
|
13
|
+
const parent = typeof envelope.parent_tool_use_id === 'string' ? envelope.parent_tool_use_id : '';
|
|
14
|
+
const event = envelope.type === 'stream_event' ? record(envelope.event) : undefined;
|
|
15
|
+
const message = record(envelope.type === 'assistant' ? envelope.message : event?.message);
|
|
16
|
+
const previous = this.messages.get(parent);
|
|
17
|
+
const id = typeof message?.id === 'string' ? message.id : previous?.id;
|
|
18
|
+
if (!id || message?.model === '<synthetic>')
|
|
19
|
+
return;
|
|
20
|
+
const usage = record(message?.usage ?? event?.usage);
|
|
21
|
+
let values = {
|
|
22
|
+
...(previous?.id === id ? previous.values : {}),
|
|
23
|
+
purpose: 'conversation',
|
|
24
|
+
agentRole: parent ? 'subagent' : 'root',
|
|
25
|
+
...(typeof message?.model === 'string' ? { actualModel: message.model } : {})
|
|
26
|
+
};
|
|
27
|
+
if (usage) {
|
|
28
|
+
const input = usageToken(usage.input_tokens), read = usageToken(usage.cache_read_input_tokens), write = usageToken(usage.cache_creation_input_tokens), output = usageToken(usage.output_tokens);
|
|
29
|
+
values = {
|
|
30
|
+
...values,
|
|
31
|
+
...(input === undefined
|
|
32
|
+
? {}
|
|
33
|
+
: { inputTokens: usageTokenSum(input, read ?? '0', write ?? '0') }),
|
|
34
|
+
...(output === undefined ? {} : { outputTokens: output }),
|
|
35
|
+
...(read === undefined ? {} : { cacheReadTokens: read }),
|
|
36
|
+
...(write === undefined ? {} : { cacheWriteTokens: write })
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
this.messages.set(parent, { id, values });
|
|
40
|
+
if (usage || event?.type === 'message_stop')
|
|
41
|
+
this.tracker.snapshot(id, values, event?.type === 'message_stop' ||
|
|
42
|
+
typeof record(event?.delta)?.stop_reason === 'string' ||
|
|
43
|
+
typeof message?.stop_reason === 'string', 'partial');
|
|
44
|
+
}
|
|
45
|
+
finish(state) {
|
|
46
|
+
this.tracker.finish(state);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
export function restoreClaudeSubagentTitles(calls) {
|
|
50
|
+
const titles = new Map();
|
|
51
|
+
for (const call of calls)
|
|
52
|
+
claudeSubagentProjection(call.toolName, call.input, call.claudeResult ?? call.outputSummary, titles);
|
|
53
|
+
return titles;
|
|
54
|
+
}
|
|
55
|
+
export function extractClaudeToolResults(message) {
|
|
56
|
+
// This ID describes the owning subagent, not the tool call producing the result.
|
|
57
|
+
if (message.parent_tool_use_id !== undefined && message.parent_tool_use_id !== null)
|
|
58
|
+
return [];
|
|
59
|
+
const blocks = claudeContentBlocks(message).filter((block) => block.type === 'tool_result' && typeof block.tool_use_id === 'string');
|
|
60
|
+
return blocks.map((block) => {
|
|
61
|
+
const output = compactRunnerValue(block.content, 10_000);
|
|
62
|
+
// A message-level structured result cannot be assigned to multiple calls.
|
|
63
|
+
const structured = blocks.length === 1 ? structuredRecord(message.tool_use_result) : undefined;
|
|
64
|
+
const fields = structured ?? structuredRecord(block.content);
|
|
65
|
+
const agentId = boundedId(fields?.agentId);
|
|
66
|
+
const taskId = boundedId(fields?.task_id);
|
|
67
|
+
const taskType = boundedId(fields?.task_type);
|
|
68
|
+
const claudeResult = {
|
|
69
|
+
...(agentId === undefined ? {} : { agentId }),
|
|
70
|
+
...(taskId === undefined ? {} : { task_id: taskId }),
|
|
71
|
+
...(taskType === undefined ? {} : { task_type: taskType })
|
|
72
|
+
};
|
|
73
|
+
return {
|
|
74
|
+
toolUseId: block.tool_use_id,
|
|
75
|
+
output: output.text,
|
|
76
|
+
failed: block.is_error === true,
|
|
77
|
+
truncated: output.truncated,
|
|
78
|
+
...(Object.keys(claudeResult).length === 0 ? {} : { claudeResult })
|
|
79
|
+
};
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
export function normalizeClaudeTool(toolName, input, output, failed, truncated, knownSubagents = new Map(), resultMetadata) {
|
|
3
83
|
const values = record(input);
|
|
4
84
|
const normalizedToolName = compactRunnerText(toolName, 128) || 'unknown';
|
|
5
85
|
const normalizedOutput = output === null ? null : compactRunnerText(output, 10_000);
|
|
6
86
|
const outputTruncated = truncated || (output?.length ?? 0) > 10_000;
|
|
7
|
-
const subagent = claudeSubagentProjection(toolName, input, output);
|
|
87
|
+
const subagent = claudeSubagentProjection(toolName, input, resultMetadata ?? output, knownSubagents);
|
|
8
88
|
if (isCommandTool(toolName))
|
|
9
89
|
return {
|
|
10
90
|
kind: 'command_execution',
|
|
@@ -70,7 +150,7 @@ function record(value) {
|
|
|
70
150
|
? value
|
|
71
151
|
: undefined;
|
|
72
152
|
}
|
|
73
|
-
function claudeSubagentProjection(toolName, input, output) {
|
|
153
|
+
function claudeSubagentProjection(toolName, input, output, knownSubagents) {
|
|
74
154
|
const normalizedName = toolName.toLowerCase();
|
|
75
155
|
const operation = normalizedName === 'agent'
|
|
76
156
|
? 'START'
|
|
@@ -90,12 +170,26 @@ function claudeSubagentProjection(toolName, input, output) {
|
|
|
90
170
|
: normalizedName === 'sendmessage'
|
|
91
171
|
? boundedId(inputData?.to)
|
|
92
172
|
: (boundedId(inputData?.task_id) ?? boundedId(outputData?.task_id));
|
|
173
|
+
if (normalizedName === 'taskoutput' || normalizedName === 'taskstop') {
|
|
174
|
+
const taskType = outputData?.task_type;
|
|
175
|
+
if (typeof taskType === 'string') {
|
|
176
|
+
if (taskType !== 'local_agent' && taskType !== 'remote_agent')
|
|
177
|
+
return undefined;
|
|
178
|
+
}
|
|
179
|
+
else if (subagentId === undefined || !knownSubagents.has(subagentId))
|
|
180
|
+
return undefined;
|
|
181
|
+
}
|
|
93
182
|
const title = normalizedName === 'agent' && typeof inputData?.description === 'string'
|
|
94
183
|
? compactRunnerText(inputData.description, 500)
|
|
95
|
-
:
|
|
184
|
+
: subagentId === undefined
|
|
185
|
+
? ''
|
|
186
|
+
: (knownSubagents.get(subagentId) ?? '');
|
|
187
|
+
if (subagentId !== undefined && normalizedName === 'agent')
|
|
188
|
+
knownSubagents.set(subagentId, title);
|
|
96
189
|
return {
|
|
97
190
|
subagentOperation: operation,
|
|
98
|
-
subagentId: subagentId ?? null,
|
|
191
|
+
subagentId: subagentId === '*' ? null : (subagentId ?? null),
|
|
192
|
+
...(operation === 'STATUS' ? { subagentWait: inputData?.block === true } : {}),
|
|
99
193
|
...(title.length === 0 ? {} : { title })
|
|
100
194
|
};
|
|
101
195
|
}
|
|
@@ -1,3 +1,41 @@
|
|
|
1
|
+
import { RunnerUsageTracker, usageToken } from '../../service/runner-usage-tracker.js';
|
|
2
|
+
export class CodexUsageNormalizer {
|
|
3
|
+
requestedModel;
|
|
4
|
+
tracker;
|
|
5
|
+
constructor(identity, publish, requestedModel) {
|
|
6
|
+
this.requestedModel = requestedModel;
|
|
7
|
+
this.tracker = new RunnerUsageTracker(identity, publish);
|
|
8
|
+
}
|
|
9
|
+
observe(method, params, compact) {
|
|
10
|
+
// TokenCount.last can replay context/compaction snapshots without a request identity.
|
|
11
|
+
// Only raw response completions are independently additive request facts.
|
|
12
|
+
if (method !== 'rawResponse/completed' || typeof params.responseId !== 'string')
|
|
13
|
+
return;
|
|
14
|
+
const usage = isPlainRecord(params.usage) ? params.usage : {};
|
|
15
|
+
const inputTokens = usageToken(usage.inputTokens ?? usage.input_tokens);
|
|
16
|
+
const outputTokens = usageToken(usage.outputTokens ?? usage.output_tokens);
|
|
17
|
+
const cacheReadTokens = usageToken(usage.cachedInputTokens ??
|
|
18
|
+
(isPlainRecord(usage.input_tokens_details)
|
|
19
|
+
? usage.input_tokens_details.cached_tokens
|
|
20
|
+
: undefined));
|
|
21
|
+
const reasoningTokens = usageToken(usage.reasoningOutputTokens ??
|
|
22
|
+
(isPlainRecord(usage.output_tokens_details)
|
|
23
|
+
? usage.output_tokens_details.reasoning_tokens
|
|
24
|
+
: undefined));
|
|
25
|
+
const values = {
|
|
26
|
+
purpose: compact ? 'compact' : 'conversation',
|
|
27
|
+
agentRole: 'root',
|
|
28
|
+
...(this.requestedModel ? { requestedModel: this.requestedModel } : {}),
|
|
29
|
+
...(typeof params.model === 'string' ? { actualModel: params.model } : {}),
|
|
30
|
+
responseId: params.responseId,
|
|
31
|
+
...(inputTokens === undefined ? {} : { inputTokens }),
|
|
32
|
+
...(outputTokens === undefined ? {} : { outputTokens }),
|
|
33
|
+
...(cacheReadTokens === undefined ? {} : { cacheReadTokens }),
|
|
34
|
+
...(reasoningTokens === undefined ? {} : { reasoningTokens })
|
|
35
|
+
};
|
|
36
|
+
this.tracker.snapshot(params.responseId, values, true, 'partial');
|
|
37
|
+
}
|
|
38
|
+
}
|
|
1
39
|
import { basename, extname } from 'node:path';
|
|
2
40
|
import { runnerErrorProjection } from '../../util/runner-error.js';
|
|
3
41
|
import { conversationImageAttachmentSchema } from '@myagentroam/protocol';
|
|
@@ -503,7 +541,7 @@ export function codexPlanText(value) {
|
|
|
503
541
|
* text with missing or unclosed tags remains a regular assistant message.
|
|
504
542
|
*/
|
|
505
543
|
export function codexOfficialItem(session, nativeTurnId, value, turnSequence, itemSequence, startedAt, completedAt) {
|
|
506
|
-
if (!isPlainRecord(value) || typeof value.type !== 'string')
|
|
544
|
+
if (!isPlainRecord(value) || typeof value.type !== 'string' || value.type === 'subAgentActivity')
|
|
507
545
|
return undefined;
|
|
508
546
|
const id = (value.type === 'custom_tool_call' ||
|
|
509
547
|
value.type === 'custom_tool_call_output' ||
|
|
@@ -686,11 +724,11 @@ export function codexOfficialItem(session, nativeTurnId, value, turnSequence, it
|
|
|
686
724
|
: {
|
|
687
725
|
...base,
|
|
688
726
|
...special,
|
|
689
|
-
status:
|
|
727
|
+
status: base.status
|
|
690
728
|
};
|
|
691
729
|
}
|
|
692
730
|
export function codexLiveConversationItem(value, completed, imageAttachments = []) {
|
|
693
|
-
if (typeof value.type !== 'string')
|
|
731
|
+
if (typeof value.type !== 'string' || value.type === 'subAgentActivity')
|
|
694
732
|
return undefined;
|
|
695
733
|
const nativeId = typeof value.call_id === 'string'
|
|
696
734
|
? value.call_id
|
|
@@ -936,33 +974,6 @@ function codexSpecialItem(value) {
|
|
|
936
974
|
inputSummary: typeof value.path === 'string' ? compactRunnerText(value.path, 4_096) : null
|
|
937
975
|
}).payload
|
|
938
976
|
};
|
|
939
|
-
if (value.type === 'subAgentActivity') {
|
|
940
|
-
const activity = typeof value.kind === 'string' ? value.kind : '';
|
|
941
|
-
const operation = activity === 'started'
|
|
942
|
-
? 'START'
|
|
943
|
-
: activity === 'interrupted'
|
|
944
|
-
? 'CANCEL'
|
|
945
|
-
: activity === 'interacted' || activity === 'completed'
|
|
946
|
-
? 'STATUS'
|
|
947
|
-
: undefined;
|
|
948
|
-
if (operation === undefined)
|
|
949
|
-
return undefined;
|
|
950
|
-
const agentId = typeof value.agentThreadId === 'string' ? compactRunnerText(value.agentThreadId, 128) : '';
|
|
951
|
-
const agentPath = typeof value.agentPath === 'string' ? compactRunnerText(value.agentPath, 500) : '';
|
|
952
|
-
return {
|
|
953
|
-
kind: 'tool_call',
|
|
954
|
-
payload: {
|
|
955
|
-
...normalizeCodexTool({
|
|
956
|
-
name: 'subagent_activity',
|
|
957
|
-
namespace: 'codex',
|
|
958
|
-
outputSummary: activity
|
|
959
|
-
}).payload,
|
|
960
|
-
...(agentPath ? { title: agentPath } : {}),
|
|
961
|
-
subagentOperation: operation,
|
|
962
|
-
subagentId: agentId || null
|
|
963
|
-
}
|
|
964
|
-
};
|
|
965
|
-
}
|
|
966
977
|
if (value.type === 'collabAgentToolCall') {
|
|
967
978
|
const names = {
|
|
968
979
|
spawnAgent: 'spawn_agent',
|
|
@@ -5,7 +5,7 @@ import { safeErrorCode } from '../../util/safe-error.js';
|
|
|
5
5
|
import { runnerErrorProjection } from '../../util/runner-error.js';
|
|
6
6
|
import { ResponseLatencyTracker } from '../../service/response-latency-tracker.js';
|
|
7
7
|
import { GenerationTimingTracker } from '../../service/generation-timing-tracker.js';
|
|
8
|
-
import { boundedConversationItemId, codexImageAttachments, codexLiveConversationItem, codexUserInputQuestions, compactRunnerText, isPlainRecord } from './conversation-parser.js';
|
|
8
|
+
import { boundedConversationItemId, codexImageAttachments, codexLiveConversationItem, CodexUsageNormalizer, codexUserInputQuestions, compactRunnerText, isPlainRecord } from './conversation-parser.js';
|
|
9
9
|
export function resolveCodexNotificationRun(runs, input) {
|
|
10
10
|
const entries = [...runs.entries()];
|
|
11
11
|
if (input.turnId !== undefined) {
|
|
@@ -28,6 +28,7 @@ export function resolveCodexNotificationRun(runs, input) {
|
|
|
28
28
|
export class CodexManagedRunController {
|
|
29
29
|
runner;
|
|
30
30
|
host;
|
|
31
|
+
usage = new Map();
|
|
31
32
|
constructor(runner, host) {
|
|
32
33
|
this.runner = runner;
|
|
33
34
|
this.host = host;
|
|
@@ -113,6 +114,7 @@ export class CodexManagedRunController {
|
|
|
113
114
|
catch (error) {
|
|
114
115
|
this.emitError(runId, error, 'CODEX_COMPACT_FAILED');
|
|
115
116
|
this.host.emit(runId, 'run.failed', runnerErrorProjection(error, 'CODEX_COMPACT_FAILED'), 'FAILED');
|
|
117
|
+
this.usage.delete(runId);
|
|
116
118
|
this.runner.execution.runs.delete(runId);
|
|
117
119
|
this.runner.releaseEnvironment(runId);
|
|
118
120
|
this.host.removeActive(runId);
|
|
@@ -138,6 +140,12 @@ export class CodexManagedRunController {
|
|
|
138
140
|
if (run === undefined)
|
|
139
141
|
return;
|
|
140
142
|
const [runId, activeRun] = run;
|
|
143
|
+
let usage = this.usage.get(runId);
|
|
144
|
+
if (!usage) {
|
|
145
|
+
usage = new CodexUsageNormalizer(runId, (value) => this.host.emit(runId, 'run.usage', value), this.host.session(activeRun.sessionId)?.model ?? undefined);
|
|
146
|
+
this.usage.set(runId, usage);
|
|
147
|
+
}
|
|
148
|
+
usage.observe(notification.method, params, activeRun.generationCompacting === true);
|
|
141
149
|
const receiptAt = activeRun.generationTiming?.timestamp();
|
|
142
150
|
if (notification.method === 'turn/started' && turnId !== undefined) {
|
|
143
151
|
const active = this.runner.execution.runs.get(runId);
|
|
@@ -332,6 +340,7 @@ export class CodexManagedRunController {
|
|
|
332
340
|
this.host.session(active.sessionId)?.titleSource !== 'AUTO'
|
|
333
341
|
? undefined
|
|
334
342
|
: this.host.syncTitle(active.sessionId, active.threadId, active.cwd);
|
|
343
|
+
this.usage.delete(runId);
|
|
335
344
|
this.runner.execution.runs.delete(runId);
|
|
336
345
|
if (titleRefresh === undefined)
|
|
337
346
|
this.runner.releaseEnvironment(runId);
|
|
@@ -624,6 +633,7 @@ export class CodexManagedRunController {
|
|
|
624
633
|
break;
|
|
625
634
|
}
|
|
626
635
|
catch (error) {
|
|
636
|
+
this.usage.delete(runId);
|
|
627
637
|
this.runner.execution.runs.delete(runId);
|
|
628
638
|
if (attempt === 0 && (await this.runner.recoverMissingRollout(runId, error)))
|
|
629
639
|
continue;
|
|
@@ -663,6 +673,7 @@ export class CodexManagedRunController {
|
|
|
663
673
|
}
|
|
664
674
|
this.emitError(runId, error, 'CODEX_RUN_FAILED');
|
|
665
675
|
this.host.emit(runId, 'run.failed', runnerErrorProjection(error, 'CODEX_RUN_FAILED'), 'FAILED');
|
|
676
|
+
this.usage.delete(runId);
|
|
666
677
|
this.runner.execution.runs.delete(runId);
|
|
667
678
|
this.runner.releaseEnvironment(runId);
|
|
668
679
|
this.host.removeActive(runId);
|