@gajae-code/coding-agent 0.18.0 → 0.18.1

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 (40) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/dist/types/config/settings-schema.d.ts +2 -2
  3. package/dist/types/gjc-runtime/session-state-sidecar.d.ts +1 -0
  4. package/dist/types/sdk/acp/adapter.d.ts +3 -1
  5. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +1 -1
  6. package/dist/types/sdk/client/client.d.ts +5 -1
  7. package/dist/types/session/agent-session.d.ts +2 -0
  8. package/dist/types/task/index.d.ts +0 -1
  9. package/dist/types/tools/descriptors.d.ts +5 -0
  10. package/dist/types/tools/eval-backends.d.ts +37 -0
  11. package/dist/types/tools/eval.d.ts +1 -6
  12. package/dist/types/tools/index.d.ts +1 -36
  13. package/dist/types/tools/session-descriptions.d.ts +22 -0
  14. package/package.json +7 -7
  15. package/scripts/generate-sdk-operation-inventory.ts +2 -0
  16. package/src/cli/doctor/managed-link.ts +4 -4
  17. package/src/cli/doctor/repairs.ts +9 -1
  18. package/src/config/settings-schema.ts +8 -2
  19. package/src/exec/bash-shell-guardian.ts +31 -15
  20. package/src/gjc-runtime/session-state-sidecar.ts +88 -58
  21. package/src/internal-urls/docs-index.generated.ts +4 -4
  22. package/src/modes/acp/acp-agent.ts +97 -16
  23. package/src/modes/interactive-mode.ts +10 -0
  24. package/src/modes/utils/context-usage.ts +2 -6
  25. package/src/prompts/tools/task.md +8 -27
  26. package/src/sdk/acp/adapter.ts +38 -3
  27. package/src/sdk/broker/doctor-restart.ts +38 -3
  28. package/src/sdk/bus/index.ts +3 -3
  29. package/src/sdk/bus/telegram-daemon-contract.ts +1 -1
  30. package/src/sdk/client/client.ts +53 -35
  31. package/src/sdk/protocol/operation-inventory.generated.json +11 -0
  32. package/src/session/agent-session.ts +65 -15
  33. package/src/task/index.ts +21 -66
  34. package/src/tools/descriptors.ts +27 -1
  35. package/src/tools/eval-backends.ts +95 -0
  36. package/src/tools/eval.ts +3 -15
  37. package/src/tools/index.ts +9 -88
  38. package/src/tools/search.ts +2 -7
  39. package/src/tools/session-descriptions.ts +135 -0
  40. package/src/tools/tool-catalog.generated.ts +1 -1
