@borgee/agents-host 0.2.44 → 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 (56) hide show
  1. package/README.md +23 -27
  2. package/dist/agents-host.d.ts +26 -5
  3. package/dist/agents-host.js +163 -192
  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 +3 -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 +39 -6
  15. package/dist/context/injection.js +317 -26
  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 +456 -22
  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 +1 -0
  24. package/dist/context/skill-manual.js +4 -1
  25. package/dist/context/turn-preparation.d.ts +8 -2
  26. package/dist/context/turn-preparation.js +56 -14
  27. package/dist/gateway/localhost-gateway.js +2 -0
  28. package/dist/managed-daemon.js +122 -9
  29. package/dist/plugin-sdk.js +276 -359
  30. package/dist/plugin-sdk.js.map +4 -4
  31. package/dist/progress-to-activity.d.ts +16 -0
  32. package/dist/progress-to-activity.js +24 -0
  33. package/dist/projection-strategy-values.d.ts +4 -0
  34. package/dist/projection-strategy-values.js +28 -0
  35. package/dist/providers/acp-progress-collector.d.ts +44 -0
  36. package/dist/providers/acp-progress-collector.js +130 -0
  37. package/dist/providers/awaiting-user.d.ts +2 -3
  38. package/dist/providers/awaiting-user.js +5 -7
  39. package/dist/providers/claude/activity-metadata.d.ts +14 -0
  40. package/dist/providers/claude/activity-metadata.js +81 -0
  41. package/dist/providers/claude/cli-client.d.ts +1 -2
  42. package/dist/providers/claude/cli-client.js +190 -117
  43. package/dist/providers/codex/cli-client.js +3 -83
  44. package/dist/providers/codex/project-doc.js +12 -11
  45. package/dist/providers/copilot/cli-client.js +3 -83
  46. package/dist/providers/create-provider.d.ts +2 -0
  47. package/dist/providers/create-provider.js +20 -4
  48. package/dist/state-paths.d.ts +1 -1
  49. package/dist/state-paths.js +3 -3
  50. package/dist/task-thread-resolution.d.ts +3 -2
  51. package/dist/types.d.ts +130 -6
  52. package/package.json +2 -2
  53. package/skills/borgee-agent/SKILL.md +9 -1
  54. package/skills/borgee-agent/references/task-properties.md +5 -2
  55. package/dist/durable-cursor-store.d.ts +0 -5
  56. package/dist/durable-cursor-store.js +0 -7
@@ -1,12 +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';
9
- import { buildSkillManualLines } from '../../context/skill-manual.js';
13
+ import { buildClaudeSessionPromptAppend } from '../../context/prompt.js';
10
14
  import { IDLE_BACKEND_SHUTDOWN_DISABLED_MS, IdleBackendShutdownScheduler, } from '../idle-backend-shutdown.js';
11
15
  const SESSION_TAINTED_ERRORS = new WeakSet();
12
16
  const IDLE_BACKEND_STOPPED_MESSAGE = 'Claude ACP backend stopped after idle timeout';
@@ -24,6 +28,7 @@ const DEFAULT_SESSION_CAPABILITIES = {
24
28
  loadSession: false,
25
29
  resumeSession: false,
26
30
  };
31
+ const CLAUDE_SDK_COMPACTION_EXPERIMENT_CONTEXT_TOKEN_THRESHOLD_ENV = 'CLAUDE_SDK_COMPACTION_EXPERIMENT_CONTEXT_TOKEN_THRESHOLD';
27
32
  const require = createRequire(import.meta.url);
28
33
  const DEFAULT_RUNTIME = {
29
34
  spawn,
@@ -111,82 +116,95 @@ class ClaudeCliProcessError extends Error {
111
116
  this.stderrLineCount = stderrLineCount;
112
117
  }
113
118
  }
