@borgee/agents-host 0.2.35 → 0.2.56

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.
Files changed (74) hide show
  1. package/README.md +23 -27
  2. package/dist/agents-host.d.ts +28 -5
  3. package/dist/agents-host.js +274 -236
  4. package/dist/chat/chat-control-plane.d.ts +3 -0
  5. package/dist/chat/sdk-chat-control-plane.d.ts +4 -3
  6. package/dist/chat/sdk-chat-control-plane.js +6 -0
  7. package/dist/cli.js +1 -5
  8. package/dist/compatibility-gates.d.ts +4 -0
  9. package/dist/compatibility-gates.js +18 -1
  10. package/dist/context/claude-file-brief.d.ts +2 -0
  11. package/dist/context/claude-file-brief.js +83 -0
  12. package/dist/context/compaction.d.ts +20 -0
  13. package/dist/context/compaction.js +59 -0
  14. package/dist/context/injection.d.ts +44 -9
  15. package/dist/context/injection.js +360 -53
  16. package/dist/context/main-session-delegation.d.ts +1 -1
  17. package/dist/context/projection-strategy.d.ts +24 -0
  18. package/dist/context/projection-strategy.js +90 -0
  19. package/dist/context/prompt.d.ts +16 -1
  20. package/dist/context/prompt.js +475 -53
  21. package/dist/context/resolved-workspace.d.ts +2 -0
  22. package/dist/context/resolved-workspace.js +64 -0
  23. package/dist/context/skill-manual.d.ts +14 -0
  24. package/dist/context/skill-manual.js +21 -0
  25. package/dist/context/turn-preparation.d.ts +8 -2
  26. package/dist/context/turn-preparation.js +68 -17
  27. package/dist/gateway/localhost-gateway.js +18 -8
  28. package/dist/hosted-turn-content.d.ts +15 -0
  29. package/dist/hosted-turn-content.js +50 -0
  30. package/dist/managed-daemon.d.ts +3 -2
  31. package/dist/managed-daemon.js +198 -37
  32. package/dist/plugin-sdk.js +276 -359
  33. package/dist/plugin-sdk.js.map +4 -4
  34. package/dist/progress-to-activity.d.ts +16 -0
  35. package/dist/progress-to-activity.js +24 -0
  36. package/dist/projection-strategy-values.d.ts +4 -0
  37. package/dist/projection-strategy-values.js +28 -0
  38. package/dist/providers/acp-progress-collector.d.ts +44 -0
  39. package/dist/providers/acp-progress-collector.js +130 -0
  40. package/dist/providers/awaiting-user.d.ts +2 -3
  41. package/dist/providers/awaiting-user.js +5 -7
  42. package/dist/providers/claude/activity-metadata.d.ts +14 -0
  43. package/dist/providers/claude/activity-metadata.js +81 -0
  44. package/dist/providers/claude/adapter.d.ts +3 -1
  45. package/dist/providers/claude/adapter.js +10 -0
  46. package/dist/providers/claude/cli-client.d.ts +9 -1
  47. package/dist/providers/claude/cli-client.js +270 -126
  48. package/dist/providers/codex/adapter.d.ts +3 -1
  49. package/dist/providers/codex/adapter.js +10 -0
  50. package/dist/providers/codex/cli-client.d.ts +10 -2
  51. package/dist/providers/codex/cli-client.js +88 -104
  52. package/dist/providers/codex/project-doc.js +22 -37
  53. package/dist/providers/copilot/adapter.d.ts +3 -1
  54. package/dist/providers/copilot/adapter.js +10 -0
  55. package/dist/providers/copilot/cli-client.d.ts +9 -1
  56. package/dist/providers/copilot/cli-client.js +68 -85
  57. package/dist/providers/create-provider.d.ts +3 -1
  58. package/dist/providers/create-provider.js +36 -9
  59. package/dist/providers/provider-adapter.d.ts +35 -0
  60. package/dist/providers/provider-adapter.js +44 -1
  61. package/dist/state-paths.d.ts +10 -2
  62. package/dist/state-paths.js +25 -6
  63. package/dist/task-thread-resolution.d.ts +3 -2
  64. package/dist/types.d.ts +163 -8
  65. package/package.json +2 -2
  66. package/skills/borgee-agent/SKILL.md +127 -38
  67. package/skills/borgee-agent/references/errors.md +38 -0
  68. package/skills/borgee-agent/references/task-properties.md +33 -0
  69. package/skills/borgee-agent/scripts/borgee-agent.mjs +553 -0
  70. package/skills/borgee-agent/scripts/borgee-agent.py +547 -0
  71. package/dist/durable-cursor-store.d.ts +0 -5
  72. package/dist/durable-cursor-store.js +0 -7
  73. package/skills/borgee-agent/borgee-agent.mjs +0 -562
  74. package/skills/borgee-agent/borgee-agent.py +0 -469
@@ -1,11 +1,16 @@
1
1
  import { Readable, Writable } from 'node:stream';
