@myagentroam/node 0.11.15 → 0.11.16
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 +13 -20
- package/dist/native-session-history.js +35 -832
- package/dist/native-transcript-adapters.js +9 -0
- package/dist/native-transcript-model.js +1 -0
- package/dist/runner/abstract-runner.js +12 -0
- package/dist/runner/claude/conversation-parser.js +80 -1
- package/dist/runner/claude/managed-run-controller.js +4 -2
- package/dist/runner/claude/message-parsers.js +37 -0
- package/dist/runner/claude/native-context-usage.js +90 -0
- package/dist/runner/claude/native-transcript.js +347 -0
- package/dist/runner/claude/tool-normalizer.js +12 -2
- package/dist/runner/claude-code-runner.js +13 -2
- package/dist/runner/codex/conversation-parser.js +67 -3
- package/dist/runner/codex/managed-run-controller.js +3 -2
- package/dist/runner/codex/native-context-usage.js +42 -0
- package/dist/runner/codex/native-transcript.js +575 -0
- package/dist/runner/codex-runner.js +18 -3
- package/dist/runner/mar-agent/managed-run-controller.js +1 -1
- package/dist/runner/mar-agent-runner.js +6 -0
- package/dist/runner/opencode/conversation-parser.js +1 -1
- package/dist/runner/opencode/managed-run-controller.js +1 -1
- package/dist/runner/opencode-runner.js +7 -1
- package/dist/runner/runner-registry.js +4 -0
- package/dist/runtime-state.js +1 -0
- package/dist/service/continuous-run-coordinator.js +1 -0
- package/dist/service/conversation-template-manifest.js +179 -0
- package/dist/service/conversation-template-node-service.js +10 -132
- package/dist/service/external-session-resume-service.js +15 -0
- package/dist/service/fake-managed-run-service.js +1 -1
- package/dist/service/goal-continuation-coordinator.js +4 -3
- package/dist/service/goal-runtime.js +0 -9
- package/dist/service/hosted-conversation-node-service.js +16 -179
- package/dist/service/native-session-projection-service.js +7 -24
- package/dist/service/node-control-channel.js +1 -1
- package/dist/service/node-request-service.js +1 -1
- package/dist/service/node-run-event-bridge.js +0 -24
- package/dist/service/node-terminal-channel.js +1 -1
- package/dist/service/recent-run-projection-service.js +2 -1
- package/dist/service/run-workbench-service.js +1 -1
- package/dist/service/runner-interaction-service.js +2 -1
- package/dist/service/runner-service.js +6 -4
- package/dist/service/session-identity-service.js +1 -0
- package/dist/service/session-message-service.js +1 -0
- package/dist/service/session-query-service.js +1 -1
- package/dist/service/workbench-event-projection.js +1 -1
- package/dist/service/workbench-event-service.js +1 -1
- package/dist/service/workspace-file-service.js +0 -2
- package/dist/service/workspace-queue-workbench-service.js +2 -2
- package/dist/util/node-operation-parsers.js +0 -45
- package/dist/util/runner-native-session-parsers.js +17 -194
- package/package.json +4 -4
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { codexNativeTranscriptAdapter } from './runner/codex/native-transcript.js';
|
|
2
|
+
import { claudeNativeTranscriptAdapter } from './runner/claude/native-transcript.js';
|
|
3
|
+
const adapters = {
|
|
4
|
+
codex: codexNativeTranscriptAdapter,
|
|
5
|
+
'claude-code': claudeNativeTranscriptAdapter
|
|
6
|
+
};
|
|
7
|
+
export function nativeTranscriptAdapter(runner) {
|
|
8
|
+
return adapters[runner];
|
|
9
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -20,6 +20,13 @@ export class AbstractRunner {
|
|
|
20
20
|
void [workspace, environment, waitForRefresh];
|
|
21
21
|
return this.profile(capabilities);
|
|
22
22
|
}
|
|
23
|
+
goalAccess(currentAccess) {
|
|
24
|
+
return { access: 'auto', accessChanged: currentAccess !== 'auto' };
|
|
25
|
+
}
|
|
26
|
+
/** Restore persisted values only; never normalize fresh configuration input. */
|
|
27
|
+
restoreStoredAccess(access, fallbackAccess) {
|
|
28
|
+
return access !== null && access.trim().length > 0 ? access : fallbackAccess;
|
|
29
|
+
}
|
|
23
30
|
supportsConfiguration(configuration, workspace, environment) {
|
|
24
31
|
void [workspace, environment];
|
|
25
32
|
const profile = this.profileForValidation();
|
|
@@ -146,6 +153,11 @@ export class AbstractRunner {
|
|
|
146
153
|
void nativeTurnId;
|
|
147
154
|
return undefined;
|
|
148
155
|
}
|
|
156
|
+
/** Synchronously migrate Runner-owned state without I/O or event publication. */
|
|
157
|
+
migrateSessionIdentity(previousId, nativeSessionId) {
|
|
158
|
+
void previousId;
|
|
159
|
+
void nativeSessionId;
|
|
160
|
+
}
|
|
149
161
|
bindControl(control) {
|
|
150
162
|
if (this.control !== undefined)
|
|
151
163
|
throw new Error(`RUNNER_CONTROL_ALREADY_BOUND:${this.name}`);
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { boundedConversationItemId, isPlainRecord,
|
|
1
|
+
import { boundedConversationItemId, isPlainRecord, compactRunnerText } from '../../util/node-operation-parsers.js';
|
|
2
|
+
import { claudeContentBlocks } from './message-parsers.js';
|
|
2
3
|
export function extractClaudeThinkingText(record) {
|
|
3
4
|
return claudeContentBlocks(record).flatMap((block) => {
|
|
4
5
|
// Completed public blocks only; signatures and redacted/encrypted data are never read.
|
|
@@ -40,3 +41,81 @@ export function claudeAssistantBlocks(record) {
|
|
|
40
41
|
];
|
|
41
42
|
});
|
|
42
43
|
}
|
|
44
|
+
export function claudePlanText(text) {
|
|
45
|
+
const trimmed = text.trim();
|
|
46
|
+
if (!trimmed.startsWith('<proposed_plan>') || !/<\/proposed_plan>\s*$/.test(trimmed))
|
|
47
|
+
return undefined;
|
|
48
|
+
const plan = trimmed
|
|
49
|
+
.slice('<proposed_plan>'.length)
|
|
50
|
+
.replace(/<\/proposed_plan>\s*$/, '')
|
|
51
|
+
.trim();
|
|
52
|
+
return plan.length > 0 ? plan : undefined;
|
|
53
|
+
}
|
|
54
|
+
export function claudeUserInputQuestions(value) {
|
|
55
|
+
if (!Array.isArray(value.questions) || value.questions.length === 0 || value.questions.length > 4)
|
|
56
|
+
return [];
|
|
57
|
+
const questions = value.questions.map((raw, index) => {
|
|
58
|
+
if (!isPlainRecord(raw) || typeof raw.question !== 'string')
|
|
59
|
+
return undefined;
|
|
60
|
+
const options = Array.isArray(raw.options)
|
|
61
|
+
? raw.options.flatMap((option) => {
|
|
62
|
+
if (!isPlainRecord(option) || typeof option.label !== 'string')
|
|
63
|
+
return [];
|
|
64
|
+
const label = compactRunnerText(option.label, 500);
|
|
65
|
+
return label.length === 0
|
|
66
|
+
? []
|
|
67
|
+
: [
|
|
68
|
+
{
|
|
69
|
+
value: label,
|
|
70
|
+
label,
|
|
71
|
+
description: typeof option.description === 'string'
|
|
72
|
+
? compactRunnerText(option.description, 1_000)
|
|
73
|
+
: null
|
|
74
|
+
}
|
|
75
|
+
];
|
|
76
|
+
})
|
|
77
|
+
: [];
|
|
78
|
+
const multiSelect = raw.multiSelect === true;
|
|
79
|
+
return {
|
|
80
|
+
id: `claude-question-${index}`,
|
|
81
|
+
prompt: compactRunnerText(raw.question, 5_000),
|
|
82
|
+
input: options.length === 0 ? 'SHORT_TEXT' : multiSelect ? 'MULTI_SELECT' : 'SINGLE_SELECT',
|
|
83
|
+
options,
|
|
84
|
+
allowOther: true,
|
|
85
|
+
multiSelect
|
|
86
|
+
};
|
|
87
|
+
});
|
|
88
|
+
return questions.some((question) => question === undefined)
|
|
89
|
+
? []
|
|
90
|
+
: questions;
|
|
91
|
+
}
|
|
92
|
+
export function claudeDiscoveredSession(value, fallbackCwd) {
|
|
93
|
+
if (typeof value !== 'object' || value === null)
|
|
94
|
+
return undefined;
|
|
95
|
+
const record = value;
|
|
96
|
+
if (record.isSidechain === true)
|
|
97
|
+
return undefined;
|
|
98
|
+
const externalSessionId = typeof record.sessionId === 'string'
|
|
99
|
+
? record.sessionId
|
|
100
|
+
: typeof record.id === 'string'
|
|
101
|
+
? record.id
|
|
102
|
+
: undefined;
|
|
103
|
+
if (externalSessionId === undefined || externalSessionId.length === 0)
|
|
104
|
+
return undefined;
|
|
105
|
+
const officialTitle = [record.customTitle, record.firstPrompt, record.title].find((candidate) => typeof candidate === 'string' && candidate.trim().length > 0);
|
|
106
|
+
const previewTitle = typeof record.summary === 'string' && record.summary.trim().length > 0
|
|
107
|
+
? record.summary
|
|
108
|
+
: undefined;
|
|
109
|
+
const title = officialTitle ?? previewTitle;
|
|
110
|
+
const cwd = typeof record.cwd === 'string' && record.cwd.length > 0 ? record.cwd : fallbackCwd;
|
|
111
|
+
return {
|
|
112
|
+
externalSessionId,
|
|
113
|
+
cwd,
|
|
114
|
+
...(title === undefined
|
|
115
|
+
? {}
|
|
116
|
+
: {
|
|
117
|
+
title: title.slice(0, 80),
|
|
118
|
+
titleOrigin: officialTitle === undefined ? 'PREVIEW' : 'OFFICIAL'
|
|
119
|
+
})
|
|
120
|
+
};
|
|
121
|
+
}
|
|
@@ -3,9 +3,11 @@ import { ClaudeUsageNormalizer } from './tool-normalizer.js';
|
|
|
3
3
|
import { createEnvelope } from '@myagentroam/protocol';
|
|
4
4
|
import { ClaudeChannelGate } from '../../claude-channel.js';
|
|
5
5
|
import { nodeExecutionEnvironment } from '../../node-execution-environment.js';
|
|
6
|
-
import { boundedConversationItemId,
|
|
6
|
+
import { boundedConversationItemId, compactRunnerText } from '../../util/node-operation-parsers.js';
|
|
7
|
+
import { extractClaudeText, extractClaudeToolUses } from './message-parsers.js';
|
|
8
|
+
import { claudePlanText } from './conversation-parser.js';
|
|
7
9
|
import { normalizeClaudeTool, extractClaudeToolResults } from './tool-normalizer.js';
|
|
8
|
-
import { claudeContentBlocks } from '
|
|
10
|
+
import { claudeContentBlocks } from './message-parsers.js';
|
|
9
11
|
import { parseSecretEnvironment } from '../../util/secret-environment.js';
|
|
10
12
|
import { safeErrorCode } from '../../util/safe-error.js';
|
|
11
13
|
import { runnerErrorProjection } from '../../util/runner-error.js';
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { compactRunnerText, compactRunnerValue, isPlainRecord } from '../../util/node-operation-parsers.js';
|
|
2
|
+
export function extractClaudeText(message) {
|
|
3
|
+
return claudeContentBlocks(message)
|
|
4
|
+
.flatMap((block) => block.type === 'text' && typeof block.text === 'string' ? [block.text] : [])
|
|
5
|
+
.join('');
|
|
6
|
+
}
|
|
7
|
+
export function extractClaudeToolUses(message) {
|
|
8
|
+
return claudeContentBlocks(message).flatMap((block) => {
|
|
9
|
+
if (block.type !== 'tool_use' || typeof block.id !== 'string' || typeof block.name !== 'string')
|
|
10
|
+
return [];
|
|
11
|
+
const input = block.input;
|
|
12
|
+
const command = isPlainRecord(input) && typeof input.command === 'string' ? input.command : undefined;
|
|
13
|
+
return [
|
|
14
|
+
{
|
|
15
|
+
id: block.id,
|
|
16
|
+
name: block.name,
|
|
17
|
+
input,
|
|
18
|
+
...(command === undefined ? {} : { command }),
|
|
19
|
+
inputTruncated: compactRunnerValue(input, 10_000).truncated
|
|
20
|
+
}
|
|
21
|
+
];
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
export function claudeContentBlocks(message) {
|
|
25
|
+
const content = message.message;
|
|
26
|
+
if (!isPlainRecord(content) || !Array.isArray(content.content))
|
|
27
|
+
return [];
|
|
28
|
+
return content.content.filter(isPlainRecord);
|
|
29
|
+
}
|
|
30
|
+
export function isClaudeCommandTool(name) {
|
|
31
|
+
return name === 'Bash' || name === 'bash' || name === 'Shell';
|
|
32
|
+
}
|
|
33
|
+
export function nativeClaudeCommand(entry) {
|
|
34
|
+
if (isPlainRecord(entry.input) && typeof entry.input.command === 'string')
|
|
35
|
+
return compactRunnerText(entry.input.command, 10_000);
|
|
36
|
+
return entry.inputSummary ?? entry.toolName;
|
|
37
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { forEachJsonlRecord, readJsonlWindow } from '../../native-jsonl-reader.js';
|
|
2
|
+
import { locateNativeSessionPath } from '../../native-session-history.js';
|
|
3
|
+
import { readFile } from 'node:fs/promises';
|
|
4
|
+
import { homedir } from 'node:os';
|
|
5
|
+
import { join } from 'node:path';
|
|
6
|
+
const CLAUDE_CONTEXT_USAGE_READ_BYTES = [256 * 1024, 1024 * 1024, 4 * 1024 * 1024];
|
|
7
|
+
const DEFAULT_CLAUDE_CONTEXT_WINDOW_TOKENS = 200_000;
|
|
8
|
+
const CLAUDE_CONTEXT_WINDOW_ENV = 'MAR_CLAUDE_CONTEXT_WINDOW_TOKENS';
|
|
9
|
+
export async function readClaudeNativeContextUsage(workspacePath, externalSessionId, options = {}) {
|
|
10
|
+
const path = await locateNativeSessionPath('claude-code', workspacePath, externalSessionId);
|
|
11
|
+
if (path === undefined)
|
|
12
|
+
return undefined;
|
|
13
|
+
const latest = await readLatestClaudeContextUsage(path);
|
|
14
|
+
if (latest === undefined)
|
|
15
|
+
return undefined;
|
|
16
|
+
return {
|
|
17
|
+
...latest,
|
|
18
|
+
maxTokens: await claudeContextWindowTokens(options)
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
async function readLatestClaudeContextUsage(path) {
|
|
22
|
+
for (const tailBytes of CLAUDE_CONTEXT_USAGE_READ_BYTES) {
|
|
23
|
+
const window = await readJsonlWindow(path, { tailBytes });
|
|
24
|
+
if (window === undefined)
|
|
25
|
+
return undefined;
|
|
26
|
+
let latest;
|
|
27
|
+
forEachJsonlRecord(window.content, (record) => {
|
|
28
|
+
const usage = claudeContextUsage(record);
|
|
29
|
+
if (usage !== undefined)
|
|
30
|
+
latest = usage;
|
|
31
|
+
});
|
|
32
|
+
if (latest !== undefined)
|
|
33
|
+
return latest;
|
|
34
|
+
if (!window.truncated)
|
|
35
|
+
return undefined;
|
|
36
|
+
}
|
|
37
|
+
return undefined;
|
|
38
|
+
}
|
|
39
|
+
export async function claudeContextWindowTokens(options = {}) {
|
|
40
|
+
const environment = options.environment ?? process.env;
|
|
41
|
+
const direct = positiveInteger(environment[CLAUDE_CONTEXT_WINDOW_ENV]);
|
|
42
|
+
if (direct !== undefined)
|
|
43
|
+
return direct;
|
|
44
|
+
try {
|
|
45
|
+
const settings = JSON.parse(await readFile(options.settingsPath ?? join(homedir(), '.claude', 'settings.json'), 'utf8'));
|
|
46
|
+
if (isRecord(settings) && isRecord(settings.env)) {
|
|
47
|
+
const configured = positiveInteger(settings.env[CLAUDE_CONTEXT_WINDOW_ENV]);
|
|
48
|
+
if (configured !== undefined)
|
|
49
|
+
return configured;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
// Missing or malformed optional Claude settings use the stable default.
|
|
54
|
+
}
|
|
55
|
+
return DEFAULT_CLAUDE_CONTEXT_WINDOW_TOKENS;
|
|
56
|
+
}
|
|
57
|
+
function claudeContextUsage(value) {
|
|
58
|
+
if (value.type !== 'assistant' || !isRecord(value.message))
|
|
59
|
+
return undefined;
|
|
60
|
+
const model = value.message.model;
|
|
61
|
+
const usage = value.message.usage;
|
|
62
|
+
if (typeof model !== 'string' ||
|
|
63
|
+
model.length === 0 ||
|
|
64
|
+
model === '<synthetic>' ||
|
|
65
|
+
!isRecord(usage))
|
|
66
|
+
return undefined;
|
|
67
|
+
const inputTokens = nonNegativeInteger(usage.input_tokens);
|
|
68
|
+
const cacheCreationTokens = optionalNonNegativeInteger(usage.cache_creation_input_tokens);
|
|
69
|
+
const cacheReadTokens = optionalNonNegativeInteger(usage.cache_read_input_tokens);
|
|
70
|
+
if (inputTokens === undefined ||
|
|
71
|
+
cacheCreationTokens === undefined ||
|
|
72
|
+
cacheReadTokens === undefined)
|
|
73
|
+
return undefined;
|
|
74
|
+
return { model, usedTokens: inputTokens + cacheCreationTokens + cacheReadTokens };
|
|
75
|
+
}
|
|
76
|
+
function optionalNonNegativeInteger(value) {
|
|
77
|
+
return value === undefined ? 0 : nonNegativeInteger(value);
|
|
78
|
+
}
|
|
79
|
+
function nonNegativeInteger(value) {
|
|
80
|
+
return typeof value === 'number' && Number.isSafeInteger(value) && value >= 0 ? value : undefined;
|
|
81
|
+
}
|
|
82
|
+
function positiveInteger(value) {
|
|
83
|
+
if (typeof value !== 'string' || !/^\d+$/.test(value.trim()))
|
|
84
|
+
return undefined;
|
|
85
|
+
const parsed = Number(value);
|
|
86
|
+
return Number.isSafeInteger(parsed) && parsed > 0 ? parsed : undefined;
|
|
87
|
+
}
|
|
88
|
+
function isRecord(value) {
|
|
89
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
90
|
+
}
|
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
import { homedir } from 'node:os';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { forEachJsonlRecord } from '../../native-jsonl-reader.js';
|
|
4
|
+
import { compactRunnerValue as compactValue, isPlainRecord } from '../../util/node-operation-parsers.js';
|
|
5
|
+
import { stripGoalStartPrompt, publicGoalHistoryText } from '../../service/goal-runtime.js';
|
|
6
|
+
import { readdir } from 'node:fs/promises';
|
|
7
|
+
import { stripClaudePlanTag } from '../../runner-command-engine.js';
|
|
8
|
+
import { claudeAssistantBlocks, extractClaudeThinkingText, claudePlanText, claudeUserInputQuestions } from './conversation-parser.js';
|
|
9
|
+
import { extractClaudeToolResults, claudeNativeToolResultMetadata, normalizeClaudeTool, restoreClaudeSubagentTitles } from './tool-normalizer.js';
|
|
10
|
+
function parse(_path, window) {
|
|
11
|
+
const { content, truncated } = window;
|
|
12
|
+
let cwd;
|
|
13
|
+
let id;
|
|
14
|
+
let derivedSession = false;
|
|
15
|
+
const items = [];
|
|
16
|
+
const toolIndexes = new Map();
|
|
17
|
+
const metaRecordIds = new Set();
|
|
18
|
+
const claudeCompactCommandIds = new Set();
|
|
19
|
+
forEachJsonlRecord(content, (value) => {
|
|
20
|
+
if (derivedTranscriptRecord(value))
|
|
21
|
+
derivedSession = true;
|
|
22
|
+
cwd ??= workspacePath(value);
|
|
23
|
+
id ??= sessionId(value);
|
|
24
|
+
if (typeof value['uuid'] === 'string' && isClaudeCompactCommand(value))
|
|
25
|
+
claudeCompactCommandIds.add(value['uuid']);
|
|
26
|
+
});
|
|
27
|
+
forEachJsonlRecord(truncated ? window.pageContent : content, (value) => {
|
|
28
|
+
const recordId = typeof value['uuid'] === 'string' ? value['uuid'] : undefined;
|
|
29
|
+
const parentId = typeof value['parentUuid'] === 'string' ? value['parentUuid'] : undefined;
|
|
30
|
+
if (value['isMeta'] === true && recordId !== undefined)
|
|
31
|
+
metaRecordIds.add(recordId);
|
|
32
|
+
if (parentId !== undefined &&
|
|
33
|
+
claudeCompactCommandIds.has(parentId) &&
|
|
34
|
+
isClaudeCompactSuccess(value)) {
|
|
35
|
+
items.push({
|
|
36
|
+
kind: 'context_compaction',
|
|
37
|
+
phase: 'COMPLETED',
|
|
38
|
+
summary: null,
|
|
39
|
+
createdAt: timestampOf(value),
|
|
40
|
+
nativeId: parentId,
|
|
41
|
+
rewindAfter: parentId
|
|
42
|
+
});
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
if (isHiddenTranscriptRecord(value) ||
|
|
46
|
+
(parentId !== undefined && metaRecordIds.has(parentId))) {
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
for (const parsedItem of transcriptItems(value)) {
|
|
50
|
+
const item = {
|
|
51
|
+
...parsedItem,
|
|
52
|
+
...(recordId === undefined ? {} : { nativeId: recordId, rewindAfter: recordId })
|
|
53
|
+
};
|
|
54
|
+
if (item.kind === 'tool_result') {
|
|
55
|
+
const toolIndex = toolIndexes.get(item.toolUseId);
|
|
56
|
+
if (toolIndex !== undefined)
|
|
57
|
+
items[toolIndex] = withToolResult(items[toolIndex], item);
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
if (item.kind === 'tool_call') {
|
|
61
|
+
toolIndexes.set(item.toolUseId, items.length);
|
|
62
|
+
items.push(item);
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
items.push(item);
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
if (derivedSession)
|
|
69
|
+
return undefined;
|
|
70
|
+
if (cwd === undefined)
|
|
71
|
+
return undefined;
|
|
72
|
+
const title = items.find((item) => item.kind === 'message' && item.role === 'USER')?.text;
|
|
73
|
+
// Tagged Plan Mode instruction labels do not form a session title.
|
|
74
|
+
const strippedTitle = title === undefined ? undefined : stripClaudePlanTag(stripGoalStartPrompt(title));
|
|
75
|
+
return {
|
|
76
|
+
...(id === undefined ? {} : { externalSessionId: id }),
|
|
77
|
+
cwd,
|
|
78
|
+
...(strippedTitle === undefined || strippedTitle.length === 0
|
|
79
|
+
? {}
|
|
80
|
+
: { title: compact(strippedTitle) }),
|
|
81
|
+
items
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
function derivedTranscriptRecord(value) {
|
|
85
|
+
return value['isSidechain'] === true;
|
|
86
|
+
}
|
|
87
|
+
function transcriptItems(record) {
|
|
88
|
+
if (record.type === 'assistant') {
|
|
89
|
+
return claudeAssistantBlocks(record).flatMap(({ record: blockRecord, blockIndex, sourceItemId }) => {
|
|
90
|
+
const block = contentBlocks(blockRecord)[0];
|
|
91
|
+
if (block?.type === 'thinking') {
|
|
92
|
+
const text = extractClaudeThinkingText(blockRecord)[0] ?? '';
|
|
93
|
+
return text.length === 0
|
|
94
|
+
? []
|
|
95
|
+
: [
|
|
96
|
+
{
|
|
97
|
+
kind: 'reasoning_summary',
|
|
98
|
+
representation: 'visible_thinking',
|
|
99
|
+
sections: [{ index: blockIndex, text }],
|
|
100
|
+
createdAt: timestampOf(record),
|
|
101
|
+
...(sourceItemId === undefined ? {} : { sourceItemId })
|
|
102
|
+
}
|
|
103
|
+
];
|
|
104
|
+
}
|
|
105
|
+
return transcriptRecordItems(blockRecord).map((item) => ({
|
|
106
|
+
...item,
|
|
107
|
+
...(sourceItemId === undefined ? {} : { sourceItemId })
|
|
108
|
+
}));
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
return transcriptRecordItems(record);
|
|
112
|
+
}
|
|
113
|
+
function transcriptRecordItems(record) {
|
|
114
|
+
const createdAt = timestampOf(record);
|
|
115
|
+
const results = toolResultsOf(record, createdAt);
|
|
116
|
+
if (results.length > 0)
|
|
117
|
+
return results;
|
|
118
|
+
const values = [
|
|
119
|
+
...toolUsesOf(record, createdAt),
|
|
120
|
+
...unsupportedTranscriptItems(record, createdAt)
|
|
121
|
+
];
|
|
122
|
+
const role = roleOf(record);
|
|
123
|
+
if (role === undefined)
|
|
124
|
+
return values;
|
|
125
|
+
const rawText = messageText(textOf(record) ?? '');
|
|
126
|
+
if (rawText.length === 0)
|
|
127
|
+
return values;
|
|
128
|
+
values.unshift({ kind: 'message', role, text: rawText, createdAt });
|
|
129
|
+
return values;
|
|
130
|
+
}
|
|
131
|
+
function toolUsesOf(record, createdAt) {
|
|
132
|
+
return contentBlocks(record).flatMap((block) => {
|
|
133
|
+
if (block['type'] !== 'tool_use' ||
|
|
134
|
+
typeof block['id'] !== 'string' ||
|
|
135
|
+
typeof block['name'] !== 'string')
|
|
136
|
+
return [];
|
|
137
|
+
const input = block['input'];
|
|
138
|
+
const summary = compactValue(input, 10_000);
|
|
139
|
+
return [
|
|
140
|
+
{
|
|
141
|
+
kind: 'tool_call',
|
|
142
|
+
toolUseId: block['id'],
|
|
143
|
+
toolName: compact(block['name']).slice(0, 128),
|
|
144
|
+
input,
|
|
145
|
+
inputSummary: summary.text || null,
|
|
146
|
+
inputTruncated: summary.truncated,
|
|
147
|
+
outputSummary: null,
|
|
148
|
+
outputTruncated: false,
|
|
149
|
+
failed: false,
|
|
150
|
+
createdAt
|
|
151
|
+
}
|
|
152
|
+
];
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
function toolResultsOf(record, createdAt) {
|
|
156
|
+
return extractClaudeToolResults(record).map((result) => ({
|
|
157
|
+
...result,
|
|
158
|
+
kind: 'tool_result',
|
|
159
|
+
createdAt
|
|
160
|
+
}));
|
|
161
|
+
}
|
|
162
|
+
function isClaudeCompactCommand(record) {
|
|
163
|
+
return textOf(record)?.trim() === '<command-name>/compact</command-name>';
|
|
164
|
+
}
|
|
165
|
+
function isClaudeCompactSuccess(record) {
|
|
166
|
+
return textOf(record)?.trim() === '<local-command-stdout>Compacted</local-command-stdout>';
|
|
167
|
+
}
|
|
168
|
+
function withToolResult(call, result) {
|
|
169
|
+
return {
|
|
170
|
+
...call,
|
|
171
|
+
outputSummary: result.output,
|
|
172
|
+
outputTruncated: result.truncated,
|
|
173
|
+
failed: result.failed,
|
|
174
|
+
...(result.claudeResult === undefined
|
|
175
|
+
? {}
|
|
176
|
+
: {
|
|
177
|
+
subagentResult: {
|
|
178
|
+
...(result.claudeResult.agentId === undefined
|
|
179
|
+
? {}
|
|
180
|
+
: { agentId: result.claudeResult.agentId }),
|
|
181
|
+
...(result.claudeResult.task_id === undefined
|
|
182
|
+
? {}
|
|
183
|
+
: { taskId: result.claudeResult.task_id }),
|
|
184
|
+
...(result.claudeResult.task_type === undefined
|
|
185
|
+
? {}
|
|
186
|
+
: { taskType: result.claudeResult.task_type })
|
|
187
|
+
}
|
|
188
|
+
})
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
function workspacePath(record) {
|
|
192
|
+
return (firstString(record, ['cwd', 'working_directory', 'workspacePath']) ??
|
|
193
|
+
nestedString(record, ['payload', 'cwd']) ??
|
|
194
|
+
nestedString(record, ['message', 'cwd']));
|
|
195
|
+
}
|
|
196
|
+
function sessionId(record) {
|
|
197
|
+
return (firstString(record, ['sessionId', 'session_id', 'thread_id', 'conversationId']) ??
|
|
198
|
+
nestedString(record, ['payload', 'id']) ??
|
|
199
|
+
nestedString(record, ['payload', 'session_id']) ??
|
|
200
|
+
nestedString(record, ['payload', 'thread_id']));
|
|
201
|
+
}
|
|
202
|
+
function timestampOf(record) {
|
|
203
|
+
const raw = record['timestamp'] ?? record['created_at'] ?? record['createdAt'];
|
|
204
|
+
if (typeof raw === 'number' && Number.isFinite(raw))
|
|
205
|
+
return raw < 10_000_000_000 ? raw * 1_000 : raw;
|
|
206
|
+
if (typeof raw === 'string') {
|
|
207
|
+
const value = Date.parse(raw);
|
|
208
|
+
return Number.isNaN(value) ? null : value;
|
|
209
|
+
}
|
|
210
|
+
return null;
|
|
211
|
+
}
|
|
212
|
+
function firstString(value, keys) {
|
|
213
|
+
for (const key of keys)
|
|
214
|
+
if (typeof value[key] === 'string' && value[key].length > 0)
|
|
215
|
+
return value[key];
|
|
216
|
+
return undefined;
|
|
217
|
+
}
|
|
218
|
+
function nestedString(value, keys) {
|
|
219
|
+
let current = value;
|
|
220
|
+
for (const key of keys) {
|
|
221
|
+
if (!isRecord(current))
|
|
222
|
+
return undefined;
|
|
223
|
+
current = current[key];
|
|
224
|
+
}
|
|
225
|
+
return typeof current === 'string' && current.length > 0 ? current : undefined;
|
|
226
|
+
}
|
|
227
|
+
function compact(value) {
|
|
228
|
+
return value.replace(/\s+/g, ' ').trim().slice(0, 16_000);
|
|
229
|
+
}
|
|
230
|
+
function messageText(value) {
|
|
231
|
+
return value.trim().slice(0, 16_000);
|
|
232
|
+
}
|
|
233
|
+
function roleOf(record) {
|
|
234
|
+
const candidates = [
|
|
235
|
+
firstString(record, ['role']),
|
|
236
|
+
nestedString(record, ['message', 'role']),
|
|
237
|
+
nestedString(record, ['payload', 'role']),
|
|
238
|
+
firstString(record, ['type']),
|
|
239
|
+
nestedString(record, ['payload', 'type'])
|
|
240
|
+
];
|
|
241
|
+
if (candidates.some((value) => value === 'user' || value === 'human' || value === 'input'))
|
|
242
|
+
return 'USER';
|
|
243
|
+
if (candidates.some((value) => value === 'assistant' || value === 'agent' || value === 'output'))
|
|
244
|
+
return 'ASSISTANT';
|
|
245
|
+
return undefined;
|
|
246
|
+
}
|
|
247
|
+
function textOf(record) {
|
|
248
|
+
return (firstString(record, ['content', 'text']) ??
|
|
249
|
+
nestedText(record['message']) ??
|
|
250
|
+
nestedText(record['payload']));
|
|
251
|
+
}
|
|
252
|
+
function nestedText(value) {
|
|
253
|
+
if (typeof value === 'string')
|
|
254
|
+
return value;
|
|
255
|
+
if (!isRecord(value))
|
|
256
|
+
return undefined;
|
|
257
|
+
const direct = firstString(value, ['content', 'text', 'message']);
|
|
258
|
+
if (direct !== undefined)
|
|
259
|
+
return direct;
|
|
260
|
+
if (Array.isArray(value['content'])) {
|
|
261
|
+
const parts = value['content'].flatMap((part) => {
|
|
262
|
+
if (typeof part === 'string')
|
|
263
|
+
return [part];
|
|
264
|
+
if (!isRecord(part))
|
|
265
|
+
return [];
|
|
266
|
+
return [firstString(part, ['text', 'content'])].filter((text) => text !== undefined);
|
|
267
|
+
});
|
|
268
|
+
return parts.join('\n');
|
|
269
|
+
}
|
|
270
|
+
return undefined;
|
|
271
|
+
}
|
|
272
|
+
function contentBlocks(record) {
|
|
273
|
+
const values = [record['message'], record['payload']];
|
|
274
|
+
for (const value of values)
|
|
275
|
+
if (isRecord(value) && Array.isArray(value['content']))
|
|
276
|
+
return value['content'].filter(isRecord);
|
|
277
|
+
return [];
|
|
278
|
+
}
|
|
279
|
+
function isHiddenTranscriptRecord(record) {
|
|
280
|
+
if (contentBlocks(record).some((block) => block['type'] === 'tool_result'))
|
|
281
|
+
return false;
|
|
282
|
+
if (record['interruptedByShutdown'] === true ||
|
|
283
|
+
record['isMeta'] === true ||
|
|
284
|
+
record['isVisibleInTranscriptOnly'] === true ||
|
|
285
|
+
record['isCompactSummary'] === true)
|
|
286
|
+
return true;
|
|
287
|
+
const text = textOf(record)?.trim() ?? '';
|
|
288
|
+
return (text.startsWith('<local-command-') ||
|
|
289
|
+
text.startsWith('<command-name>') ||
|
|
290
|
+
text.startsWith('<command-message>'));
|
|
291
|
+
}
|
|
292
|
+
function unsupportedTranscriptItems(record, createdAt) {
|
|
293
|
+
return contentBlocks(record).flatMap((block) => {
|
|
294
|
+
const type = typeof block.type === 'string' ? block.type : undefined;
|
|
295
|
+
if (type === undefined ||
|
|
296
|
+
type === 'text' ||
|
|
297
|
+
type === 'input_text' ||
|
|
298
|
+
type === 'output_text' ||
|
|
299
|
+
type === 'image' ||
|
|
300
|
+
type === 'input_image' ||
|
|
301
|
+
type === 'thinking' ||
|
|
302
|
+
type === 'redacted_thinking' ||
|
|
303
|
+
type === 'encrypted_thinking' ||
|
|
304
|
+
type === 'tool_use' ||
|
|
305
|
+
type === 'tool_result')
|
|
306
|
+
return [];
|
|
307
|
+
return [
|
|
308
|
+
{
|
|
309
|
+
kind: 'unknown',
|
|
310
|
+
sourceEventType: `transcript:${type}`,
|
|
311
|
+
label: `Agent ${compact(type).slice(0, 128)}`,
|
|
312
|
+
createdAt
|
|
313
|
+
}
|
|
314
|
+
];
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
function isRecord(value) {
|
|
318
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
319
|
+
}
|
|
320
|
+
export const claudeNativeTranscriptAdapter = {
|
|
321
|
+
runner: 'claude-code',
|
|
322
|
+
transcriptRoot: () => join(homedir(), '.claude', 'projects'),
|
|
323
|
+
includesPath: (path) => !path.split(/[\\/]/u).includes('subagents'),
|
|
324
|
+
async locateCandidates(externalSessionId) {
|
|
325
|
+
try {
|
|
326
|
+
const root = this.transcriptRoot();
|
|
327
|
+
return (await readdir(root, { withFileTypes: true }))
|
|
328
|
+
.filter((entry) => entry.isDirectory())
|
|
329
|
+
.map((entry) => join(root, entry.name, `${externalSessionId}.jsonl`));
|
|
330
|
+
}
|
|
331
|
+
catch {
|
|
332
|
+
return [];
|
|
333
|
+
}
|
|
334
|
+
},
|
|
335
|
+
parse,
|
|
336
|
+
createProjection(precedingItems) {
|
|
337
|
+
const titles = restoreClaudeSubagentTitles(precedingItems.filter((entry) => entry.kind === 'tool_call'));
|
|
338
|
+
return {
|
|
339
|
+
userText: (text) => publicGoalHistoryText(stripClaudePlanTag(text)),
|
|
340
|
+
planText: claudePlanText,
|
|
341
|
+
normalizeTool: (entry) => normalizeClaudeTool(entry.toolName, entry.input, entry.outputSummary, entry.failed, entry.inputTruncated || entry.outputTruncated, titles, claudeNativeToolResultMetadata(entry.subagentResult)),
|
|
342
|
+
userInputQuestions: (entry) => entry.toolName === 'AskUserQuestion' && isPlainRecord(entry.input)
|
|
343
|
+
? claudeUserInputQuestions(entry.input)
|
|
344
|
+
: []
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
};
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { claudeContentBlocks
|
|
1
|
+
import { claudeContentBlocks } from './message-parsers.js';
|
|
2
|
+
import { compactRunnerText, compactRunnerValue } from '../../util/node-operation-parsers.js';
|
|
2
3
|
import { RunnerUsageTracker, usageToken, usageTokenSum } from '../../service/runner-usage-tracker.js';
|
|
3
4
|
export class ClaudeUsageNormalizer {
|
|
4
5
|
tracker;
|
|
@@ -46,10 +47,19 @@ export class ClaudeUsageNormalizer {
|
|
|
46
47
|
this.tracker.finish(state);
|
|
47
48
|
}
|
|
48
49
|
}
|
|
50
|
+
export function claudeNativeToolResultMetadata(result) {
|
|
51
|
+
if (result === undefined)
|
|
52
|
+
return undefined;
|
|
53
|
+
return {
|
|
54
|
+
...(result.agentId === undefined ? {} : { agentId: result.agentId }),
|
|
55
|
+
...(result.taskId === undefined ? {} : { task_id: result.taskId }),
|
|
56
|
+
...(result.taskType === undefined ? {} : { task_type: result.taskType })
|
|
57
|
+
};
|
|
58
|
+
}
|
|
49
59
|
export function restoreClaudeSubagentTitles(calls) {
|
|
50
60
|
const titles = new Map();
|
|
51
61
|
for (const call of calls)
|
|
52
|
-
claudeSubagentProjection(call.toolName, call.input, call.
|
|
62
|
+
claudeSubagentProjection(call.toolName, call.input, claudeNativeToolResultMetadata(call.subagentResult) ?? call.outputSummary, titles);
|
|
53
63
|
return titles;
|
|
54
64
|
}
|
|
55
65
|
export function extractClaudeToolResults(message) {
|