@@ -14,6 +14,8 @@ export type SdkErrorCode =
14
14
  export class SdkClientError extends Error {
15
15
  readonly code: SdkErrorCode;
16
16
  readonly details: unknown;
17
+ /** True only when the SDK client itself reported a transport failure, never for a server error frame. */
18
+ readonly transport: boolean;
17
19
  /**
18
20
  * Reconnect-cycle diagnostics, separate from `details` because `details` is an
19
21
  * established contract: callers read the terminating transport error straight off
@@ -22,19 +24,35 @@ export class SdkClientError extends Error {
22
24
  * break every such reader, so the new attribution rides alongside it instead.
23
25
  */
24
26
  readonly reconnect?: SdkReconnectExhaustedDetails;
25
- constructor(code: SdkErrorCode, message: string, details?: unknown, reconnect?: SdkReconnectExhaustedDetails) {
27
+ constructor(
28
+ code: SdkErrorCode,
29
+ message: string,
30
+ details?: unknown,
31
+ reconnect?: SdkReconnectExhaustedDetails,
32
+ options: { transport?: boolean } = {},
33
+ ) {
26
34
  super(message);
27
35
  this.name = "SdkClientError";
28
36
  this.code = code;
29
37
  this.details = details;
38
+ this.transport = options.transport === true;
30
39
  if (reconnect) this.reconnect = reconnect;
31
40
  }
32
41
  }
33
42
 
43
+ function transportError(
44
+ code: SdkErrorCode,
45
+ message: string,
46
+ details?: unknown,
47
+ reconnect?: SdkReconnectExhaustedDetails,
48
+ ): SdkClientError {
49
+ return new SdkClientError(code, message, details, reconnect, { transport: true });
50
+ }
51
+
34
52
  /** A prepared transport operation failed before any wire handoff. */
35
53
  export class SdkPreparedDispatchError extends SdkClientError {
36
54
  constructor(error: SdkClientError) {
37
- super(error.code, error.message, error.details, error.reconnect);
55
+ super(error.code, error.message, error.details, error.reconnect, { transport: error.transport });
38
56
  this.name = "SdkPreparedDispatchError";
39
57
  }
40
58
  }
@@ -350,18 +368,18 @@ export class SdkClient {
350
368
  }
351
369
 
352
370
  send(frame: SdkFrame): void {
353
- if (this.#closed) throw new SdkClientError("connection_closed", "SDK client closed");
371
+ if (this.#closed) throw transportError("connection_closed", "SDK client closed");
354
372
  this.#throwIfDeadlineElapsed();
355
373
  const current = this.#currentSocketRecord ?? this.#opening?.candidate;
356
374
  const authoritative =
357
375
  this.#isActive(current ?? null) ||
358
376
  (!!current && current.phase === "hello" && this.#isCandidate(current.cycle, current));
359
377
  if (!current || !authoritative || current.socket.readyState !== WebSocket.OPEN)
360
- throw new SdkClientError("connection_closed", "SDK WebSocket is not connected");
378
+ throw transportError("connection_closed", "SDK WebSocket is not connected");
361
379
  try {
362
380
  current.socket.send(JSON.stringify(frame));
363
381
  } catch (error) {
364
- throw new SdkClientError("unavailable", "SDK WebSocket send failed", error);
382
+ throw transportError("unavailable", "SDK WebSocket send failed", error);
365
383
  }
366
384
  }
367
385
 
@@ -383,19 +401,19 @@ export class SdkClient {
383
401
  if (cycle.backoffTimer) clearTimeout(cycle.backoffTimer);
384
402
  if (cycle.candidate) {
385
403
  transports.add(cycle.candidate);
386
- this.#retire(cycle.candidate, new SdkClientError("connection_closed", "SDK client closed"), false);
404
+ this.#retire(cycle.candidate, transportError("connection_closed", "SDK client closed"), false);
387
405
  }
388
- cycle.rejectBackoff?.(new SdkClientError("connection_closed", "SDK client closed"));
406
+ cycle.rejectBackoff?.(transportError("connection_closed", "SDK client closed"));
389
407
  cycle.rejectBackoff = undefined;
390
408
  if (this.#opening === cycle) this.#opening = null;
391
409
  }
392
410
  const current = this.#currentSocketRecord;
393
411
  if (current) {
394
412
  transports.add(current);
395
- this.#retire(current, new SdkClientError("connection_closed", "SDK client closed"), false, true);
413
+ this.#retire(current, transportError("connection_closed", "SDK client closed"), false, true);
396
414
  }
397
415
  for (const [id, pending] of this.#pending)
398
- this.#settlePending(id, pending, new SdkClientError("connection_closed", "SDK client closed"), true);
416
+ this.#settlePending(id, pending, transportError("connection_closed", "SDK client closed"), true);
399
417
  try {
400
418
  await Promise.all([...transports].map(incarnation => this.#closeTransport(incarnation)));
401
419
  } finally {
@@ -469,7 +487,7 @@ export class SdkClient {
469
487
 
470
488
  async #request(frame: Frame, options: SdkRequestOptions, onResponse?: () => void): Promise<unknown> {
471
489
  if (this.#closed) {
472
- const error = new SdkClientError("connection_closed", "SDK client closed");
490
+ const error = transportError("connection_closed", "SDK client closed");
473
491
  throw options.connectedOnly ? new SdkPreparedDispatchError(error) : error;
474
492
  }
475
493
  try {
@@ -526,7 +544,7 @@ export class SdkClient {
526
544
  this.#settlePending(
527
545
  id,
528
546
  pending,
529
- new SdkClientError("timeout", `SDK request timed out after ${timeoutMs}ms`, {
547
+ transportError("timeout", `SDK request timed out after ${timeoutMs}ms`, {
530
548
  requestId: id,
531
549
  requestSent: pending.sent,
532
550
  } satisfies SdkRequestTimeoutDetails),
@@ -537,7 +555,7 @@ export class SdkClient {
537
555
  };
538
556
  this.#pending.set(id, pending);
539
557
  if (!this.#isActive(incarnation) || incarnation.socket.readyState !== WebSocket.OPEN) {
540
- const error = new SdkClientError("unavailable", "SDK WebSocket is not connected");
558
+ const error = transportError("unavailable", "SDK WebSocket is not connected");
541
559
  this.#settlePending(id, pending, options.connectedOnly ? new SdkPreparedDispatchError(error) : error);
542
560
  return await deferred.promise;
543
561
  }
@@ -591,9 +609,9 @@ export class SdkClient {
591
609
  pending,
592
610
  options.connectedOnly
593
611
  ? new SdkPreparedDispatchError(
594
- new SdkClientError("connection_closed", "SDK client closed during dispatch"),
612
+ transportError("connection_closed", "SDK client closed during dispatch"),
595
613
  )
596
- : new SdkClientError("connection_closed", "SDK client closed during dispatch"),
614
+ : transportError("connection_closed", "SDK client closed during dispatch"),
597
615
  );
598
616
  return await deferred.promise;
599
617
  }
@@ -603,7 +621,7 @@ export class SdkClient {
603
621
  return await deferred.promise;
604
622
  }
605
623
  if (incarnation.socket.readyState !== WebSocket.OPEN) {
606
- const error = new SdkClientError("unavailable", "SDK WebSocket is not connected");
624
+ const error = transportError("unavailable", "SDK WebSocket is not connected");
607
625
  this.#settlePending(id, pending, options.connectedOnly ? new SdkPreparedDispatchError(error) : error);
608
626
  return await deferred.promise;
609
627
  }
@@ -638,7 +656,7 @@ export class SdkClient {
638
656
  const sendError =
639
657
  error instanceof SdkClientError
640
658
  ? error
641
- : new SdkClientError("unavailable", "SDK WebSocket send failed", error);
659
+ : transportError("unavailable", "SDK WebSocket send failed", error);
642
660
  this.#settlePending(
643
661
  id,
644
662
  pending,
@@ -665,7 +683,7 @@ export class SdkClient {
665
683
  }
666
684
 
667
685
  #deadlineError(reconnect?: SdkReconnectExhaustedDetails): SdkClientError {
668
- return new SdkClientError("timeout", "SDK client deadline elapsed.", undefined, reconnect);
686
+ return transportError("timeout", "SDK client deadline elapsed.", undefined, reconnect);
669
687
  }
670
688
 
671
689
  #remainingTimeout(limit = this.#timeoutMs): number {
@@ -682,7 +700,7 @@ export class SdkClient {
682
700
  const current = this.#currentSocketRecord;
683
701
  if (current && this.#isActive(current) && current.socket.readyState === WebSocket.OPEN) return current;
684
702
  if (current)
685
- this.#retire(current, new SdkClientError("connection_closed", "SDK WebSocket connection closed"), true, true);
703
+ this.#retire(current, transportError("connection_closed", "SDK WebSocket connection closed"), true, true);
686
704
 
687
705
  let cycle = this.#opening;
688
706
  if (!cycle) {
@@ -696,7 +714,7 @@ export class SdkClient {
696
714
  #requireConnectedIncarnation(): Incarnation {
697
715
  const current = this.#currentSocketRecord;
698
716
  if (!current || !this.#isActive(current) || current.socket.readyState !== WebSocket.OPEN)
699
- throw new SdkClientError("connection_closed", "SDK WebSocket disconnected before prepared dispatch.");
717
+ throw transportError("connection_closed", "SDK WebSocket disconnected before prepared dispatch.");
700
718
  return current;
701
719
  }
702
720
 
@@ -711,7 +729,7 @@ export class SdkClient {
711
729
  reason,
712
730
  });
713
731
  const cancelled = (): SdkClientError =>
714
- new SdkClientError("connection_closed", "SDK client closed", lastError, diagnostics("cancelled"));
732
+ transportError("connection_closed", "SDK client closed", lastError, diagnostics("cancelled"));
715
733
  /**
716
734
  * A deadline and retry budget measure different failure axes. One-shot clients
717
735
  * need the deadline to fast-fail their operation, while long-lived ACP sessions
@@ -730,10 +748,10 @@ export class SdkClient {
730
748
  try {
731
749
  const incarnation = await this.#open(cycle);
732
750
  if (!this.#isActive(incarnation) && (!this.#isOpening(cycle) || cycle.candidate !== incarnation))
733
- throw new SdkClientError("connection_closed", "SDK WebSocket is not connected");
751
+ throw transportError("connection_closed", "SDK WebSocket is not connected");
734
752
  await this.#waitForHello(incarnation);
735
753
  if (this.#isActive(incarnation)) return incarnation;
736
- throw new SdkClientError("connection_closed", "SDK WebSocket is not connected");
754
+ throw transportError("connection_closed", "SDK WebSocket is not connected");
737
755
  } catch (error) {
738
756
  lastError = error;
739
757
  if (!this.#isOpening(cycle)) throw cancelled();
@@ -743,7 +761,7 @@ export class SdkClient {
743
761
  candidate,
744
762
  error instanceof SdkClientError
745
763
  ? error
746
- : new SdkClientError("unavailable", "SDK WebSocket connection failed", error),
764
+ : transportError("unavailable", "SDK WebSocket connection failed", error),
747
765
  true,
748
766
  );
749
767
  if (attempt < this.#reconnectAttempts) {
@@ -780,7 +798,7 @@ export class SdkClient {
780
798
  }
781
799
  cycle.phase = "complete";
782
800
  if (this.#opening === cycle) this.#opening = null;
783
- const error = new SdkClientError(
801
+ const error = transportError(
784
802
  "reconnect_exhausted",
785
803
  "SDK WebSocket reconnect attempts exhausted",
786
804
  lastError,
@@ -891,7 +909,7 @@ export class SdkClient {
891
909
  if (incarnation.failure) return Promise.reject(incarnation.failure);
892
910
  if (this.#isActive(incarnation)) return Promise.resolve();
893
911
  if (!this.#isCandidate(incarnation.cycle, incarnation) || incarnation.phase !== "hello")
894
- return Promise.reject(new SdkClientError("connection_closed", "SDK WebSocket is not connected"));
912
+ return Promise.reject(transportError("connection_closed", "SDK WebSocket is not connected"));
895
913
  const deferred = Promise.withResolvers<void>();
896
914
  incarnation.resolveHello = deferred.resolve;
897
915
  incarnation.rejectHello = deferred.reject;
@@ -903,7 +921,7 @@ export class SdkClient {
903
921
  const error =
904
922
  this.#deadline !== undefined && Date.now() >= this.#deadline
905
923
  ? this.#deadlineError()
906
- : new SdkClientError("timeout", `SDK WebSocket connection timed out after ${timeoutMs}ms`);
924
+ : transportError("timeout", `SDK WebSocket connection timed out after ${timeoutMs}ms`);
907
925
  incarnation.rejectOpen?.(error);
908
926
  this.#retire(incarnation, error, true);
909
927
  }
@@ -911,20 +929,20 @@ export class SdkClient {
911
929
  #onSocketFailure(incarnation: Incarnation, event?: Event): void {
912
930
  if (!this.#isCandidate(incarnation.cycle, incarnation) && !this.#isActive(incarnation)) return;
913
931
  const detail = event as (Event & { error?: unknown; message?: unknown }) | undefined;
932
+ let failureMessage = "SDK WebSocket connection closed";
933
+ if (detail?.error instanceof Error) failureMessage = detail.error.message;
934
+ else if (typeof detail?.message === "string") failureMessage = detail.message;
914
935
  const error =
915
- detail?.error instanceof Error
936
+ detail?.error instanceof SdkClientError && detail.error.transport
916
937
  ? detail.error
917
- : new SdkClientError(
918
- "connection_closed",
919
- typeof detail?.message === "string" ? detail.message : "SDK WebSocket connection closed",
920
- );
938
+ : transportError("connection_closed", failureMessage);
921
939
  if (incarnation.phase === "opening") incarnation.rejectOpen?.(error);
922
940
  if (incarnation.phase === "hello") incarnation.rejectHello?.(error);
923
941
  this.#retire(
924
942
  incarnation,
925
943
  error instanceof SdkClientError
926
944
  ? error
927
- : new SdkClientError("unavailable", "SDK WebSocket connection failed", error),
945
+ : transportError("unavailable", "SDK WebSocket connection failed", error),
928
946
  true,
929
947
  true,
930
948
  );
@@ -1115,7 +1133,7 @@ export class SdkClient {
1115
1133
  const onClose = (): void => resolve();
1116
1134
  socket.addEventListener("close", onClose, { once: true });
1117
1135
  const timer = setTimeout(
1118
- () => reject(new SdkClientError("timeout", `SDK WebSocket close timed out after ${timeoutMs}ms`)),
1136
+ () => reject(transportError("timeout", `SDK WebSocket close timed out after ${timeoutMs}ms`)),
1119
1137
  timeoutMs,
1120
1138
  );
1121
1139
  timer.unref?.();
@@ -1124,9 +1142,9 @@ export class SdkClient {
1124
1142
  if (Number(socket.readyState) === WebSocket.CLOSED) resolve();
1125
1143
  await promise;
1126
1144
  } catch (error) {
1127
- if (error instanceof SdkClientError) throw error;
1145
+ if (error instanceof SdkClientError && error.transport) throw error;
1128
1146
  if (Number(socket.readyState) !== WebSocket.CLOSED)
1129
- throw new SdkClientError("connection_closed", "SDK WebSocket close failed", error);
1147
+ throw transportError("connection_closed", "SDK WebSocket close failed", error);
1130
1148
  } finally {
1131
1149
  clearTimeout(timer);
1132
1150
  socket.removeEventListener("close", onClose);
@@ -5506,6 +5506,17 @@
5506
5506
  "packages/coding-agent/test/sdk-operation-inventory.test.ts"
5507
5507
  ]
5508
5508
  },
5509
+ {
5510
+ "sourceId": "agent_session:getAutoCompactionThresholdTokens",
5511
+ "sourceFile": "packages/coding-agent/src/session/agent-session.ts",
5512
+ "sourceKind": "agent_session",
5513
+ "decision": "exclude",
5514
+ "rationale": "internal compaction-threshold accessor feeding the /context breakdown; exposed through context.get, not an independent SDK operation",
5515
+ "exclusionMetadata": {
5516
+ "adapterMappings": "not_applicable",
5517
+ "testIds": "not_applicable"
5518
+ }
5519
+ },
5509
5520
  {
5510
5521
  "sourceId": "agent_session:getContextUsage",
5511
5522
  "sourceFile": "packages/coding-agent/src/session/agent-session.ts",
@@ -58,23 +58,28 @@ import {
58
58
  CompactionCancelledError,
59
59
  type CompactionPreparation,
60
60
  type CompactionResult,
61
+ type CompactionSettings,
61
62
  calculateContextTokens,
62
63
  calculatePromptTokens,
63
64
  collectEntriesForBranchSummary,
64
65
  compact,
65
66
  type EmergencyCompactionSample,
67
+ effectiveReserveTokens,
66
68
  emergencyCompactionReason,
67
69
  estimateMessageTokensHeuristic,
68
70
  estimateTextTokensHeuristic,
69
71
  generateBranchSummary,
70
72
  generateHandoff,
71
73
  IMAGE_TOKEN_ESTIMATE,
74
+ isDefaultAutoThresholdCeilingApplied,
72
75
  prepareCompaction,
73
76
  type RemoteCompactionFallbackHealthEvent,
74
77
  type RemoteCompactionFallbackHealthHooks,
78
+ resolveThresholdTokens,
75
79
  type SummaryOptions,
76
80
  shouldCompact,
77
81
  } from "@gajae-code/agent-core/compaction";
82
+
78
83
  import {
79
84
  commitToolOutputPrune,
80
85
  createPrunedNotice,
@@ -19954,10 +19959,15 @@ export class AgentSession {
19954
19959
  const compactionSettings = this.settings.getGroup("compaction");
19955
19960
  const pathEntries = this.#withoutEphemeralCustomMessageEntries(this.sessionManager.getBranch());
19956
19961
 
19957
- const preparation = prepareCompaction(pathEntries, compactionSettings, {
19958
- contextWindow: this.model.contextWindow,
19959
- tokenCorrectionRatio: this.#computeCompactionTokenCorrectionRatio(),
19960
- });
19962
+ const preparation = prepareCompaction(
19963
+ pathEntries,
19964
+ this.#compactionSettingsForContextPromotion(compactionSettings),
19965
+ {
19966
+ contextWindow: this.model.contextWindow,
19967
+ tokenCorrectionRatio: this.#computeCompactionTokenCorrectionRatio(),
19968
+ },
19969
+ );
19970
+
19961
19971
  if (!preparation) {
19962
19972
  // Check why we can't compact
19963
19973
  const lastEntry = pathEntries[pathEntries.length - 1];
@@ -20974,12 +20984,12 @@ export class AgentSession {
20974
20984
  }
20975
20985
  }
20976
20986
 
20977
- #recordAdaptiveCompactionCall<T extends { adaptive?: { enabled: boolean; turnWindow: number } }>(
20987
+ #recordAdaptiveCompactionCall(
20978
20988
  contextTokens: number,
20979
- settings: T,
20989
+ settings: CompactionSettings,
20980
20990
  assistantMessage: AssistantMessage,
20981
- ): T {
20982
- if (!settings.adaptive?.enabled) return settings;
20991
+ ): CompactionSettings {
20992
+ if (!settings.adaptive?.enabled) return this.#compactionSettingsWithAdaptiveState(settings);
20983
20993
  this.#adaptiveCompactionTracker.setWindowMs(settings.adaptive.turnWindow * 60_000);
20984
20994
  const messageKey = this.#adaptiveCompactionMessageKey(assistantMessage);
20985
20995
  if (this.#adaptiveCompactionRecordedMessageKey === messageKey) {
@@ -20994,11 +21004,31 @@ export class AgentSession {
20994
21004
  return `${message.provider}\u0000${message.model}\u0000${message.timestamp}\u0000${message.usage?.totalTokens ?? ""}`;
20995
21005
  }
20996
21006
 
20997
- #compactionSettingsWithAdaptiveState<T extends { adaptive?: { enabled: boolean } }>(settings: T): T {
20998
- if (!settings.adaptive?.enabled) return settings;
21007
+ #compactionSettingsWithAdaptiveState(settings: CompactionSettings): CompactionSettings {
21008
+ const adaptiveSettings = settings.adaptive?.enabled
21009
+ ? { ...settings, adaptiveState: this.#adaptiveCompactionTracker.decisionState() }
21010
+ : settings;
21011
+ return this.#compactionSettingsForContextPromotion(adaptiveSettings);
21012
+ }
21013
+
21014
+ #compactionSettingsForContextPromotion(settings: CompactionSettings): CompactionSettings {
21015
+ const contextWindow = this.model?.contextWindow ?? 0;
21016
+ const isContextPromoted = this.#temporaryProviderSessionScopes.some(
21017
+ scope => scope.token.reason === "context-promotion",
21018
+ );
21019
+ if (
21020
+ !isContextPromoted ||
21021
+ !isDefaultAutoThresholdCeilingApplied(contextWindow, settings)
21022
+ ) {
21023
+ return settings;
21024
+ }
21025
+
21026
+ // Context promotion is opt-in and exists to use the larger model's headroom.
21027
+ // Keep the reserve-based default on the promoted model instead of immediately
21028
+ // compacting at the same 300K ceiling that triggered promotion.
20999
21029
  return {
21000
21030
  ...settings,
21001
- adaptiveState: this.#adaptiveCompactionTracker.decisionState(),
21031
+ thresholdTokens: contextWindow - effectiveReserveTokens(contextWindow, settings),
21002
21032
  };
21003
21033
  }
21004
21034
 
@@ -22304,10 +22334,15 @@ export class AgentSession {
22304
22334
  // correction only when it SHRINKS the keep window (ratio >= 1), never when
22305
22335
  // it would grow it, so recovery cannot re-overflow the provider window.
22306
22336
  const overflowRatio = this.#computeCompactionTokenCorrectionRatio();
22307
- const preparation = prepareCompaction(pathEntries, compactionSettings, {
22308
- contextWindow: this.model?.contextWindow,
22309
- tokenCorrectionRatio: overflowRatio !== undefined ? Math.max(1, overflowRatio) : undefined,
22310
- });
22337
+ const preparation = prepareCompaction(
22338
+ pathEntries,
22339
+ this.#compactionSettingsForContextPromotion(compactionSettings),
22340
+ {
22341
+ contextWindow: this.model?.contextWindow,
22342
+ tokenCorrectionRatio: overflowRatio !== undefined ? Math.max(1, overflowRatio) : undefined,
22343
+ },
22344
+ );
22345
+
22311
22346
  if (autoCompactionSignal.aborted) return await emitAborted();
22312
22347
 
22313
22348
  if (!preparation) {
@@ -27184,6 +27219,21 @@ export class AgentSession {
27184
27219
  };
27185
27220
  }
27186
27221
 
27222
+ /** Current auto-compaction threshold, including active model-promotion policy. */
27223
+ getAutoCompactionThresholdTokens(contextTokens?: number): number {
27224
+ const model = this.model;
27225
+ if (!model) return 0;
27226
+ const contextWindow = model.contextWindow ?? 0;
27227
+ if (contextWindow <= 0) return 0;
27228
+ const settings = this.settings.getGroup("compaction") as CompactionSettings;
27229
+ return resolveThresholdTokens(
27230
+ contextWindow,
27231
+ this.#compactionSettingsWithAdaptiveState(settings),
27232
+ 0,
27233
+ contextTokens,
27234
+ );
27235
+ }
27236
+
27187
27237
  /**
27188
27238
  * Get current context usage statistics.
27189
27239
  * Uses the last assistant message's usage data when available,
package/src/task/index.ts CHANGED
@@ -32,11 +32,11 @@ import { resolveProfileBindings } from "../config/model-profiles";
32
32
  import { resolveAgentModelPatterns } from "../config/model-resolver";
33
33
  import type { Theme } from "../modes/theme/theme";
34
34
  import planModeSubagentPrompt from "../prompts/system/plan-mode-subagent.md" with { type: "text" };
35
- import taskDescriptionTemplate from "../prompts/tools/task.md" with { type: "text" };
36
35
  import taskSummaryTemplate from "../prompts/tools/task-summary.md" with { type: "text" };
37
36
  import type { ForkContextSeed } from "../session/agent-session";
38
37
  import { splitSelectorThinkingSuffix } from "../thinking";
39
38
  import { formatBytes, formatDuration } from "../tools/render-utils";
39
+ import { renderTaskDescription } from "../tools/session-descriptions";
40
40
  import { escapeXmlAttribute } from "../utils/xml-escape";
41
41
  import {
42
42
  type AgentDefinition,
@@ -317,57 +317,26 @@ export {
317
317
  taskSchema,
318
318
  } from "./types";
319
319
 
320
- /**
321
- * Render the tool description from a cached agent list and current settings.
322
- */
323
320
  function hasAvailableIrcTool(session: ToolSession): boolean {
324
321
  return session.settings.get("irc.enabled") === true && session.getToolByName?.("irc") !== undefined;
325
322
  }
326
323
 
327
- function renderDescription(
328
- agents: AgentDefinition[],
329
- maxConcurrency: number,
330
- isolationEnabled: boolean,
331
- asyncEnabled: boolean,
332
- disabledAgents: string[],
333
- simpleMode: TaskSimpleMode,
334
- ircEnabled: boolean,
335
- parentSpawns: string,
336
- autoroutingActive: boolean,
337
- ): string {
338
- const spawningDisabled = parentSpawns === "";
339
- let filteredAgents = filterVisibleAgents(agents);
340
- filteredAgents =
341
- disabledAgents.length > 0 ? filteredAgents.filter(a => !disabledAgents.includes(a.name)) : filteredAgents;
342
- if (spawningDisabled) {
343
- filteredAgents = [];
344
- } else if (parentSpawns !== "*") {
345
- const allowed = new Set(
346
- parentSpawns
347
- .split(",")
348
- .map(s => s.trim())
349
- .filter(Boolean),
350
- );
351
- filteredAgents = filteredAgents.filter(a => allowed.has(a.name));
352
- }
353
- const { contextEnabled, customSchemaEnabled } = getTaskSimpleModeCapabilities(simpleMode);
354
- const description = prompt.render(taskDescriptionTemplate, {
355
- agents: filteredAgents,
356
- spawningDisabled,
357
- MAX_CONCURRENCY: maxConcurrency,
358
- isolationEnabled,
359
- asyncEnabled,
360
- contextEnabled,
361
- customSchemaEnabled,
362
- ircEnabled,
363
- defaultMode: simpleMode === "default",
364
- schemaFreeMode: simpleMode === "schema-free",
365
- independentMode: simpleMode === "independent",
366
- autoroutingActive,
367
- });
368
- return description;
324
+ function callableAgents(agents: readonly AgentDefinition[], session: ToolSession): AgentDefinition[] {
325
+ const disabledAgents = new Set(session.settings.get("task.disabledAgents"));
326
+ const parentSpawns = session.getSessionSpawns() ?? "*";
327
+ const allowedSpawns =
328
+ parentSpawns === "*"
329
+ ? undefined
330
+ : new Set(
331
+ parentSpawns
332
+ .split(",")
333
+ .map(agent => agent.trim())
334
+ .filter(Boolean),
335
+ );
336
+ return filterVisibleAgents(agents).filter(
337
+ agent => !disabledAgents.has(agent.name) && (allowedSpawns === undefined || allowedSpawns.has(agent.name)),
338
+ );
369
339
  }
370
-
371
340
  function createTaskModeError(text: string): AgentToolResult<TaskToolDetails> {
372
341
  return {
373
342
  content: [{ type: "text", text }],
@@ -614,22 +583,8 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
614
583
  return renderTaskCall(args as TaskParams, options, theme);
615
584
  }
616
585
 
617
- /** Dynamic description that reflects current disabled-agent settings */
618
586
  get description(): string {
619
- const disabledAgents = this.session.settings.get("task.disabledAgents") as string[];
620
- const maxConcurrency = this.session.settings.get("task.maxConcurrency");
621
- const isolationMode = this.session.settings.get("task.isolation.mode");
622
- return renderDescription(
623
- this.#discoveredAgents,
624
- maxConcurrency,
625
- isolationMode !== "none",
626
- true,
627
- disabledAgents,
628
- this.#getTaskSimpleMode(),
629
- hasAvailableIrcTool(this.session),
630
- this.session.getSessionSpawns() ?? "*",
631
- this.session.settings.getEffectiveAutorouting().active,
632
- );
587
+ return renderTaskDescription(this.session);
633
588
  }
634
589
  readonly #sessionRepositoryBinding: RepositoryBinding;
635
590
  #testRunSubprocess: typeof runSubprocess | undefined;
@@ -945,8 +900,8 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
945
900
  const agent = getAgent(this.#discoveredAgents, params.agent);
946
901
  if (!agent) {
947
902
  const available =
948
- filterVisibleAgents(this.#discoveredAgents)
949
- .map(a => a.name)
903
+ callableAgents(this.#discoveredAgents, this.session)
904
+ .map(agent => agent.name)
950
905
  .join(", ") || "none";
951
906
  return {
952
907
  content: [{ type: "text", text: `Unknown agent "${params.agent}". Available: ${available}` }],
@@ -1714,8 +1669,8 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
1714
1669
  const agent = getAgent(agents, agentName);
1715
1670
  if (!agent) {
1716
1671
  const available =
1717
- filterVisibleAgents(agents)
1718
- .map(a => a.name)
1672
+ callableAgents(agents, this.session)
1673
+ .map(agent => agent.name)
1719
1674
  .join(", ") || "none";
1720
1675
  return {
1721
1676
  content: [
@@ -11,6 +11,12 @@ import {
11
11
  validateDeferredAskArguments,
12
12
  validateDeferredTodoArguments,
13
13
  } from "./descriptor-validation";
14
+ import {
15
+ evalToolDescriptionForSession,
16
+ renderTaskDescription,
17
+ searchToolDescriptionForSession,
18
+ taskParametersForSession,
19
+ } from "./session-descriptions";
14
20
  import { TOOL_CATALOG, type ToolCatalogEntry } from "./tool-catalog.generated";
15
21
  import { ToolError } from "./tool-errors";
16
22
 
@@ -33,6 +39,11 @@ export interface ToolDescriptorMetadata {
33
39
  readonly intent?: AgentTool<any, any, any>["intent"];
34
40
  readonly parametersForSession?: (session?: ToolSession) => TSchema;
35
41
  readonly parameters?: TSchema;
42
+ /**
43
+ * Renders the description the loaded tool will report for this session. Tools whose prompt text
44
+ * depends on session settings need it, or the advertised text changes on first load (#5992).
45
+ */
46
+ readonly descriptionForSession?: (session: ToolSession) => string;
36
47
  readonly description?: string;
37
48
  readonly label?: string;
38
49
  readonly customWireName?: string;
@@ -121,8 +132,10 @@ export class LazyAgentTool implements AgentTool<any, any, any> {
121
132
  return this.#tool?.name ?? this.descriptor.metadata.name;
122
133
  }
123
134
  get description(): string {
135
+ const session = this.#session;
124
136
  return (
125
137
  this.#tool?.description ??
138
+ (session ? this.descriptor.metadata.descriptionForSession?.(session) : undefined) ??
126
139
  this.descriptor.metadata.description ??
127
140
  this.descriptor.presentation.summary ??
128
141
  this.descriptor.metadata.summary ??
@@ -393,6 +406,7 @@ function descriptor(spec: DescriptorSpec): ToolDescriptor {
393
406
  inline: catalog?.inline ?? spec.inline,
394
407
  rawArgumentValidation: spec.rawArgumentValidation,
395
408
  parametersForSession: spec.parametersForSession,
409
+ descriptionForSession: spec.descriptionForSession,
396
410
  intent: catalog?.intent ?? spec.intent,
397
411
  description: catalog?.description ?? spec.description,
398
412
  parameters: spec.parameters ?? (catalog?.parameters as TSchema | undefined),
@@ -465,6 +479,15 @@ const names: Array<[name: string, label: string, summary: string | undefined, lo
465
479
  ["move_session", "Move Session", undefined, "essential"],
466
480
  ];
467
481
 
482
+ const sessionDescriptions: Readonly<Record<string, ToolDescriptorMetadata["descriptionForSession"]>> = {
483
+ eval: evalToolDescriptionForSession,
484
+ search: searchToolDescriptionForSession,
485
+ task: renderTaskDescription,
486
+ };
487
+
488
+ const sessionParameters: Readonly<Record<string, ToolDescriptorMetadata["parametersForSession"]>> = {
489
+ task: taskParametersForSession,
490
+ };
468
491
  const descriptorRawArgumentValidations: Readonly<Record<string, ToolDescriptorMetadata["rawArgumentValidation"]>> = {
469
492
  ask: validateDeferredAskArguments,
470
493
  todo_write: validateDeferredTodoArguments,
@@ -487,7 +510,10 @@ const builtins = names
487
510
  : undefined,
488
511
  parameters: name === "ask" ? deferredAskParameters : undefined,
489
512
  parametersForSession:
490
- name === "ask" ? session => selectAskParameters(session?.getDeepInterviewAskStage?.()) : undefined,
513
+ name === "ask"
514
+ ? session => selectAskParameters(session?.getDeepInterviewAskStage?.())
515
+ : sessionParameters[name],
516
+ descriptionForSession: sessionDescriptions[name],
491
517
  rawArgumentValidation: descriptorRawArgumentValidations[name],
492
518
  intent: deferredIntentPolicies[name],
493
519
  loader: loaders[name],