2
+ import { createHash } from 'node:crypto';
2
3
  import { createRequire } from 'node:module';
3
4
  import { dirname, resolve } from 'node:path';
4
5
  import spawn from 'cross-spawn';
6
+ import { DEFAULT_PROJECTION_STRATEGY, normalizeProjectionStrategy, } from '../../projection-strategy-values.js';
5
7
  import { assertClaudeCommandCompatibility, DEFAULT_CLAUDE_COMMAND, isLegacyClaudeCompatibilityAlias, } from '../../config.js';
6
8
  import { PROTOCOL_VERSION, client, methods, ndJsonStream, } from '@agentclientprotocol/sdk';
7
9
  import { HostLogger, summarizeChildStderr, summarizeError } from '../../debug.js';
10
+ import { AcpProgressCollector } from '../acp-progress-collector.js';
11
+ import { readClaudeActivityMetadata } from './activity-metadata.js';
8
12
  import { resolveCopilotPermissionResponse } from '../../policy/copilot-permission.js';
13
+ import { buildClaudeSessionPromptAppend } from '../../context/prompt.js';
9
14
  import { IDLE_BACKEND_SHUTDOWN_DISABLED_MS, IdleBackendShutdownScheduler, } from '../idle-backend-shutdown.js';
10
15
  const SESSION_TAINTED_ERRORS = new WeakSet();
11
16
  const IDLE_BACKEND_STOPPED_MESSAGE = 'Claude ACP backend stopped after idle timeout';
@@ -23,6 +28,7 @@ const DEFAULT_SESSION_CAPABILITIES = {
23
28
  loadSession: false,
24
29
  resumeSession: false,
25
30
  };
31
+ const CLAUDE_SDK_COMPACTION_EXPERIMENT_CONTEXT_TOKEN_THRESHOLD_ENV = 'CLAUDE_SDK_COMPACTION_EXPERIMENT_CONTEXT_TOKEN_THRESHOLD';
26
32
  const require = createRequire(import.meta.url);
