@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.
- package/CHANGELOG.md +35 -0
- package/dist/types/config/settings-schema.d.ts +2 -2
- package/dist/types/gjc-runtime/session-state-sidecar.d.ts +1 -0
- package/dist/types/sdk/acp/adapter.d.ts +3 -1
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +1 -1
- package/dist/types/sdk/client/client.d.ts +5 -1
- package/dist/types/session/agent-session.d.ts +2 -0
- package/dist/types/task/index.d.ts +0 -1
- package/dist/types/tools/descriptors.d.ts +5 -0
- package/dist/types/tools/eval-backends.d.ts +37 -0
- package/dist/types/tools/eval.d.ts +1 -6
- package/dist/types/tools/index.d.ts +1 -36
- package/dist/types/tools/session-descriptions.d.ts +22 -0
- package/package.json +7 -7
- package/scripts/generate-sdk-operation-inventory.ts +2 -0
- package/src/cli/doctor/managed-link.ts +4 -4
- package/src/cli/doctor/repairs.ts +9 -1
- package/src/config/settings-schema.ts +8 -2
- package/src/exec/bash-shell-guardian.ts +31 -15
- package/src/gjc-runtime/session-state-sidecar.ts +88 -58
- package/src/internal-urls/docs-index.generated.ts +4 -4
- package/src/modes/acp/acp-agent.ts +97 -16
- package/src/modes/interactive-mode.ts +10 -0
- package/src/modes/utils/context-usage.ts +2 -6
- package/src/prompts/tools/task.md +8 -27
- package/src/sdk/acp/adapter.ts +38 -3
- package/src/sdk/broker/doctor-restart.ts +38 -3
- package/src/sdk/bus/index.ts +3 -3
- package/src/sdk/bus/telegram-daemon-contract.ts +1 -1
- package/src/sdk/client/client.ts +53 -35
- package/src/sdk/protocol/operation-inventory.generated.json +11 -0
- package/src/session/agent-session.ts +65 -15
- package/src/task/index.ts +21 -66
- package/src/tools/descriptors.ts +27 -1
- package/src/tools/eval-backends.ts +95 -0
- package/src/tools/eval.ts +3 -15
- package/src/tools/index.ts +9 -88
- package/src/tools/search.ts +2 -7
- package/src/tools/session-descriptions.ts +135 -0
- package/src/tools/tool-catalog.generated.ts +1 -1
package/src/sdk/client/client.ts
CHANGED
|
@@ -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(
|
|
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
|
|
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
|
|
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
|
|
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,
|
|
404
|
+
this.#retire(cycle.candidate, transportError("connection_closed", "SDK client closed"), false);
|
|
387
405
|
}
|
|
388
|
-
cycle.rejectBackoff?.(
|
|
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,
|
|
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,
|
|
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 =
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
612
|
+
transportError("connection_closed", "SDK client closed during dispatch"),
|
|
595
613
|
)
|
|
596
|
-
:
|
|
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 =
|
|
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
|
-
:
|
|
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
|
|
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,
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
:
|
|
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 =
|
|
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(
|
|
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
|
-
:
|
|
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
|
|
936
|
+
detail?.error instanceof SdkClientError && detail.error.transport
|
|
916
937
|
? detail.error
|
|
917
|
-
:
|
|
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
|
-
:
|
|
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(
|
|
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
|
|
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(
|
|
19958
|
-
|
|
19959
|
-
|
|
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
|
|
20987
|
+
#recordAdaptiveCompactionCall(
|
|
20978
20988
|
contextTokens: number,
|
|
20979
|
-
settings:
|
|
20989
|
+
settings: CompactionSettings,
|
|
20980
20990
|
assistantMessage: AssistantMessage,
|
|
20981
|
-
):
|
|
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
|
|
20998
|
-
|
|
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
|
-
|
|
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(
|
|
22308
|
-
|
|
22309
|
-
|
|
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
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
)
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
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
|
-
|
|
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
|
-
|
|
949
|
-
.map(
|
|
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
|
-
|
|
1718
|
-
.map(
|
|
1672
|
+
callableAgents(agents, this.session)
|
|
1673
|
+
.map(agent => agent.name)
|
|
1719
1674
|
.join(", ") || "none";
|
|
1720
1675
|
return {
|
|
1721
1676
|
content: [
|
package/src/tools/descriptors.ts
CHANGED
|
@@ -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"
|
|
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],
|