farming-code 2.2.28 → 2.2.30
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/README.md +27 -90
- package/README.zh_cn.md +17 -90
- package/THIRD_PARTY_NOTICES.md +25 -6
- package/backend/acp/packaged-claude-acp.js +13 -0
- package/backend/acp/packaged-codex-acp.js +2 -2
- package/backend/acp-runtime.js +38 -17
- package/backend/acp-session-state.js +78 -88
- package/backend/acp-transcript.js +174 -9
- package/backend/agent-manager.js +27 -2
- package/backend/agent-session-history.js +45 -12
- package/backend/codex-session-history.js +1 -1
- package/backend/codex-transcript.js +0 -202
- package/backend/config-manager.js +44 -16
- package/backend/data/LICENSE.agent-browser +201 -0
- package/backend/data/runtime-dependency-manifest.json +199 -0
- package/backend/data/runtime-dependency-sources.json +5 -0
- package/backend/executable-discovery.js +7 -0
- package/backend/farming-agent-bootstrap.zh_cn.md +4 -2
- package/backend/farming-app-cli.js +161 -65
- package/backend/farming-cli.js +7 -59
- package/backend/index-html.js +31 -0
- package/backend/main-agent-skills.js +7 -28
- package/backend/npm-update-helper.js +43 -6
- package/backend/provider-adapters.js +10 -0
- package/backend/runtime-dependency-manager.js +759 -0
- package/backend/runtime-executable-invocation.js +20 -0
- package/backend/server-process-identity.js +47 -0
- package/backend/server.js +145 -204
- package/backend/slash-command-discovery.js +138 -1
- package/backend/storage-layout.js +30 -0
- package/backend/update-service.js +60 -914
- package/config/farming.deploy.env.example +1 -1
- package/config/farming.install.env.example +5 -1
- package/dist/acp/LICENSE.claude-agent-acp +191 -0
- package/dist/acp/LICENSE.claude-agent-sdk +1 -0
- package/dist/acp/claude-agent-acp-0.59.0.mjs +41606 -0
- package/dist/acp/{codex-acp-1.1.4.js → codex-acp-1.1.4.mjs} +387 -7
- package/dist/assets/{App-CM7Cy-l4.css → App-BKNUILuL.css} +1 -1
- package/dist/assets/App-Z1N7T0eM.js +695 -0
- package/dist/assets/{FileEditorMarkdownPreview-DYOMm-b1.js → FileEditorMarkdownPreview-Cs9cLv8a.js} +1 -1
- package/dist/assets/FileEditorPane-CNtuGjQ3.js +2 -0
- package/dist/assets/IconGlyphs-BClgag6E.js +1 -0
- package/dist/assets/ProjectFilesSection-K3GHuQ4U.js +12 -0
- package/dist/assets/ReviewPage-BRuxFaH5.js +3 -0
- package/dist/assets/code-dark-kLsQLvpf.css +1 -0
- package/dist/assets/{index-BzQO3pUr.js → index-D-zetSol.js} +2 -2
- package/dist/assets/main-DUTz-Rd2.css +1 -0
- package/dist/assets/{workspace-editor-monaco-tXA-udGx.js → workspace-editor-monaco-CJNwbpCy.js} +1 -1
- package/dist/assets/workspace-editor-monaco-CO9w8YV0.js +1 -0
- package/dist/assets/workspace-view-state-BjKPYp0E.js +7 -0
- package/dist/index.html +33 -3
- package/extensions/browser/backend/agent-browser-runtime.js +1041 -0
- package/extensions/browser/backend/browser-mcp-server.js +352 -0
- package/extensions/browser/backend/browser-resource-manager.js +921 -114
- package/extensions/browser/backend/browser-resource-store.js +47 -6
- package/extensions/browser/backend/browser-router.js +15 -2
- package/extensions/browser/backend/executable-discovery.js +164 -17
- package/extensions/browser/backend/managed-chromium-installer.js +593 -0
- package/extensions/browser/bin/farming-browser +384 -30
- package/extensions/browser/frontend/BrowserSidebarPortals.tsx +17 -19
- package/extensions/browser/frontend/BrowserViewer.tsx +217 -46
- package/extensions/browser/frontend/browser.css +68 -17
- package/extensions/browser/frontend/types.ts +13 -0
- package/extensions/browser/frontend/useBrowserResources.ts +18 -3
- package/index.html +32 -2
- package/package.json +10 -6
- package/shared/backend-connection-status.d.ts +12 -0
- package/shared/backend-connection-status.js +35 -4
- package/backend/agent-activity.js +0 -35
- package/backend/agent-memory-report.js +0 -583
- package/backend/bundle-update-helper.js +0 -113
- package/backend/codex-rollout-follower.js +0 -326
- package/dist/assets/App-CPG4WcjO.js +0 -658
- package/dist/assets/FileEditorPane-DXXq82aM.js +0 -2
- package/dist/assets/IconGlyphs-DrdPjYXr.js +0 -1
- package/dist/assets/ProjectFilesSection-CP66XVPl.js +0 -12
- package/dist/assets/ReviewPage-w21MEPD6.js +0 -3
- package/dist/assets/code-dark-BDjwj7Um.css +0 -1
- package/dist/assets/main-BlVUXfxa.css +0 -1
- package/dist/assets/workspace-editor-monaco-CFEfFNcA.js +0 -1
- package/dist/assets/workspace-view-state-BS_V8JVs.js +0 -7
- package/extensions/browser/backend/cdp-browser-runtime.js +0 -648
- package/extensions/browser/backend/cdp-client.js +0 -167
|
@@ -1,21 +1,9 @@
|
|
|
1
|
-
const fsp = require('fs/promises');
|
|
2
|
-
const path = require('path');
|
|
3
|
-
const { fileURLToPath } = require('url');
|
|
4
|
-
|
|
5
1
|
const MAX_ACP_UPDATES = 2_000;
|
|
6
2
|
const MAX_ACP_UPDATE_LOG_VALUE_CHARS = 32 * 1024;
|
|
7
|
-
const
|
|
8
|
-
const
|
|
9
|
-
const
|
|
10
|
-
'.gif': 'image/gif',
|
|
11
|
-
'.jpeg': 'image/jpeg',
|
|
12
|
-
'.jpg': 'image/jpeg',
|
|
13
|
-
'.png': 'image/png',
|
|
14
|
-
'.svg': 'image/svg+xml',
|
|
15
|
-
'.webp': 'image/webp',
|
|
16
|
-
});
|
|
3
|
+
const MAX_CODEX_SUBAGENTS = 128;
|
|
4
|
+
const MAX_CODEX_SUBAGENT_ID_CHARS = 160;
|
|
5
|
+
const MAX_CODEX_SUBAGENT_NAME_CHARS = 120;
|
|
17
6
|
const {
|
|
18
|
-
codexHistoryImageTargets,
|
|
19
7
|
visibleUserMessageText,
|
|
20
8
|
} = require('./codex-transcript');
|
|
21
9
|
const {
|
|
@@ -27,6 +15,54 @@ function clone(value) {
|
|
|
27
15
|
return value === undefined ? undefined : JSON.parse(JSON.stringify(value));
|
|
28
16
|
}
|
|
29
17
|
|
|
18
|
+
function boundedSubagentId(value) {
|
|
19
|
+
return String(value || '').trim().slice(0, MAX_CODEX_SUBAGENT_ID_CHARS);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function normalizeCodexSubagentStatus(value) {
|
|
23
|
+
const status = String(value || '');
|
|
24
|
+
return [
|
|
25
|
+
'pendingInit',
|
|
26
|
+
'running',
|
|
27
|
+
'completed',
|
|
28
|
+
'interrupted',
|
|
29
|
+
'errored',
|
|
30
|
+
'shutdown',
|
|
31
|
+
'notFound',
|
|
32
|
+
].includes(status) ? status : '';
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function normalizeCodexSubagentUpdate(raw, sessionId) {
|
|
36
|
+
if (!raw || raw.version !== 1 || !Array.isArray(raw.agents)) return null;
|
|
37
|
+
const rootThreadId = boundedSubagentId(raw.rootThreadId);
|
|
38
|
+
if (!rootThreadId || rootThreadId !== sessionId) return null;
|
|
39
|
+
const agents = [];
|
|
40
|
+
const seen = new Set();
|
|
41
|
+
for (const candidate of raw.agents.slice(0, MAX_CODEX_SUBAGENTS)) {
|
|
42
|
+
const threadId = boundedSubagentId(candidate?.threadId);
|
|
43
|
+
const status = normalizeCodexSubagentStatus(candidate?.status);
|
|
44
|
+
if (!threadId || threadId === rootThreadId || !status || seen.has(threadId)) continue;
|
|
45
|
+
seen.add(threadId);
|
|
46
|
+
const parentThreadId = boundedSubagentId(candidate?.parentThreadId);
|
|
47
|
+
const name = String(candidate?.name || '').trim().slice(0, MAX_CODEX_SUBAGENT_NAME_CHARS);
|
|
48
|
+
agents.push({
|
|
49
|
+
threadId,
|
|
50
|
+
parentThreadId: parentThreadId && parentThreadId !== threadId ? parentThreadId : null,
|
|
51
|
+
...(name ? { name } : {}),
|
|
52
|
+
status,
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
return {
|
|
56
|
+
version: 1,
|
|
57
|
+
rootThreadId,
|
|
58
|
+
revision: Number.isFinite(Number(raw.revision))
|
|
59
|
+
? Math.max(0, Math.floor(Number(raw.revision)))
|
|
60
|
+
: 0,
|
|
61
|
+
kind: raw.kind === 'delta' ? 'delta' : 'snapshot',
|
|
62
|
+
agents,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
30
66
|
function compactUpdateForLog(update) {
|
|
31
67
|
let serialized = '';
|
|
32
68
|
try {
|
|
@@ -62,40 +98,6 @@ function contentText(content) {
|
|
|
62
98
|
.join('');
|
|
63
99
|
}
|
|
64
100
|
|
|
65
|
-
function localPathFromHistoryImageTarget(value) {
|
|
66
|
-
const target = String(value || '').trim();
|
|
67
|
-
if (!target || target.includes('\0') || /^data:/i.test(target)) return '';
|
|
68
|
-
if (/^file:\/\//i.test(target)) {
|
|
69
|
-
try {
|
|
70
|
-
return fileURLToPath(target);
|
|
71
|
-
} catch {
|
|
72
|
-
return '';
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
return path.isAbsolute(target) ? target : '';
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
function imageBlockFromDataUrl(value) {
|
|
79
|
-
const match = String(value || '').match(/^data:(image\/(?:gif|jpe?g|png|svg\+xml|webp));base64,([a-z0-9+/=]+)$/i);
|
|
80
|
-
if (!match) return null;
|
|
81
|
-
const data = match[2];
|
|
82
|
-
if (!data || Math.ceil(data.length * 3 / 4) > MAX_CODEX_HISTORY_IMAGE_BYTES) return null;
|
|
83
|
-
return { type: 'image', mimeType: match[1].toLowerCase(), data };
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
async function imageBlockFromLocalPath(filePath) {
|
|
87
|
-
const ext = path.extname(filePath).toLowerCase();
|
|
88
|
-
const mimeType = CODEX_HISTORY_IMAGE_MIME_BY_EXT[ext];
|
|
89
|
-
if (!mimeType) return null;
|
|
90
|
-
try {
|
|
91
|
-
const stat = await fsp.stat(filePath);
|
|
92
|
-
if (!stat.isFile() || stat.size <= 0 || stat.size > MAX_CODEX_HISTORY_IMAGE_BYTES) return null;
|
|
93
|
-
return { type: 'image', mimeType, data: (await fsp.readFile(filePath)).toString('base64') };
|
|
94
|
-
} catch {
|
|
95
|
-
return null;
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
|
|
99
101
|
function isContextCompactionText(content) {
|
|
100
102
|
const text = contentText(content).trim();
|
|
101
103
|
return /^\*?Context compacted(?: to fit the model's context window)?\.?\*?$/i.test(text)
|
|
@@ -157,6 +159,7 @@ class AcpSessionState {
|
|
|
157
159
|
this.configOptions = [];
|
|
158
160
|
this.title = '';
|
|
159
161
|
this.updatedAt = '';
|
|
162
|
+
this.codexSubagents = null;
|
|
160
163
|
this.truncated = false;
|
|
161
164
|
this.sequence = 0;
|
|
162
165
|
this.revision = Number.isFinite(Number(options.revisionBase))
|
|
@@ -202,6 +205,7 @@ class AcpSessionState {
|
|
|
202
205
|
state.configOptions = clone(checkpoint.configOptions || []);
|
|
203
206
|
state.title = String(checkpoint.title || '');
|
|
204
207
|
state.updatedAt = String(checkpoint.updatedAt || '');
|
|
208
|
+
state.codexSubagents = clone(checkpoint.codexSubagents ?? null);
|
|
205
209
|
state.truncated = checkpoint.truncated === true;
|
|
206
210
|
return state;
|
|
207
211
|
}
|
|
@@ -282,44 +286,6 @@ class AcpSessionState {
|
|
|
282
286
|
// History replay uses the same reducer as live updates; nothing to close.
|
|
283
287
|
}
|
|
284
288
|
|
|
285
|
-
hasCodexHistoryImageReferences() {
|
|
286
|
-
return this.provider === 'codex' && this.entries.some(entry => (
|
|
287
|
-
entry?.type === 'message'
|
|
288
|
-
&& entry.role === 'user'
|
|
289
|
-
&& codexHistoryImageTargets(contentText(entry.content)).length > 0
|
|
290
|
-
));
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
async hydrateCodexHistoryAttachments(options = {}) {
|
|
294
|
-
if (this.provider !== 'codex') return 0;
|
|
295
|
-
const imageDataByPath = options.imageDataByPath instanceof Map ? options.imageDataByPath : new Map();
|
|
296
|
-
let hydrated = 0;
|
|
297
|
-
for (const entry of this.entries) {
|
|
298
|
-
if (entry?.type !== 'message' || entry.role !== 'user') continue;
|
|
299
|
-
const existingImages = (entry.content || []).filter(content => content?.type === 'image');
|
|
300
|
-
const remaining = MAX_CODEX_HISTORY_IMAGES_PER_MESSAGE - existingImages.length;
|
|
301
|
-
if (remaining <= 0) continue;
|
|
302
|
-
const targets = codexHistoryImageTargets(contentText(entry.content));
|
|
303
|
-
const seenPaths = new Set();
|
|
304
|
-
const blocks = [];
|
|
305
|
-
for (const target of targets) {
|
|
306
|
-
const filePath = localPathFromHistoryImageTarget(target);
|
|
307
|
-
if (!filePath || seenPaths.has(filePath)) continue;
|
|
308
|
-
seenPaths.add(filePath);
|
|
309
|
-
const fallback = imageDataByPath.get(filePath);
|
|
310
|
-
const block = await imageBlockFromLocalPath(filePath) || imageBlockFromDataUrl(fallback);
|
|
311
|
-
if (!block) continue;
|
|
312
|
-
blocks.push(block);
|
|
313
|
-
if (blocks.length >= remaining) break;
|
|
314
|
-
}
|
|
315
|
-
if (blocks.length === 0) continue;
|
|
316
|
-
entry.content.push(...blocks);
|
|
317
|
-
this.touchEntry(entry);
|
|
318
|
-
hydrated += blocks.length;
|
|
319
|
-
}
|
|
320
|
-
return hydrated;
|
|
321
|
-
}
|
|
322
|
-
|
|
323
289
|
apply(notification) {
|
|
324
290
|
if (!notification || notification.sessionId !== this.sessionId) return false;
|
|
325
291
|
const update = notification.update;
|
|
@@ -360,10 +326,31 @@ class AcpSessionState {
|
|
|
360
326
|
} else if (kind === 'session_info_update') {
|
|
361
327
|
if (Object.prototype.hasOwnProperty.call(update, 'title')) this.title = String(update.title || '');
|
|
362
328
|
if (Object.prototype.hasOwnProperty.call(update, 'updatedAt')) this.updatedAt = String(update.updatedAt || '');
|
|
329
|
+
this.applyCodexSubagentUpdate(update?._meta?.codex?.subagents);
|
|
363
330
|
}
|
|
364
331
|
return true;
|
|
365
332
|
}
|
|
366
333
|
|
|
334
|
+
applyCodexSubagentUpdate(raw) {
|
|
335
|
+
const update = normalizeCodexSubagentUpdate(raw, this.sessionId);
|
|
336
|
+
if (!update) return false;
|
|
337
|
+
const agentsById = update.kind === 'delta' && this.codexSubagents?.rootThreadId === update.rootThreadId
|
|
338
|
+
? new Map(this.codexSubagents.agents.map(agent => [agent.threadId, agent]))
|
|
339
|
+
: new Map();
|
|
340
|
+
for (const agent of update.agents) agentsById.set(agent.threadId, agent);
|
|
341
|
+
this.codexSubagents = {
|
|
342
|
+
version: 1,
|
|
343
|
+
rootThreadId: update.rootThreadId,
|
|
344
|
+
revision: update.revision,
|
|
345
|
+
agents: [...agentsById.values()].slice(0, MAX_CODEX_SUBAGENTS),
|
|
346
|
+
};
|
|
347
|
+
// Session metadata changes are visible transcript state even when no
|
|
348
|
+
// message/tool entry changed. Advance the reducer revision so the existing
|
|
349
|
+
// revision-driven transcript read reconciles this snapshot without polling.
|
|
350
|
+
this.revision += 1;
|
|
351
|
+
return true;
|
|
352
|
+
}
|
|
353
|
+
|
|
367
354
|
applyMessageChunk(update, kind) {
|
|
368
355
|
const role = kind === 'user_message_chunk' ? 'user' : 'assistant';
|
|
369
356
|
const type = kind === 'agent_thought_chunk' ? 'thought' : 'message';
|
|
@@ -523,6 +510,7 @@ class AcpSessionState {
|
|
|
523
510
|
revision: this.revision,
|
|
524
511
|
delta,
|
|
525
512
|
hasMoreBefore: startIndex > 0,
|
|
513
|
+
codexSubagents: clone(this.codexSubagents),
|
|
526
514
|
};
|
|
527
515
|
}
|
|
528
516
|
|
|
@@ -564,7 +552,7 @@ class AcpSessionState {
|
|
|
564
552
|
if (
|
|
565
553
|
entry.type === 'message'
|
|
566
554
|
&& entry.role === 'user'
|
|
567
|
-
&& (
|
|
555
|
+
&& (entry.content || []).some(content => content?.type === 'image')
|
|
568
556
|
) {
|
|
569
557
|
renderedAttachmentKinds.push('image');
|
|
570
558
|
}
|
|
@@ -608,6 +596,7 @@ class AcpSessionState {
|
|
|
608
596
|
availableCommands: clone(this.availableCommands),
|
|
609
597
|
currentModeId: this.currentModeId,
|
|
610
598
|
configOptions: clone(this.configOptions),
|
|
599
|
+
codexSubagents: clone(this.codexSubagents),
|
|
611
600
|
...clone(extra),
|
|
612
601
|
};
|
|
613
602
|
if (options.includeUpdates === true) snapshot.updates = clone(this.updates);
|
|
@@ -632,6 +621,7 @@ class AcpSessionState {
|
|
|
632
621
|
configOptions: clone(this.configOptions),
|
|
633
622
|
title: this.title,
|
|
634
623
|
updatedAt: this.updatedAt,
|
|
624
|
+
codexSubagents: clone(this.codexSubagents),
|
|
635
625
|
truncated: this.truncated,
|
|
636
626
|
};
|
|
637
627
|
}
|
|
@@ -1,12 +1,31 @@
|
|
|
1
|
+
const crypto = require('crypto');
|
|
1
2
|
const { createTwoFilesPatch, diffLines } = require('diff');
|
|
2
3
|
|
|
3
4
|
const MAX_RENDERED_DIFF_CHARS = 64 * 1024;
|
|
4
5
|
const MAX_INLINE_TOOL_DETAIL_CHARS = 4 * 1024;
|
|
6
|
+
const MAX_TRANSCRIPT_INLINE_TOOL_DETAIL_CHARS = 64 * 1024;
|
|
7
|
+
const MAX_TRANSCRIPT_MEDIA_BYTES = 25 * 1024 * 1024;
|
|
8
|
+
const MAX_TRANSCRIPT_MEDIA_BASE64_CHARS = Math.ceil(MAX_TRANSCRIPT_MEDIA_BYTES / 3) * 4;
|
|
5
9
|
const MAX_EMBEDDED_RESOURCE_TEXT_CHARS = 4 * 1024;
|
|
6
10
|
const MAX_COLLABORATION_AGENTS = 16;
|
|
7
11
|
const MAX_COLLABORATION_ID_CHARS = 160;
|
|
8
12
|
const MAX_COLLABORATION_PATH_CHARS = 512;
|
|
9
13
|
const MAX_COLLABORATION_MESSAGE_CHARS = 160;
|
|
14
|
+
const TRANSCRIPT_MEDIA_MIME_TYPES = new Set([
|
|
15
|
+
'audio/aac',
|
|
16
|
+
'audio/flac',
|
|
17
|
+
'audio/mp4',
|
|
18
|
+
'audio/mpeg',
|
|
19
|
+
'audio/ogg',
|
|
20
|
+
'audio/wav',
|
|
21
|
+
'audio/wave',
|
|
22
|
+
'audio/webm',
|
|
23
|
+
'audio/x-wav',
|
|
24
|
+
'image/gif',
|
|
25
|
+
'image/jpeg',
|
|
26
|
+
'image/png',
|
|
27
|
+
'image/webp',
|
|
28
|
+
]);
|
|
10
29
|
|
|
11
30
|
function diffBlocks(content) {
|
|
12
31
|
return (Array.isArray(content) ? content : [])
|
|
@@ -184,7 +203,92 @@ function transcriptResource(resourceValue) {
|
|
|
184
203
|
};
|
|
185
204
|
}
|
|
186
205
|
|
|
187
|
-
function
|
|
206
|
+
function transcriptMediaValue(block) {
|
|
207
|
+
if (!block || typeof block !== 'object') return null;
|
|
208
|
+
if (['image', 'audio'].includes(block.type)) return block;
|
|
209
|
+
if (
|
|
210
|
+
block.type === 'content'
|
|
211
|
+
&& block.content
|
|
212
|
+
&& typeof block.content === 'object'
|
|
213
|
+
&& ['image', 'audio'].includes(block.content.type)
|
|
214
|
+
) {
|
|
215
|
+
return block.content;
|
|
216
|
+
}
|
|
217
|
+
return null;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function validatedTranscriptMedia(block) {
|
|
221
|
+
const type = String(block?.type || '');
|
|
222
|
+
const data = typeof block?.data === 'string' ? block.data : '';
|
|
223
|
+
const mimeType = String(block?.mimeType || '').trim().toLowerCase();
|
|
224
|
+
if (
|
|
225
|
+
!['image', 'audio'].includes(type)
|
|
226
|
+
|| !data
|
|
227
|
+
|| data.length > MAX_TRANSCRIPT_MEDIA_BASE64_CHARS
|
|
228
|
+
|| data.length % 4 !== 0
|
|
229
|
+
|| !TRANSCRIPT_MEDIA_MIME_TYPES.has(mimeType)
|
|
230
|
+
|| !/^[a-z0-9+/]*={0,2}$/i.test(data)
|
|
231
|
+
) return null;
|
|
232
|
+
const paddingChars = data.endsWith('==') ? 2 : (data.endsWith('=') ? 1 : 0);
|
|
233
|
+
const decodedBytes = (data.length / 4) * 3 - paddingChars;
|
|
234
|
+
if (decodedBytes <= 0 || decodedBytes > MAX_TRANSCRIPT_MEDIA_BYTES) return null;
|
|
235
|
+
return { data, decodedBytes, mimeType, type };
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function transcriptMediaId(block) {
|
|
239
|
+
const media = validatedTranscriptMedia(block);
|
|
240
|
+
if (!media) return '';
|
|
241
|
+
return crypto.createHash('sha256')
|
|
242
|
+
.update(media.type)
|
|
243
|
+
.update('\0')
|
|
244
|
+
.update(media.mimeType)
|
|
245
|
+
.update('\0')
|
|
246
|
+
.update(media.data)
|
|
247
|
+
.digest('hex');
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function decodeAcpTranscriptMedia(block) {
|
|
251
|
+
const media = validatedTranscriptMedia(block);
|
|
252
|
+
if (!media) return null;
|
|
253
|
+
const content = Buffer.from(media.data, 'base64');
|
|
254
|
+
if (content.length !== media.decodedBytes) return null;
|
|
255
|
+
return {
|
|
256
|
+
content,
|
|
257
|
+
mimeType: media.mimeType,
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function compactTranscriptMedia(block, entry, options = {}) {
|
|
262
|
+
const mediaPathPrefix = String(options.mediaPathPrefix || '').replace(/\/+$/, '');
|
|
263
|
+
if (!mediaPathPrefix) return JSON.parse(JSON.stringify(block));
|
|
264
|
+
const hasInlineData = typeof block?.data === 'string' && block.data.length > 0;
|
|
265
|
+
if (!hasInlineData) return JSON.parse(JSON.stringify(block));
|
|
266
|
+
const mediaId = transcriptMediaId(block);
|
|
267
|
+
const projected = { ...block };
|
|
268
|
+
delete projected.data;
|
|
269
|
+
if (!mediaId) return projected;
|
|
270
|
+
projected.url = `${mediaPathPrefix}/${encodeURIComponent(String(entry?.id || ''))}/${mediaId}`;
|
|
271
|
+
return projected;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function acpTranscriptMedia(entry, requestedMediaId) {
|
|
275
|
+
const mediaId = String(requestedMediaId || '').trim().toLowerCase();
|
|
276
|
+
if (!/^[a-f0-9]{64}$/.test(mediaId)) return null;
|
|
277
|
+
const direct = Array.isArray(entry?.content) ? entry.content : [];
|
|
278
|
+
const output = entry?.rawOutput && typeof entry.rawOutput === 'object' ? entry.rawOutput : {};
|
|
279
|
+
const result = output?.result && typeof output.result === 'object' ? output.result : {};
|
|
280
|
+
const raw = Array.isArray(result.content)
|
|
281
|
+
? result.content
|
|
282
|
+
: (Array.isArray(output.content) ? output.content : []);
|
|
283
|
+
for (const block of [...direct, ...raw]) {
|
|
284
|
+
const media = transcriptMediaValue(block);
|
|
285
|
+
if (!media) continue;
|
|
286
|
+
if (transcriptMediaId(media) === mediaId) return media;
|
|
287
|
+
}
|
|
288
|
+
return null;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function transcriptMediaBlocks(entry, options = {}) {
|
|
188
292
|
const direct = Array.isArray(entry?.content) ? entry.content : [];
|
|
189
293
|
const output = entry?.rawOutput && typeof entry.rawOutput === 'object' ? entry.rawOutput : {};
|
|
190
294
|
const result = output?.result && typeof output.result === 'object' ? output.result : {};
|
|
@@ -194,7 +298,10 @@ function transcriptMediaBlocks(entry) {
|
|
|
194
298
|
const blocks = [...direct, ...raw].flatMap(block => {
|
|
195
299
|
if (!block || typeof block !== 'object') return [];
|
|
196
300
|
if (block.type === 'terminal') return [{ type: 'terminal', terminalId: String(block.terminalId || '') }];
|
|
197
|
-
if (['image', 'audio'
|
|
301
|
+
if (['image', 'audio'].includes(block.type)) {
|
|
302
|
+
return [compactTranscriptMedia(block, entry, options)];
|
|
303
|
+
}
|
|
304
|
+
if (block.type === 'resource_link') return [JSON.parse(JSON.stringify(block))];
|
|
198
305
|
if (block.type === 'resource') {
|
|
199
306
|
return [{
|
|
200
307
|
type: 'resource',
|
|
@@ -203,7 +310,11 @@ function transcriptMediaBlocks(entry) {
|
|
|
203
310
|
}
|
|
204
311
|
if (block.type !== 'content' || !block.content || typeof block.content !== 'object') return [];
|
|
205
312
|
const content = block.content;
|
|
206
|
-
if (['image', 'audio'
|
|
313
|
+
if (['image', 'audio'].includes(content.type)) {
|
|
314
|
+
const projected = compactTranscriptMedia(content, entry, options);
|
|
315
|
+
return [{ type: 'content', content: projected }];
|
|
316
|
+
}
|
|
317
|
+
if (content.type === 'resource_link') {
|
|
207
318
|
return [{ type: 'content', content: JSON.parse(JSON.stringify(content)) }];
|
|
208
319
|
}
|
|
209
320
|
if (content.type === 'resource') {
|
|
@@ -226,6 +337,20 @@ function transcriptMediaBlocks(entry) {
|
|
|
226
337
|
});
|
|
227
338
|
}
|
|
228
339
|
|
|
340
|
+
function transcriptEntryWithCompactMedia(entry, options = {}) {
|
|
341
|
+
const mediaPathPrefix = String(options.mediaPathPrefix || '').trim();
|
|
342
|
+
if (!mediaPathPrefix || !Array.isArray(entry?.content)) return entry;
|
|
343
|
+
const content = entry.content.map(block => {
|
|
344
|
+
const media = transcriptMediaValue(block);
|
|
345
|
+
if (!media) return block;
|
|
346
|
+
const projected = compactTranscriptMedia(media, entry, options);
|
|
347
|
+
return block?.type === 'content'
|
|
348
|
+
? { ...block, content: projected }
|
|
349
|
+
: projected;
|
|
350
|
+
});
|
|
351
|
+
return { ...entry, content };
|
|
352
|
+
}
|
|
353
|
+
|
|
229
354
|
function generatedMediaTool(entry) {
|
|
230
355
|
const title = String(entry?.title || '').trim().toLowerCase();
|
|
231
356
|
const id = String(entry?.id || '').trim().toLowerCase();
|
|
@@ -296,9 +421,13 @@ function transcriptCodexToolMeta(entry) {
|
|
|
296
421
|
return Object.keys(compact).length > 0 ? compact : null;
|
|
297
422
|
}
|
|
298
423
|
|
|
299
|
-
function acpTranscriptToolEntry(entry) {
|
|
424
|
+
function acpTranscriptToolEntry(entry, options = {}) {
|
|
300
425
|
if (!entry || entry.type !== 'tool') return entry;
|
|
301
426
|
const detail = detailForTool(entry);
|
|
427
|
+
const requestedInlineDetailChars = Number(options.maxInlineDetailChars);
|
|
428
|
+
const inlineDetailChars = Number.isFinite(requestedInlineDetailChars)
|
|
429
|
+
? Math.max(0, Math.min(MAX_INLINE_TOOL_DETAIL_CHARS, Math.floor(requestedInlineDetailChars)))
|
|
430
|
+
: MAX_INLINE_TOOL_DETAIL_CHARS;
|
|
302
431
|
const changes = patchChanges(entry.content).map(change => ({
|
|
303
432
|
path: change.path,
|
|
304
433
|
kind: change.kind,
|
|
@@ -313,7 +442,11 @@ function acpTranscriptToolEntry(entry) {
|
|
|
313
442
|
.join('\n');
|
|
314
443
|
const meta = {};
|
|
315
444
|
if (entry?._meta?.subagent_session_info) {
|
|
316
|
-
|
|
445
|
+
const sessionId = boundedCollaborationString(
|
|
446
|
+
entry._meta.subagent_session_info.session_id,
|
|
447
|
+
MAX_COLLABORATION_ID_CHARS
|
|
448
|
+
);
|
|
449
|
+
if (sessionId) meta.subagent_session_info = { session_id: sessionId };
|
|
317
450
|
}
|
|
318
451
|
if (entry?._meta?.farming_patch_decisions) {
|
|
319
452
|
meta.farming_patch_decisions = JSON.parse(JSON.stringify(entry._meta.farming_patch_decisions));
|
|
@@ -326,12 +459,12 @@ function acpTranscriptToolEntry(entry) {
|
|
|
326
459
|
title: String(entry.title || ''),
|
|
327
460
|
kind: String(entry.kind || 'other'),
|
|
328
461
|
status: String(entry.status || ''),
|
|
329
|
-
content: transcriptMediaBlocks(entry),
|
|
462
|
+
content: transcriptMediaBlocks(entry, options),
|
|
330
463
|
...(Object.keys(meta).length > 0 ? { _meta: meta } : {}),
|
|
331
|
-
transcriptDetail: detail.length <=
|
|
464
|
+
transcriptDetail: detail.length <= inlineDetailChars
|
|
332
465
|
? detail
|
|
333
|
-
: `${detail.slice(0,
|
|
334
|
-
transcriptDetailTruncated: detail.length >
|
|
466
|
+
: `${detail.slice(0, inlineDetailChars)}${inlineDetailChars > 0 ? '\n\n' : ''}[Open to load full detail]`,
|
|
467
|
+
transcriptDetailTruncated: detail.length > inlineDetailChars,
|
|
335
468
|
transcriptPatchSummary: patchSummary,
|
|
336
469
|
transcriptChanges: changes,
|
|
337
470
|
generatedMedia: generatedMediaTool(entry),
|
|
@@ -339,9 +472,41 @@ function acpTranscriptToolEntry(entry) {
|
|
|
339
472
|
};
|
|
340
473
|
}
|
|
341
474
|
|
|
475
|
+
function acpTranscriptEntries(entries, options = {}) {
|
|
476
|
+
const requestedBudget = Number(options.maxInlineToolDetailChars);
|
|
477
|
+
let remainingInlineDetailChars = Number.isFinite(requestedBudget)
|
|
478
|
+
? Math.max(0, Math.floor(requestedBudget))
|
|
479
|
+
: MAX_TRANSCRIPT_INLINE_TOOL_DETAIL_CHARS;
|
|
480
|
+
const source = Array.isArray(entries) ? entries : [];
|
|
481
|
+
const projected = new Array(source.length);
|
|
482
|
+
|
|
483
|
+
// The transcript page owns only a bounded summary. Exact tool and subagent
|
|
484
|
+
// detail remains backend-owned and is loaded on explicit expansion.
|
|
485
|
+
for (let index = source.length - 1; index >= 0; index -= 1) {
|
|
486
|
+
const entry = source[index];
|
|
487
|
+
if (entry?.type !== 'tool') {
|
|
488
|
+
projected[index] = transcriptEntryWithCompactMedia(entry, options);
|
|
489
|
+
continue;
|
|
490
|
+
}
|
|
491
|
+
const projectedEntry = acpTranscriptToolEntry(entry, {
|
|
492
|
+
maxInlineDetailChars: remainingInlineDetailChars,
|
|
493
|
+
mediaPathPrefix: options.mediaPathPrefix,
|
|
494
|
+
});
|
|
495
|
+
remainingInlineDetailChars = Math.max(
|
|
496
|
+
0,
|
|
497
|
+
remainingInlineDetailChars - String(projectedEntry.transcriptDetail || '').length
|
|
498
|
+
);
|
|
499
|
+
projected[index] = projectedEntry;
|
|
500
|
+
}
|
|
501
|
+
return projected;
|
|
502
|
+
}
|
|
503
|
+
|
|
342
504
|
module.exports = {
|
|
505
|
+
acpTranscriptEntries,
|
|
506
|
+
acpTranscriptMedia,
|
|
343
507
|
acpToolChanges: entry => patchChanges(entry?.content, { includeDiff: true }),
|
|
344
508
|
acpToolDetail: detailForTool,
|
|
345
509
|
acpToolReviewChanges: entry => patchReviewChanges(entry?.content),
|
|
346
510
|
acpTranscriptToolEntry,
|
|
511
|
+
decodeAcpTranscriptMedia,
|
|
347
512
|
};
|
package/backend/agent-manager.js
CHANGED
|
@@ -50,7 +50,13 @@ const {
|
|
|
50
50
|
stopPersistedAcpProcessGroup,
|
|
51
51
|
} = require('./acp-runtime');
|
|
52
52
|
const { chatRuntimeForProvider, isChatMode } = require('./chat-runtime');
|
|
53
|
-
const {
|
|
53
|
+
const {
|
|
54
|
+
acpTranscriptEntries,
|
|
55
|
+
acpTranscriptMedia,
|
|
56
|
+
acpToolChanges,
|
|
57
|
+
acpToolDetail,
|
|
58
|
+
acpToolReviewChanges,
|
|
59
|
+
} = require('./acp-transcript');
|
|
54
60
|
const {
|
|
55
61
|
applyCodexTerminalProfile,
|
|
56
62
|
codexTerminalProfileFromOutput,
|
|
@@ -5085,10 +5091,21 @@ class AgentManager extends EventEmitter {
|
|
|
5085
5091
|
const transcript = this.acpRuntime.getTranscriptSession(agentId, options);
|
|
5086
5092
|
return {
|
|
5087
5093
|
...transcript,
|
|
5088
|
-
entries: transcript.entries
|
|
5094
|
+
entries: acpTranscriptEntries(transcript.entries, {
|
|
5095
|
+
mediaPathPrefix: options.mediaPathPrefix,
|
|
5096
|
+
}),
|
|
5089
5097
|
};
|
|
5090
5098
|
}
|
|
5091
5099
|
|
|
5100
|
+
getAcpTranscriptMedia(agentId, entryId, mediaId) {
|
|
5101
|
+
this.requireLiveAcpAgent(agentId);
|
|
5102
|
+
const entry = this.acpRuntime.getTranscriptEntry(agentId, entryId);
|
|
5103
|
+
if (!entry) throw new Error('ACP transcript entry not found');
|
|
5104
|
+
const media = acpTranscriptMedia(entry, mediaId);
|
|
5105
|
+
if (!media) throw new Error('ACP transcript media not found');
|
|
5106
|
+
return media;
|
|
5107
|
+
}
|
|
5108
|
+
|
|
5092
5109
|
getAcpToolDetail(agentId, toolCallId) {
|
|
5093
5110
|
this.requireLiveAcpAgent(agentId);
|
|
5094
5111
|
const entry = this.acpRuntime.getToolEntry(agentId, toolCallId);
|
|
@@ -5661,6 +5678,14 @@ class AgentManager extends EventEmitter {
|
|
|
5661
5678
|
if (admission) {
|
|
5662
5679
|
transitionLifecycleOperation(staged, admission.operation.id, 'succeeded');
|
|
5663
5680
|
}
|
|
5681
|
+
if (!admission && !structuralUpdateChanged && !readUpdateChanged) {
|
|
5682
|
+
return {
|
|
5683
|
+
agentId,
|
|
5684
|
+
...updates,
|
|
5685
|
+
changed: false,
|
|
5686
|
+
requiresState: false,
|
|
5687
|
+
};
|
|
5688
|
+
}
|
|
5664
5689
|
try {
|
|
5665
5690
|
this.ensurePersistentAgentSession(staged);
|
|
5666
5691
|
} catch (error) {
|