114
- function formatToolProgress(title, status) {
115
- const normalizedTitle = hasVisibleText(title) ? title.trim() : null;
116
- switch (status) {
117
- case 'completed':
118
- return normalizedTitle ? `Completed ${normalizedTitle}` : 'Completed tool call';
119
- case 'failed':
120
- return normalizedTitle ? `Failed ${normalizedTitle}` : 'Tool call failed';
121
- case 'pending':
122
- case 'in_progress':
123
- case undefined:
124
- case null:
125
- return normalizedTitle ? `Running ${normalizedTitle}…` : 'Running tool…';
126
- default:
127
- return normalizedTitle ? `${status} ${normalizedTitle}` : status;
119
+ function parseCompactionBanner(text) {
120
+ if (/(?:^|\n\n)Compacting\.\.\.\s*$/u.test(text)) {
121
+ return { stage: 'started' };
128
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
+ };
129
134
  }
130
- function formatPlanProgress(entries) {
131
- const current = entries.find((entry) => entry.status === 'in_progress') ??
132
- entries.find((entry) => entry.status === 'pending') ??
133
- entries[0];
134
- return hasVisibleText(current?.content) ? `Plan: ${current.content.trim()}` : null;
135
- }
136
- class ClaudeProgressCollector {
135
+ class ClaudeCompactionObserver {
137
136
  onProgress;
138
- publicText = '';
139
- lastPublished = null;
140
- toolTitles = new Map();
137
+ answerText = '';
138
+ compactionState = 'idle';
139
+ lastCompactionBannerKey = null;
141
140
  constructor(onProgress) {
142
141
  this.onProgress = onProgress;
143
142
  }
144
- consume(update) {
145
- switch (update.update.sessionUpdate) {
146
- case 'agent_message_chunk':
147
- if (update.update.content.type !== 'text') {
148
- return;
149
- }
150
- this.publicText += update.update.content.text;
151
- this.publish(this.publicText);
152
- return;
153
- case 'tool_call':
154
- this.toolTitles.set(update.update.toolCallId, update.update.title);
155
- this.publishFallback(formatToolProgress(update.update.title, update.update.status));
156
- return;
157
- case 'tool_call_update': {
158
- const nextTitle = update.update.title ?? this.toolTitles.get(update.update.toolCallId);
159
- if (hasVisibleText(nextTitle)) {
160
- this.toolTitles.set(update.update.toolCallId, nextTitle);
161
- }
162
- this.publishFallback(formatToolProgress(nextTitle, update.update.status));
163
- return;
164
- }
165
- case 'plan':
166
- this.publishFallback(formatPlanProgress(update.update.entries.map((entry) => ({
167
- content: entry.content,
168
- status: entry.status,
169
- }))));
170
- return;
171
- default:
172
- return;
173
- }
174
- }
175
- getFinalText() {
176
- return this.publicText.trim();
143
+ consumeAnswerChunk(chunk) {
144
+ this.answerText += chunk;
145
+ this.publishCompactionFromText(this.answerText);
177
146
  }
178
- publishFallback(text) {
179
- if (hasVisibleText(this.publicText)) {
147
+ consumeUsageUpdate(usedTokens, contextWindowTokens) {
148
+ if (!this.onProgress || this.compactionState !== 'completed-awaiting-usage') {
180
149
  return;
181
150
  }
182
- 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
+ });
183
161
  }
184
- publish(text) {
185
- 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) {
186
172
  return;
187
173
  }
188
- this.lastPublished = text;
189
- 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
+ }
190
208
  }
191
209
  }
192
210
  function createDeferredTurn(channelId, preparedTurn, sessionPersistence, options) {
@@ -290,39 +308,65 @@ function resolveClaudeLaunch(command, args, baseCwd) {
290
308
  args: args.map((arg) => (isRelativePathLike(arg) ? resolve(baseCwd, arg) : arg)),
291
309
  };
292
310
  }
293
- export function buildClaudeSessionSystemPrompt(promptContext) {
294
- if (!promptContext) {
311
+ function resolveClaudeSdkCompactionControl() {
312
+ const rawThreshold = process.env[CLAUDE_SDK_COMPACTION_EXPERIMENT_CONTEXT_TOKEN_THRESHOLD_ENV];
313
+ if (!hasVisibleText(rawThreshold)) {
295
314
  return undefined;
296
315
  }
297
- const lines = [];
298
- // Every command the CLI carries needs the gateway credential file, so the session names it only
299
- // when one exists; otherwise the model is handed an invocation template it cannot fill.
300
- if (promptContext.skillRuntime && promptContext.gatewayCredentialPath) {
301
- lines.push(...buildSkillManualLines(promptContext.skillRuntime));
302
- }
303
- if (promptContext.localhostGateway && promptContext.gatewayCredentialPath) {
304
- lines.push(`Gateway credential file for this session: ${promptContext.gatewayCredentialPath}`);
305
- }
306
- if (promptContext.taskWorkspace) {
307
- lines.push(`Task-scoped writable workspace root: ${promptContext.taskWorkspace.rootPath}.`);
308
- 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.');
309
- }
310
- if (promptContext.taskAssignmentContext?.currentTaskId) {
311
- 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`);
312
319
  }
313
- return lines.length > 0 ? lines.join('\n') : undefined;
320
+ return {
321
+ enabled: true,
322
+ context_token_threshold: parsedThreshold,
323
+ };
314
324
  }
315
- function buildClaudeSessionMeta(promptContext) {
316
- const systemPromptAppend = buildClaudeSessionSystemPrompt(promptContext);
317
- if (!systemPromptAppend) {
325
+ function buildClaudeSessionMeta(systemPromptAppend) {
326
+ const sdkCompactionControl = resolveClaudeSdkCompactionControl();
327
+ if (!systemPromptAppend && !sdkCompactionControl) {
318
328
  return undefined;
319
329
  }
320
330
  return {
321
- systemPrompt: {
322
- append: systemPromptAppend,
323
- },
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
+ : {}),
324
347
  };
325
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
+ }
326
370
  function resolveSessionAdditionalDirectories(promptContext) {
327
371
  const directories = new Set();
328
372
  if (promptContext?.channelContextPayloadPath) {
@@ -336,27 +380,48 @@ function resolveSessionAdditionalDirectories(promptContext) {
336
380
  }
337
381
  return directories.size > 0 ? [...directories].sort((left, right) => left.localeCompare(right)) : undefined;
338
382
  }
339
- /**
340
- * A session carries its scope from `session/new`: the directories it may read, plus the append that
341
- * names the CLI manual and this channel's gateway credential file. The credential file sits in the
342
- * channel context directory the payload already contributes, so its absence is invisible in the
343
- * directory set alone — without the path itself in the key, a session opened while the credential
344
- * write failed keeps an append that never names the CLI for as long as the session lives.
345
- */
346
- 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;
347
396
  const additionalDirectories = resolveSessionAdditionalDirectories(promptContext);
348
- if (!additionalDirectories) {
397
+ const sessionAppendHash = hashSessionAppend(systemPromptAppend);
398
+ const projectedBriefHash = promptContext?.claudeProjectedBriefHash;
399
+ if (!additionalDirectories
400
+ && !sessionAppendHash
401
+ && !projectedBriefHash
402
+ && !promptContext?.gatewayCredentialPath
403
+ && !explicitPromptStrategy) {
349
404
  return undefined;
350
405
  }
406
+ if (sessionAppendHash) {
407
+ return JSON.stringify({
408
+ ...(additionalDirectories ? { additionalDirectories } : {}),
409
+ sessionAppendHash,
410
+ ...(projectedBriefHash ? { projectedBriefHash } : {}),
411
+ ...(explicitPromptStrategy ? { promptStrategy: explicitPromptStrategy } : {}),
412
+ });
413
+ }
351
414
  return JSON.stringify({
352
- additionalDirectories,
415
+ ...(additionalDirectories ? { additionalDirectories } : {}),
353
416
  ...(promptContext?.gatewayCredentialPath
354
417
  ? { gatewayCredentialPath: promptContext.gatewayCredentialPath }
355
418
  : {}),
419
+ ...(projectedBriefHash ? { projectedBriefHash } : {}),
420
+ ...(explicitPromptStrategy ? { promptStrategy: explicitPromptStrategy } : {}),
356
421
  });
357
422
  }
358
- function buildSessionRequest(cwd, promptContext, additionalDirectories) {
359
- const meta = buildClaudeSessionMeta(promptContext);
423
+ function buildSessionRequest(cwd, systemPromptAppend, additionalDirectories) {
424
+ const meta = buildClaudeSessionMeta(systemPromptAppend);
360
425
  return {
361
426
  cwd,
362
427
  mcpServers: [],
@@ -656,10 +721,11 @@ export class ClaudeCliClient {
656
721
  state.activeTurn = turn;
657
722
  try {
658
723
  state.cwd = this.resolveSessionCwd(turn.preparedTurn.promptContext);
659
- state.visibilityKey = resolveSessionVisibilityKey(turn.preparedTurn.promptContext);
724
+ const sessionPromptAppend = resolveTurnSessionPromptAppend(turn.preparedTurn);
725
+ state.visibilityKey = resolveSessionVisibilityKey(turn.preparedTurn.promptContext, sessionPromptAppend, turn.preparedTurn.projectionStrategy);
660
726
  await this.ensureStarted();
661
727
  await this.recycleSessionIfScopeChanged(channelId, state);
662
- const session = await this.getOrCreateSession(channelId, state, turn.preparedTurn.promptContext);
728
+ const session = await this.getOrCreateSession(channelId, state, turn.preparedTurn, sessionPromptAppend);
663
729
  let reply;
664
730
  try {
665
731
  const promptInput = await this.buildPromptInput(turn.preparedTurn);
@@ -703,7 +769,7 @@ export class ClaudeCliClient {
703
769
  }
704
770
  })();
705
771
  }
706
- async getOrCreateSession(channelId, state, promptContext) {
772
+ async getOrCreateSession(channelId, state, turn, systemPromptAppend) {
707
773
  if (state.session) {
708
774
  return state.session;
709
775
  }
@@ -717,10 +783,10 @@ export class ClaudeCliClient {
717
783
  const persistedSessionId = state.sessionPersistence === 'persistent'
718
784
  ? await this.readPersistedSessionId(channelId, cwd, state.visibilityKey)
719
785
  : undefined;
720
- const additionalDirectories = resolveSessionAdditionalDirectories(promptContext);
786
+ const additionalDirectories = resolveSessionAdditionalDirectories(turn.promptContext);
721
787
  const sessionPromise = persistedSessionId
722
- ? this.restoreOrCreateSession(channelId, persistedSessionId, cwd, promptContext, additionalDirectories)
723
- : this.startFreshSession(channelId, cwd, promptContext, additionalDirectories);
788
+ ? this.restoreOrCreateSession(channelId, persistedSessionId, cwd, systemPromptAppend, additionalDirectories)
789
+ : this.startFreshSession(channelId, cwd, systemPromptAppend, additionalDirectories);
724
790
  this.pendingSessionStarts.add(sessionPromise);
725
791
  state.sessionPromise = sessionPromise;
726
792
  let sessionAdopted = false;
@@ -759,11 +825,11 @@ export class ClaudeCliClient {
759
825
  }
760
826
  }
761
827
  }
762
- async startFreshSession(channelId, cwd, promptContext, additionalDirectories) {
828
+ async startFreshSession(channelId, cwd, systemPromptAppend, additionalDirectories) {
763
829
  if (!this.connection) {
764
830
  throw new Error('Claude ACP connection is not available');
765
831
  }
766
- 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();
767
833
  this.logger?.debug('started fresh Claude ACP session', { channelId, cwd });
768
834
  const state = this.channels.get(channelId);
769
835
  if (state?.sessionPersistence === 'persistent') {
@@ -771,13 +837,13 @@ export class ClaudeCliClient {
771
837
  }
772
838
  return session;
773
839
  }
774
- async restoreOrCreateSession(channelId, sessionId, cwd, promptContext, additionalDirectories) {
840
+ async restoreOrCreateSession(channelId, sessionId, cwd, systemPromptAppend, additionalDirectories) {
775
841
  if (!this.sessionCapabilities.resumeSession) {
776
842
  this.logger?.debug('Claude ACP restore unsupported; starting fresh session', { channelId });
777
- return this.startFreshSession(channelId, cwd, promptContext, additionalDirectories);
843
+ return this.startFreshSession(channelId, cwd, systemPromptAppend, additionalDirectories);
778
844
  }
779
845
  try {
780
- const session = await this.restoreSession(sessionId, cwd, promptContext, additionalDirectories);
846
+ const session = await this.restoreSession(sessionId, cwd, systemPromptAppend, additionalDirectories);
781
847
  this.logger?.debug('restored Claude ACP session', {
782
848
  channelId,
783
849
  restoreMethod: this.sessionCapabilities.resumeSession ? 'session/resume' : 'session/load',
@@ -790,10 +856,10 @@ export class ClaudeCliClient {
790
856
  }
791
857
  this.logger?.debug('discarded stale Claude ACP session and started fresh', { channelId });
792
858
  await this.clearPersistedSessionBestEffort(channelId);
793
- return this.startFreshSession(channelId, cwd, promptContext, additionalDirectories);
859
+ return this.startFreshSession(channelId, cwd, systemPromptAppend, additionalDirectories);
794
860
  }
795
861
  }
796
- async restoreSession(sessionId, cwd, promptContext, additionalDirectories) {
862
+ async restoreSession(sessionId, cwd, systemPromptAppend, additionalDirectories) {
797
863
  if (!this.connection) {
798
864
  throw new Error('Claude ACP connection is not available');
799
865
  }
@@ -805,7 +871,7 @@ export class ClaudeCliClient {
805
871
  try {
806
872
  const request = {
807
873
  sessionId,
808
- ...buildSessionRequest(cwd, promptContext, additionalDirectories),
874
+ ...buildSessionRequest(cwd, systemPromptAppend, additionalDirectories),
809
875
  };
810
876
  if (this.sessionCapabilities.resumeSession) {
811
877
  await this.connection.agent.request(this.runtime.methods.agent.session.resume, request);
@@ -830,7 +896,7 @@ export class ClaudeCliClient {
830
896
  }
831
897
  }
832
898
  resolveSessionCwd(promptContext) {
833
- return promptContext?.taskWorkspace?.rootPath ?? this.runtime.cwd;
899
+ return promptContext?.resolvedWorkspace?.rootPath ?? this.runtime.cwd;
834
900
  }
835
901
  async recycleSessionIfScopeChanged(channelId, state) {
836
902
  const session = state.session;
@@ -862,7 +928,8 @@ export class ClaudeCliClient {
862
928
  const promptFailure = new Promise((_, reject) => {
863
929
  void promptPromise.catch((error) => reject(markSessionTainted(error)));
864
930
  });
865
- const collector = new ClaudeProgressCollector(options?.onProgress);
931
+ const collector = new AcpProgressCollector(options?.onProgress, readClaudeActivityMetadata);
932
+ const compactionObserver = new ClaudeCompactionObserver(options?.onProgress);
866
933
  for (;;) {
867
934
  let update;
868
935
  try {
@@ -888,6 +955,12 @@ export class ClaudeCliClient {
888
955
  }
889
956
  return output;
890
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
+ }
891
964
  collector.consume(update);
892
965
  }
893
966
  }
@@ -5,6 +5,7 @@ import { createRequire } from 'node:module';
5
5
  import spawn from 'cross-spawn';
6
6
  import { PROTOCOL_VERSION, client, methods, ndJsonStream, } from '@agentclientprotocol/sdk';
7
7
  import { HostLogger, summarizeChildStderr, summarizeError } from '../../debug.js';
8
+ import { AcpProgressCollector } from '../acp-progress-collector.js';
8
9
  import { assertCodexProjectDocumentSize, buildCodexProjectDocument } from './project-doc.js';
9
10
  import { isGatewayCredentialSidecarBasename } from '../../context/injection.js';
10
11
  import { IDLE_BACKEND_SHUTDOWN_DISABLED_MS, IdleBackendShutdownScheduler, } from '../idle-backend-shutdown.js';
@@ -60,9 +61,6 @@ const DEFAULT_RUNTIME = {
60
61
  },
61
62
  fetch: async (input, init) => fetch(input, init),
62
63
  };
63
- function hasVisibleText(value) {
64
- return typeof value === 'string' && value.trim().length > 0;
65
- }
66
64
  function cloneDirectories(directories) {
67
65
  return directories ? [...directories] : undefined;
68
66
  }
@@ -103,84 +101,6 @@ function serializePersistedSessionRecord(record) {
103
101
  ...(record.cwd ? { cwd: record.cwd } : {}),
104
102
  });
105
103
  }
106
- function formatToolProgress(title, status) {
107
- const normalizedTitle = hasVisibleText(title) ? title.trim() : null;
108
- switch (status) {
109
- case 'completed':
110
- return normalizedTitle ? `Completed ${normalizedTitle}` : 'Completed tool call';
111
- case 'failed':
112
- return normalizedTitle ? `Failed ${normalizedTitle}` : 'Tool call failed';
113
- case 'pending':
114
- case 'in_progress':
115
- case undefined:
116
- case null:
117
- return normalizedTitle ? `Running ${normalizedTitle}…` : 'Running tool…';
118
- default:
119
- return normalizedTitle ? `${status} ${normalizedTitle}` : status;
120
- }
121
- }
122
- function formatPlanProgress(entries) {
123
- const current = entries.find((entry) => entry.status === 'in_progress') ??
124
- entries.find((entry) => entry.status === 'pending') ??
125
- entries[0];
126
- return hasVisibleText(current?.content) ? `Plan: ${current.content.trim()}` : null;
127
- }
128
- class CodexProgressCollector {
129
- onProgress;
130
- publicText = '';
131
- lastPublished = null;
132
- toolTitles = new Map();
133
- constructor(onProgress) {
134
- this.onProgress = onProgress;
135
- }
136
- consume(update) {
137
- switch (update.update.sessionUpdate) {
138
- case 'agent_message_chunk':
139
- if (update.update.content.type !== 'text') {
140
- return;
141
- }
142
- this.publicText += update.update.content.text;
143
- this.publish(this.publicText);
144
- return;
145
- case 'tool_call':
146
- this.toolTitles.set(update.update.toolCallId, update.update.title);
147
- this.publishFallback(formatToolProgress(update.update.title, update.update.status));
148
- return;
149
- case 'tool_call_update': {
150
- const nextTitle = update.update.title ?? this.toolTitles.get(update.update.toolCallId);
151
- if (hasVisibleText(nextTitle)) {
152
- this.toolTitles.set(update.update.toolCallId, nextTitle);
153
- }
154
- this.publishFallback(formatToolProgress(nextTitle, update.update.status));
155
- return;
156
- }
157
- case 'plan':
158
- this.publishFallback(formatPlanProgress(update.update.entries.map((entry) => ({
159
- content: entry.content,
160
- status: entry.status,
161
- }))));
162
- return;
163
- default:
164
- return;
165
- }
166
- }
167
- getFinalText() {
168
- return this.publicText.trim();
169
- }
170
- publishFallback(text) {
171
- if (hasVisibleText(this.publicText)) {
172
- return;
173
- }
174
- this.publish(text);
175
- }
176
- publish(text) {
177
- if (!this.onProgress || !hasVisibleText(text) || text === this.lastPublished) {
178
- return;
179
- }
180
- this.lastPublished = text;
181
- this.onProgress({ text });
182
- }
183
- }
184
104
  function createDeferredTurn(channelId, preparedTurn, sessionPersistence, options) {
185
105
  let settled = false;
186
106
  let resolvePromise;
@@ -734,7 +654,7 @@ export class CodexCliClient {
734
654
  }
735
655
  }
736
656
  async resolveSessionCwd(promptContext) {
737
- return promptContext?.taskWorkspace?.rootPath ?? this.runtime.cwd;
657
+ return promptContext?.resolvedWorkspace?.rootPath ?? this.runtime.cwd;
738
658
  }
739
659
  async recycleSessionIfInjectionScopeChanged(channelId, state) {
740
660
  const session = state.session;
@@ -906,7 +826,7 @@ export class CodexCliClient {
906
826
  const promptFailure = new Promise((_, reject) => {
907
827
  void promptPromise.catch((error) => reject(markSessionTainted(error)));
908
828
  });
909
- const collector = new CodexProgressCollector(options?.onProgress);
829
+ const collector = new AcpProgressCollector(options?.onProgress);
910
830
  for (;;) {
911
831
  let update;
912
832
  try {
@@ -2,17 +2,20 @@ import { buildAttentionSummaryLines } from '../../context/attention.js';
2
2
  import { buildCollaborationCapabilityDeclarationSummaryLines, buildMissedCollaborationDiagnosticSummaryLines, } from '../../context/collaboration-capabilities-diagnostics.js';
3
3
  import { buildCollaborationOutcomeSummaryLines } from '../../context/collaboration-outcome.js';
4
4
  import { MAIN_SESSION_DELEGATION_LINES } from '../../context/main-session-delegation.js';
5
- import { buildSkillManualLines } from '../../context/skill-manual.js';
5
+ import { buildResolvedWorkspaceGuidanceLines } from '../../context/resolved-workspace.js';
6
+ import { buildSkillManualLines, buildSkillManualReadLine } from '../../context/skill-manual.js';
6
7
  import { buildTaskThreadCollaborationSummaryLines } from '../../context/task-thread-collaboration.js';
7
8
  const PROJECT_DOC_MAX_BYTES = 32 * 1024;
8
9
  // Every command the CLI carries needs the gateway credential file, so the capability is only
9
10
  // worth naming in a workspace that also names one; otherwise the model is handed an invocation
10
11
  // template it cannot fill.
11
12
  function buildSkillRuntimeLines(context) {
12
- if (!context?.skillRuntime || !context.gatewayCredentialPath) {
13
+ if (!context?.skillRuntime) {
13
14
  return [];
14
15
  }
15
- return buildSkillManualLines(context.skillRuntime);
16
+ return context.gatewayCredentialPath
17
+ ? buildSkillManualLines(context.skillRuntime)
18
+ : [buildSkillManualReadLine(context.skillRuntime)];
16
19
  }
17
20
  // The credential file is the whole handoff: it carries the gateway's base URL along with the
18
21
  // channel id and the token, and the CLI reads all three out of it. Naming the base URL separately
@@ -24,20 +27,18 @@ function buildGatewayCredentialLines(context) {
24
27
  return [`Gateway credential file: ${context.gatewayCredentialPath}`];
25
28
  }
26
29
  export function buildCodexProjectDocument(context) {
30
+ const sessionDelegationLines = context?.skillRuntime ? [] : MAIN_SESSION_DELEGATION_LINES;
27
31
  const lines = [
28
32
  '# Borgee channel workspace',
29
33
  '',
30
34
  'This workspace belongs to one Borgee channel session hosted by agents-host.',
31
35
  'Keep visible replies concise, do not claim actions you did not perform, and prefer the per-turn prompt when it provides newer turn-local details.',
32
36
  '',
33
- ...MAIN_SESSION_DELEGATION_LINES,
34
- ...(context?.taskWorkspace
35
- ? [
36
- '',
37
- `Writable task workspace root: ${context.taskWorkspace.rootPath}`,
38
- 'That writable task workspace is local to agents-host for the current task thread and does not imply that the target repository has already been checked out there.',
39
- ]
40
- : []),
37
+ ...sessionDelegationLines,
38
+ ...(() => {
39
+ const workspaceLines = buildResolvedWorkspaceGuidanceLines(context);
40
+ return workspaceLines.length > 0 ? ['', ...workspaceLines] : [];
41
+ })(),
41
42
  ...(() => {
42
43
  const collaborationOutcomeLines = buildCollaborationOutcomeSummaryLines(context?.collaborationOutcome);
43
44
  return collaborationOutcomeLines.length > 0 ? ['', ...collaborationOutcomeLines] : [];