27
33
  const DEFAULT_RUNTIME = {
28
34
  spawn,
@@ -30,6 +36,7 @@ const DEFAULT_RUNTIME = {
30
36
  ndJsonStream,
31
37
  methods,
32
38
  protocolVersion: PROTOCOL_VERSION,
39
+ fetch,
33
40
  cwd: process.cwd(),
34
41
  idleBackendShutdownMs: IDLE_BACKEND_SHUTDOWN_DISABLED_MS,
35
42
  idleShutdownGracePeriodMs: DEFAULT_IDLE_SHUTDOWN_GRACE_PERIOD_MS,
@@ -42,6 +49,25 @@ function hasVisibleText(value) {
42
49
  function normalizeError(error) {
43
50
  return error instanceof Error ? error : new Error(String(error));
44
51
  }
52
+ function isAbsoluteHttpUrl(value) {
53
+ return value.startsWith('http://') || value.startsWith('https://');
54
+ }
55
+ function resolveHostedAttachmentUrl(rawUrl, borgeeBaseUrl) {
56
+ if (isAbsoluteHttpUrl(rawUrl)) {
57
+ return rawUrl;
58
+ }
59
+ const base = borgeeBaseUrl.replace(/\/$/, '');
60
+ const path = rawUrl.startsWith('/') ? rawUrl : `/${rawUrl}`;
61
+ return `${base}${path}`;
62
+ }
63
+ function isSameOrigin(left, right) {
64
+ try {
65
+ return new URL(left).origin === new URL(right).origin;
66
+ }
67
+ catch {
68
+ return false;
69
+ }
70
+ }
45
71
  function parsePersistedSessionRecord(rawValue) {
46
72
  const trimmed = rawValue.trim();
47
73
  if (trimmed.startsWith('{')) {
@@ -90,85 +116,98 @@ class ClaudeCliProcessError extends Error {
90
116
  this.stderrLineCount = stderrLineCount;
91
117
  }
92
118
  }
93
- function formatToolProgress(title, status) {
94
- const normalizedTitle = hasVisibleText(title) ? title.trim() : null;
95
- switch (status) {
96
- case 'completed':
97
- return normalizedTitle ? `Completed ${normalizedTitle}` : 'Completed tool call';
98
- case 'failed':
99
- return normalizedTitle ? `Failed ${normalizedTitle}` : 'Tool call failed';
100
- case 'pending':
101
- case 'in_progress':
102
- case undefined:
103
- case null:
104
- return normalizedTitle ? `Running ${normalizedTitle}…` : 'Running tool…';
105
- default:
106
- return normalizedTitle ? `${status} ${normalizedTitle}` : status;
119
+ function parseCompactionBanner(text) {
120
+ if (/(?:^|\n\n)Compacting\.\.\.\s*$/u.test(text)) {
121
+ return { stage: 'started' };
107
122
  }
123
+ if (/(?:^|\n\n)Compacting completed\.\s*$/u.test(text)) {
124
+ return { stage: 'completed' };
125
+ }
126
+ const match = /(?:^|\n\n)Compacting failed(?::\s*(.+?)|\.)\s*$/u.exec(text);
127
+ if (!match) {
128
+ return null;
129
+ }
130
+ return {
131
+ stage: 'failed',
132
+ ...(match?.[1] ? { failureReason: match[1].trim() } : {}),
133
+ };
108
134
  }
109
- function formatPlanProgress(entries) {
110
- const current = entries.find((entry) => entry.status === 'in_progress') ??
111
- entries.find((entry) => entry.status === 'pending') ??
112
- entries[0];
113
- return hasVisibleText(current?.content) ? `Plan: ${current.content.trim()}` : null;
114
- }
115
- class ClaudeProgressCollector {
135
+ class ClaudeCompactionObserver {
116
136
  onProgress;
117
- publicText = '';
118
- lastPublished = null;
119
- toolTitles = new Map();
137
+ answerText = '';
138
+ compactionState = 'idle';
139
+ lastCompactionBannerKey = null;
120
140
  constructor(onProgress) {
121
141
  this.onProgress = onProgress;
122
142
  }
123
- consume(update) {
124
- switch (update.update.sessionUpdate) {
125
- case 'agent_message_chunk':
126
- if (update.update.content.type !== 'text') {
127
- return;
128
- }
129
- this.publicText += update.update.content.text;
130
- this.publish(this.publicText);
131
- return;
132
- case 'tool_call':
133
- this.toolTitles.set(update.update.toolCallId, update.update.title);
134
- this.publishFallback(formatToolProgress(update.update.title, update.update.status));
135
- return;
136
- case 'tool_call_update': {
137
- const nextTitle = update.update.title ?? this.toolTitles.get(update.update.toolCallId);
138
- if (hasVisibleText(nextTitle)) {
139
- this.toolTitles.set(update.update.toolCallId, nextTitle);
140
- }
141
- this.publishFallback(formatToolProgress(nextTitle, update.update.status));
142
- return;
143
- }
144
- case 'plan':
145
- this.publishFallback(formatPlanProgress(update.update.entries.map((entry) => ({
146
- content: entry.content,
147
- status: entry.status,
148
- }))));
149
- return;
150
- default:
151
- return;
152
- }
153
- }
154
- getFinalText() {
155
- return this.publicText.trim();
143
+ consumeAnswerChunk(chunk) {
144
+ this.answerText += chunk;
145
+ this.publishCompactionFromText(this.answerText);
156
146
  }
157
- publishFallback(text) {
158
- if (hasVisibleText(this.publicText)) {
147
+ consumeUsageUpdate(usedTokens, contextWindowTokens) {
148
+ if (!this.onProgress || this.compactionState !== 'completed-awaiting-usage') {
159
149
  return;
160
150
  }
161
- this.publish(text);
151
+ this.compactionState = 'idle';
152
+ this.onProgress({
153
+ type: 'compaction',
154
+ compaction: {
155
+ provider: 'claude',
156
+ stage: 'completed',
157
+ usedTokens,
158
+ contextWindowTokens,
159
+ },
160
+ });
162
161
  }
163
- publish(text) {
164
- if (!this.onProgress || !hasVisibleText(text) || text === this.lastPublished) {
162
+ publishCompactionFromText(text) {
163
+ if (!this.onProgress) {
164
+ return;
165
+ }
166
+ const banner = parseCompactionBanner(text);
167
+ if (!banner) {
168
+ return;
169
+ }
170
+ const bannerKey = `${banner.stage}:${banner.stage === 'failed' ? banner.failureReason ?? '' : ''}:${text.trimEnd().length}`;
171
+ if (bannerKey === this.lastCompactionBannerKey) {
165
172
  return;
166
173
  }
167
- this.lastPublished = text;
168
- this.onProgress({ text });
174
+ this.lastCompactionBannerKey = bannerKey;
175
+ switch (banner.stage) {
176
+ case 'started':
177
+ this.compactionState = 'active';
178
+ this.onProgress({
179
+ type: 'compaction',
180
+ compaction: {
181
+ provider: 'claude',
182
+ stage: 'started',
183
+ },
184
+ });
185
+ return;
186
+ case 'completed':
187
+ this.compactionState = 'completed-awaiting-usage';
188
+ this.onProgress({
189
+ type: 'compaction',
190
+ compaction: {
191
+ provider: 'claude',
192
+ stage: 'completed',
193
+ },
194
+ });
195
+ return;
196
+ case 'failed':
197
+ this.compactionState = 'idle';
198
+ this.onProgress({
199
+ type: 'compaction',
200
+ compaction: {
201
+ provider: 'claude',
202
+ stage: 'failed',
203
+ ...(banner.failureReason ? { failureReason: banner.failureReason } : {}),
204
+ },
205
+ });
206
+ return;
207
+ }
169
208
  }
170
209
  }
171
- function createDeferredTurn(channelId, prompt, sessionPersistence, promptContext, options) {
210
+ function createDeferredTurn(channelId, preparedTurn, sessionPersistence, options) {
172
211
  let settled = false;
173
212
  let resolvePromise;
174
213
  let rejectPromise;
@@ -178,9 +217,8 @@ function createDeferredTurn(channelId, prompt, sessionPersistence, promptContext
178
217
  });
179
218
  return {
180
219
  channelId,
181
- prompt,
220
+ preparedTurn,
182
221
  sessionPersistence,
183
- promptContext,
184
222
  options,
185
223
  promise,
186
224
  resolve(value) {
@@ -204,6 +242,9 @@ function resolveSessionRouting(turn) {
204
242
  persistence: turn.providerSessionRouting?.persistence ?? 'persistent',
205
243
  };
206
244
  }
245
+ function asImagePart(part) {
246
+ return part.type === 'image' ? part : null;
247
+ }
207
248
  function asObject(value) {
208
249
  return typeof value === 'object' && value !== null ? value : null;
209
250
  }
@@ -267,68 +308,120 @@ function resolveClaudeLaunch(command, args, baseCwd) {
267
308
  args: args.map((arg) => (isRelativePathLike(arg) ? resolve(baseCwd, arg) : arg)),
268
309
  };
269
310
  }
270
- function buildClaudeSessionSystemPrompt(promptContext) {
271
- if (!promptContext) {
311
+ function resolveClaudeSdkCompactionControl() {
312
+ const rawThreshold = process.env[CLAUDE_SDK_COMPACTION_EXPERIMENT_CONTEXT_TOKEN_THRESHOLD_ENV];
313
+ if (!hasVisibleText(rawThreshold)) {
272
314
  return undefined;
273
315
  }
274
- const lines = [];
275
- if (promptContext.channelContextPayloadPath) {
276
- lines.push('Channel-scoped local context is available for this session.');
277
- lines.push(`Channel context payload: ${promptContext.channelContextPayloadPath}`);
278
- }
279
- if (promptContext.skillRuntime) {
280
- lines.push(`Skill guide: ${promptContext.skillRuntime.skillMarkdownPath}`);
281
- lines.push(`Node bootstrap CLI: node ${promptContext.skillRuntime.nodeCliPath} --context ${promptContext.channelContextPayloadPath} --print-bootstrap`);
282
- lines.push(`Python bootstrap CLI: python3 ${promptContext.skillRuntime.pythonCliPath} --context ${promptContext.channelContextPayloadPath} --print-bootstrap`);
283
- }
284
- if (promptContext.localhostGateway && promptContext.gatewayAuthPath) {
285
- lines.push(`Gateway auth sidecar for this session: ${promptContext.gatewayAuthPath}`);
286
- if (promptContext.localhostGateway.baseUrl) {
287
- lines.push(`Loopback gateway base URL: ${promptContext.localhostGateway.baseUrl}`);
288
- }
289
- }
290
- if (promptContext.taskWorkspace) {
291
- lines.push(`Task-scoped writable workspace root: ${promptContext.taskWorkspace.rootPath}.`);
292
- lines.push('This writable workspace is local to agents-host for the current task thread and does not imply that the target repository is already checked out there.');
293
- }
294
- if (promptContext.taskAssignmentContext?.currentTaskId) {
295
- lines.push(`Current assigned task id: ${promptContext.taskAssignmentContext.currentTaskId}.`);
316
+ const parsedThreshold = Number(rawThreshold.trim());
317
+ if (!Number.isSafeInteger(parsedThreshold) || parsedThreshold <= 0) {
318
+ throw new Error(`Invalid ${CLAUDE_SDK_COMPACTION_EXPERIMENT_CONTEXT_TOKEN_THRESHOLD_ENV}: expected a positive integer context token threshold for the experiment-scoped Claude SDK compaction opt-in`);
296
319
  }
297
- return lines.length > 0 ? lines.join('\n') : undefined;
320
+ return {
321
+ enabled: true,
322
+ context_token_threshold: parsedThreshold,
323
+ };
298
324
  }
299
- function buildClaudeSessionMeta(promptContext) {
300
- const systemPromptAppend = buildClaudeSessionSystemPrompt(promptContext);
301
- if (!systemPromptAppend) {
325
+ function buildClaudeSessionMeta(systemPromptAppend) {
326
+ const sdkCompactionControl = resolveClaudeSdkCompactionControl();
327
+ if (!systemPromptAppend && !sdkCompactionControl) {
302
328
  return undefined;
303
329
  }
304
330
  return {
305
- systemPrompt: {
306
- append: systemPromptAppend,
307
- },
331
+ ...(systemPromptAppend
332
+ ? {
333
+ systemPrompt: {
334
+ append: systemPromptAppend,
335
+ },
336
+ }
337
+ : {}),
338
+ ...(sdkCompactionControl
339
+ ? {
340
+ claudeCode: {
341
+ options: {
342
+ compaction_control: sdkCompactionControl,
343
+ },
344
+ },
345
+ }
346
+ : {}),
308
347
  };
309
348
  }
349
+ function buildFallbackClaudeSessionPrompt(agentName, promptContext, promptStrategy) {
350
+ if (!promptContext) {
351
+ return undefined;
352
+ }
353
+ return buildClaudeSessionPromptAppend({
354
+ agentName: agentName ?? 'Borgee agent',
355
+ promptContext,
356
+ promptStrategy,
357
+ });
358
+ }
359
+ function resolveProjectionStrategy(promptStrategy) {
360
+ return normalizeProjectionStrategy(promptStrategy) ?? DEFAULT_PROJECTION_STRATEGY;
361
+ }
362
+ function resolveTurnSessionPromptAppend(turn) {
363
+ const promptStrategy = resolveProjectionStrategy(turn.projectionStrategy);
364
+ if (promptStrategy === 'turn-full') {
365
+ return undefined;
366
+ }
367
+ return turn.claudeSessionPromptAppend
368
+ ?? buildFallbackClaudeSessionPrompt(turn.agentName, turn.promptContext, promptStrategy);
369
+ }
310
370
  function resolveSessionAdditionalDirectories(promptContext) {
311
371
  const directories = new Set();
312
372
  if (promptContext?.channelContextPayloadPath) {
313
373
  directories.add(dirname(promptContext.channelContextPayloadPath));
314
374
  }
315
- if (promptContext?.gatewayAuthPath) {
316
- directories.add(dirname(promptContext.gatewayAuthPath));
375
+ if (promptContext?.gatewayCredentialPath) {
376
+ directories.add(dirname(promptContext.gatewayCredentialPath));
317
377
  }
318
378
  if (promptContext?.skillRuntime?.skillDirectoryPath) {
319
379
  directories.add(promptContext.skillRuntime.skillDirectoryPath);
320
380
  }
321
381
  return directories.size > 0 ? [...directories].sort((left, right) => left.localeCompare(right)) : undefined;
322
382
  }
323
- function resolveSessionVisibilityKey(promptContext) {
383
+ function hashSessionAppend(systemPromptAppend) {
384
+ if (!systemPromptAppend) {
385
+ return undefined;
386
+ }
387
+ return createHash('sha256').update(systemPromptAppend).digest('hex');
388
+ }
389
+ function resolveSessionVisibilityKey(promptContext, systemPromptAppend, promptStrategy) {
390
+ const resolvedPromptStrategy = resolveProjectionStrategy(promptStrategy);
391
+ const explicitPromptStrategy = resolvedPromptStrategy === DEFAULT_PROJECTION_STRATEGY && promptStrategy == null
392
+ ? undefined
393
+ : resolvedPromptStrategy === DEFAULT_PROJECTION_STRATEGY
394
+ ? undefined
395
+ : resolvedPromptStrategy;
324
396
  const additionalDirectories = resolveSessionAdditionalDirectories(promptContext);
325
- if (!additionalDirectories) {
397
+ const sessionAppendHash = hashSessionAppend(systemPromptAppend);
398
+ const projectedBriefHash = promptContext?.claudeProjectedBriefHash;
399
+ if (!additionalDirectories
400
+ && !sessionAppendHash
401
+ && !projectedBriefHash
402
+ && !promptContext?.gatewayCredentialPath
403
+ && !explicitPromptStrategy) {
326
404
  return undefined;
327
405
  }
328
- return JSON.stringify({ additionalDirectories });
406
+ if (sessionAppendHash) {
407
+ return JSON.stringify({
408
+ ...(additionalDirectories ? { additionalDirectories } : {}),
409
+ sessionAppendHash,
410
+ ...(projectedBriefHash ? { projectedBriefHash } : {}),
411
+ ...(explicitPromptStrategy ? { promptStrategy: explicitPromptStrategy } : {}),
412
+ });
413
+ }
414
+ return JSON.stringify({
415
+ ...(additionalDirectories ? { additionalDirectories } : {}),
416
+ ...(promptContext?.gatewayCredentialPath
417
+ ? { gatewayCredentialPath: promptContext.gatewayCredentialPath }
418
+ : {}),
419
+ ...(projectedBriefHash ? { projectedBriefHash } : {}),
420
+ ...(explicitPromptStrategy ? { promptStrategy: explicitPromptStrategy } : {}),
421
+ });
329
422
  }
330
- function buildSessionRequest(cwd, promptContext, additionalDirectories) {
331
- const meta = buildClaudeSessionMeta(promptContext);
423
+ function buildSessionRequest(cwd, systemPromptAppend, additionalDirectories) {
424
+ const meta = buildClaudeSessionMeta(systemPromptAppend);
332
425
  return {
333
426
  cwd,
334
427
  mcpServers: [],
@@ -369,6 +462,7 @@ export class ClaudeCliClient {
369
462
  sessionStore;
370
463
  resolveSessionStoreAgentId;
371
464
  logger;
465
+ imageInputConfig;
372
466
  runtime;
373
467
  channels = new Map();
374
468
  /**
@@ -407,12 +501,13 @@ export class ClaudeCliClient {
407
501
  sessionCapabilities = DEFAULT_SESSION_CAPABILITIES;
408
502
  idleBackendShutdown;
409
503
  childStderr = '';
410
- constructor(command, args = [], runtimeOverrides = {}, sessionStore, resolveSessionStoreAgentId = () => undefined, logger = new HostLogger()) {
504
+ constructor(command, args = [], runtimeOverrides = {}, sessionStore, resolveSessionStoreAgentId = () => undefined, logger = new HostLogger(), imageInputConfig = {}) {
411
505
  this.command = command;
412
506
  this.args = args;
413
507
  this.sessionStore = sessionStore;
414
508
  this.resolveSessionStoreAgentId = resolveSessionStoreAgentId;
415
509
  this.logger = logger;
510
+ this.imageInputConfig = imageInputConfig;
416
511
  this.runtime = { ...DEFAULT_RUNTIME, ...runtimeOverrides };
417
512
  assertClaudeCommandCompatibility(command, args, 'Claude provider runtime');
418
513
  this.idleBackendShutdown = new IdleBackendShutdownScheduler({
@@ -434,19 +529,26 @@ export class ClaudeCliClient {
434
529
  if (this.fatalError) {
435
530
  throw this.fatalError;
436
531
  }
437
- const preparedTurn = typeof channelIdOrTurn === 'string'
532
+ const preparedTurnBase = typeof channelIdOrTurn === 'string'
438
533
  ? {
439
534
  channelId: channelIdOrTurn,
535
+ incomingContent: '',
536
+ incomingParts: [],
440
537
  prompt: typeof promptOrOptions === 'string' ? promptOrOptions : '',
441
538
  }
442
539
  : channelIdOrTurn;
540
+ const preparedTurn = {
541
+ ...preparedTurnBase,
542
+ incomingContent: preparedTurnBase.incomingContent ?? '',
543
+ incomingParts: preparedTurnBase.incomingParts ?? [],
544
+ };
443
545
  const options = typeof channelIdOrTurn === 'string'
444
546
  ? maybeOptions
445
547
  : promptOrOptions;
446
548
  const sessionRouting = resolveSessionRouting(preparedTurn);
447
549
  const state = this.getOrCreateChannelState(sessionRouting.key, preparedTurn.channelId, sessionRouting.persistence);
448
550
  this.idleBackendShutdown.cancel();
449
- const turn = createDeferredTurn(preparedTurn.channelId, preparedTurn.prompt, sessionRouting.persistence, preparedTurn.promptContext, options);
551
+ const turn = createDeferredTurn(preparedTurn.channelId, preparedTurn, sessionRouting.persistence, options);
450
552
  state.queue.push(turn);
451
553
  this.processChannelQueue(sessionRouting.key, state);
452
554
  return turn.promise;
@@ -618,14 +720,16 @@ export class ClaudeCliClient {
618
720
  }
619
721
  state.activeTurn = turn;
620
722
  try {
621
- state.cwd = this.resolveSessionCwd(turn.promptContext);
622
- state.visibilityKey = resolveSessionVisibilityKey(turn.promptContext);
723
+ state.cwd = this.resolveSessionCwd(turn.preparedTurn.promptContext);
724
+ const sessionPromptAppend = resolveTurnSessionPromptAppend(turn.preparedTurn);
725
+ state.visibilityKey = resolveSessionVisibilityKey(turn.preparedTurn.promptContext, sessionPromptAppend, turn.preparedTurn.projectionStrategy);
623
726
  await this.ensureStarted();
624
727
  await this.recycleSessionIfScopeChanged(channelId, state);
625
- const session = await this.getOrCreateSession(channelId, state, turn.promptContext);
728
+ const session = await this.getOrCreateSession(channelId, state, turn.preparedTurn, sessionPromptAppend);
626
729
  let reply;
627
730
  try {
628
- reply = await this.runTurn(session, turn.prompt, turn.options);
731
+ const promptInput = await this.buildPromptInput(turn.preparedTurn);
732
+ reply = await this.runTurn(session, promptInput, turn.options);
629
733
  }
630
734
  catch (error) {
631
735
  if (isSessionTainted(error)) {
@@ -665,7 +769,7 @@ export class ClaudeCliClient {
665
769
  }
666
770
  })();
667
771
  }
668
- async getOrCreateSession(channelId, state, promptContext) {
772
+ async getOrCreateSession(channelId, state, turn, systemPromptAppend) {
669
773
  if (state.session) {
670
774
  return state.session;
671
775
  }
@@ -679,10 +783,10 @@ export class ClaudeCliClient {
679
783
  const persistedSessionId = state.sessionPersistence === 'persistent'
680
784
  ? await this.readPersistedSessionId(channelId, cwd, state.visibilityKey)
681
785
  : undefined;
682
- const additionalDirectories = resolveSessionAdditionalDirectories(promptContext);
786
+ const additionalDirectories = resolveSessionAdditionalDirectories(turn.promptContext);
683
787
  const sessionPromise = persistedSessionId
684
- ? this.restoreOrCreateSession(channelId, persistedSessionId, cwd, promptContext, additionalDirectories)
685
- : this.startFreshSession(channelId, cwd, promptContext, additionalDirectories);
788
+ ? this.restoreOrCreateSession(channelId, persistedSessionId, cwd, systemPromptAppend, additionalDirectories)
789
+ : this.startFreshSession(channelId, cwd, systemPromptAppend, additionalDirectories);
686
790
  this.pendingSessionStarts.add(sessionPromise);
687
791
  state.sessionPromise = sessionPromise;
688
792
  let sessionAdopted = false;
@@ -721,11 +825,11 @@ export class ClaudeCliClient {
721
825
  }
722
826
  }
723
827
  }
724
- async startFreshSession(channelId, cwd, promptContext, additionalDirectories) {
828
+ async startFreshSession(channelId, cwd, systemPromptAppend, additionalDirectories) {
725
829
  if (!this.connection) {
726
830
  throw new Error('Claude ACP connection is not available');
727
831
  }
728
- const session = await this.connection.agent.buildSession(buildSessionRequest(cwd, promptContext, additionalDirectories)).start();
832
+ const session = await this.connection.agent.buildSession(buildSessionRequest(cwd, systemPromptAppend, additionalDirectories)).start();
729
833
  this.logger?.debug('started fresh Claude ACP session', { channelId, cwd });
730
834
  const state = this.channels.get(channelId);
731
835
  if (state?.sessionPersistence === 'persistent') {
@@ -733,13 +837,13 @@ export class ClaudeCliClient {
733
837
  }
734
838
  return session;
735
839
  }
736
- async restoreOrCreateSession(channelId, sessionId, cwd, promptContext, additionalDirectories) {
840
+ async restoreOrCreateSession(channelId, sessionId, cwd, systemPromptAppend, additionalDirectories) {
737
841
  if (!this.sessionCapabilities.resumeSession) {
738
842
  this.logger?.debug('Claude ACP restore unsupported; starting fresh session', { channelId });
739
- return this.startFreshSession(channelId, cwd, promptContext, additionalDirectories);
843
+ return this.startFreshSession(channelId, cwd, systemPromptAppend, additionalDirectories);
740
844
  }
741
845
  try {
742
- const session = await this.restoreSession(sessionId, cwd, promptContext, additionalDirectories);
846
+ const session = await this.restoreSession(sessionId, cwd, systemPromptAppend, additionalDirectories);
743
847
  this.logger?.debug('restored Claude ACP session', {
744
848
  channelId,
745
849
  restoreMethod: this.sessionCapabilities.resumeSession ? 'session/resume' : 'session/load',
@@ -752,10 +856,10 @@ export class ClaudeCliClient {
752
856
  }
753
857
  this.logger?.debug('discarded stale Claude ACP session and started fresh', { channelId });
754
858
  await this.clearPersistedSessionBestEffort(channelId);
755
- return this.startFreshSession(channelId, cwd, promptContext, additionalDirectories);
859
+ return this.startFreshSession(channelId, cwd, systemPromptAppend, additionalDirectories);
756
860
  }
757
861
  }
758
- async restoreSession(sessionId, cwd, promptContext, additionalDirectories) {
862
+ async restoreSession(sessionId, cwd, systemPromptAppend, additionalDirectories) {
759
863
  if (!this.connection) {
760
864
  throw new Error('Claude ACP connection is not available');
761
865
  }
@@ -767,7 +871,7 @@ export class ClaudeCliClient {
767
871
  try {
768
872
  const request = {
769
873
  sessionId,
770
- ...buildSessionRequest(cwd, promptContext, additionalDirectories),
874
+ ...buildSessionRequest(cwd, systemPromptAppend, additionalDirectories),
771
875
  };
772
876
  if (this.sessionCapabilities.resumeSession) {
773
877
  await this.connection.agent.request(this.runtime.methods.agent.session.resume, request);
@@ -792,7 +896,7 @@ export class ClaudeCliClient {
792
896
  }
793
897
  }
794
898
  resolveSessionCwd(promptContext) {
795
- return promptContext?.taskWorkspace?.rootPath ?? this.runtime.cwd;
899
+ return promptContext?.resolvedWorkspace?.rootPath ?? this.runtime.cwd;
796
900
  }
797
901
  async recycleSessionIfScopeChanged(channelId, state) {
798
902
  const session = state.session;
@@ -824,7 +928,8 @@ export class ClaudeCliClient {
824
928
  const promptFailure = new Promise((_, reject) => {
825
929
  void promptPromise.catch((error) => reject(markSessionTainted(error)));
826
930
  });
827
- const collector = new ClaudeProgressCollector(options?.onProgress);
931
+ const collector = new AcpProgressCollector(options?.onProgress, readClaudeActivityMetadata);
932
+ const compactionObserver = new ClaudeCompactionObserver(options?.onProgress);
828
933
  for (;;) {
829
934
  let update;
830
935
  try {
@@ -850,6 +955,12 @@ export class ClaudeCliClient {
850
955
  }
851
956
  return output;
852
957
  }
958
+ if (update.update.sessionUpdate === 'agent_message_chunk' && update.update.content.type === 'text') {
959
+ compactionObserver.consumeAnswerChunk(update.update.content.text);
960
+ }
961
+ if (update.update.sessionUpdate === 'usage_update') {
962
+ compactionObserver.consumeUsageUpdate(update.update.used, update.update.size);
963
+ }
853
964
  collector.consume(update);
854
965
  }
855
966
  }
@@ -1199,4 +1310,37 @@ export class ClaudeCliClient {
1199
1310
  }
1200
1311
  }
1201
1312
  }
1313
+ async buildPromptInput(turn) {
1314
+ const imageParts = turn.incomingParts.map(asImagePart).filter((part) => part !== null);
1315
+ if (imageParts.length === 0) {
1316
+ return turn.prompt;
1317
+ }
1318
+ const imageBlocks = await Promise.all(imageParts.map(async (part) => this.fetchImageBlock(part.attachment.url, part.attachment.contentType)));
1319
+ return [
1320
+ { type: 'text', text: turn.prompt },
1321
+ ...imageBlocks,
1322
+ ];
1323
+ }
1324
+ async fetchImageBlock(rawUrl, contentType) {
1325
+ const baseUrl = this.imageInputConfig.borgeeBaseUrl?.trim();
1326
+ const agentApiKey = this.imageInputConfig.agentApiKey?.trim();
1327
+ if (!baseUrl || !agentApiKey) {
1328
+ throw new Error('Claude image input requires Borgee base URL and agent API key');
1329
+ }
1330
+ const resolvedUrl = resolveHostedAttachmentUrl(rawUrl, baseUrl);
1331
+ if (!isSameOrigin(resolvedUrl, baseUrl)) {
1332
+ throw new Error(`Claude image input only supports Borgee-origin attachments: ${resolvedUrl}`);
1333
+ }
1334
+ const init = { headers: { Authorization: `Bearer ${agentApiKey}` } };
1335
+ const response = await this.runtime.fetch(resolvedUrl, init);
1336
+ if (!response.ok) {
1337
+ throw new Error(`Claude image fetch failed with ${response.status} ${response.statusText}`.trim());
1338
+ }
1339
+ const blob = Buffer.from(await response.arrayBuffer()).toString('base64');
1340
+ return {
1341
+ type: 'image',
1342
+ mimeType: contentType,
1343
+ data: blob,
1344
+ };
1345
+ }
1202
1346
  }
@@ -1,10 +1,12 @@
1
- import type { ProviderAdapter } from '../provider-adapter.js';
1
+ import { type ProviderAdapter } from '../provider-adapter.js';
2
2
  import type { ProviderGenerateOptions, ProviderInput, ProviderReply } from '../../types.js';
3
3
  import { ProviderTurnPreparer } from '../../context/turn-preparation.js';
4
4
  import { CodexCliClient } from './cli-client.js';
5
+ export declare const CODEX_HOSTED_PROVIDER_CAPABILITIES: import("../provider-adapter.js").ProviderCapabilities;
5
6
  export declare class CodexProviderAdapter implements ProviderAdapter {
6
7
  private readonly cli;
7
8
  private readonly turnPreparer;
9
+ readonly capabilities: import("../provider-adapter.js").ProviderCapabilities;
8
10
  constructor(cli: CodexCliClient, turnPreparer: ProviderTurnPreparer);
9
11
  generateReply(input: ProviderInput, options?: ProviderGenerateOptions): Promise<ProviderReply>;
10
12
  dispose(): Promise<void>;
@@ -1,7 +1,17 @@
1
+ import { createHostedProviderCapabilities } from '../provider-adapter.js';
1
2
  import { createAwaitingUserProgressHandler, parseProviderReply } from '../awaiting-user.js';
3
+ export const CODEX_HOSTED_PROVIDER_CAPABILITIES = createHostedProviderCapabilities({
4
+ imageInputTransport: {
5
+ source: 'transport-derived',
6
+ support: 'supported',
7
+ reason: 'real-media-delivered',
8
+ delivery: ['blob'],
9
+ },
10
+ });
2
11
  export class CodexProviderAdapter {
3
12
  cli;
4
13
  turnPreparer;
14
+ capabilities = CODEX_HOSTED_PROVIDER_CAPABILITIES;
5
15
  constructor(cli, turnPreparer) {
6
16
  this.cli = cli;
7
17
  this.turnPreparer = turnPreparer;