condukt 0.16.9 → 0.16.11
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/dist/runtimes/copilot/copilot-adapter.d.ts.map +1 -1
- package/dist/runtimes/copilot/copilot-adapter.js +12 -0
- package/dist/runtimes/copilot/copilot-adapter.js.map +1 -1
- package/dist/runtimes/copilot/copilot-backend.d.ts +93 -2
- package/dist/runtimes/copilot/copilot-backend.d.ts.map +1 -1
- package/dist/runtimes/copilot/index.d.ts +3 -3
- package/dist/runtimes/copilot/index.d.ts.map +1 -1
- package/dist/runtimes/copilot/index.js +8 -1
- package/dist/runtimes/copilot/index.js.map +1 -1
- package/dist/runtimes/copilot/sdk-backend.d.ts +8 -2
- package/dist/runtimes/copilot/sdk-backend.d.ts.map +1 -1
- package/dist/runtimes/copilot/sdk-backend.js +902 -117
- package/dist/runtimes/copilot/sdk-backend.js.map +1 -1
- package/dist/runtimes/copilot/subagents.d.ts +94 -1
- package/dist/runtimes/copilot/subagents.d.ts.map +1 -1
- package/dist/runtimes/copilot/subagents.js +138 -10
- package/dist/runtimes/copilot/subagents.js.map +1 -1
- package/dist/src/agent-node.d.ts +3 -2
- package/dist/src/agent-node.d.ts.map +1 -1
- package/dist/src/agent-node.js +4 -0
- package/dist/src/agent-node.js.map +1 -1
- package/dist/src/agent.d.ts.map +1 -1
- package/dist/src/agent.js +4 -0
- package/dist/src/agent.js.map +1 -1
- package/dist/src/index.d.ts +1 -1
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js.map +1 -1
- package/dist/src/panel-node.d.ts +4 -2
- package/dist/src/panel-node.d.ts.map +1 -1
- package/dist/src/panel-node.js +4 -0
- package/dist/src/panel-node.js.map +1 -1
- package/dist/src/types.d.ts +92 -4
- package/dist/src/types.d.ts.map +1 -1
- package/dist/src/types.js.map +1 -1
- package/package.json +1 -1
|
@@ -49,9 +49,45 @@ exports.SdkBackend = void 0;
|
|
|
49
49
|
const cp = __importStar(require("child_process"));
|
|
50
50
|
const fs = __importStar(require("fs"));
|
|
51
51
|
const path = __importStar(require("path"));
|
|
52
|
+
const node_crypto_1 = require("node:crypto");
|
|
52
53
|
const types_1 = require("../../src/types");
|
|
53
54
|
const lifecycle_events_1 = require("./lifecycle-events");
|
|
54
55
|
const subagents_1 = require("./subagents");
|
|
56
|
+
// The CLI requires at least four messages before history.compact() can make
|
|
57
|
+
// progress. This is a runtime constraint, not a tuned safety threshold.
|
|
58
|
+
const MIN_MESSAGES_FOR_COMPACTION = 4;
|
|
59
|
+
function parseContextUsage(data) {
|
|
60
|
+
if (!data)
|
|
61
|
+
return undefined;
|
|
62
|
+
const currentTokens = data.currentTokens;
|
|
63
|
+
const tokenLimit = data.tokenLimit;
|
|
64
|
+
const messagesLength = data.messagesLength;
|
|
65
|
+
if (typeof currentTokens !== 'number'
|
|
66
|
+
|| !Number.isFinite(currentTokens)
|
|
67
|
+
|| currentTokens < 0
|
|
68
|
+
|| typeof tokenLimit !== 'number'
|
|
69
|
+
|| !Number.isFinite(tokenLimit)
|
|
70
|
+
|| tokenLimit <= 0
|
|
71
|
+
|| typeof messagesLength !== 'number'
|
|
72
|
+
|| !Number.isFinite(messagesLength)
|
|
73
|
+
|| messagesLength < 0) {
|
|
74
|
+
return undefined;
|
|
75
|
+
}
|
|
76
|
+
return {
|
|
77
|
+
currentTokens,
|
|
78
|
+
tokenLimit,
|
|
79
|
+
messagesLength,
|
|
80
|
+
...(nonNegativeFiniteNumber(data.systemTokens) !== undefined
|
|
81
|
+
? { systemTokens: data.systemTokens }
|
|
82
|
+
: {}),
|
|
83
|
+
...(nonNegativeFiniteNumber(data.conversationTokens) !== undefined
|
|
84
|
+
? { conversationTokens: data.conversationTokens }
|
|
85
|
+
: {}),
|
|
86
|
+
...(nonNegativeFiniteNumber(data.toolDefinitionsTokens) !== undefined
|
|
87
|
+
? { toolDefinitionsTokens: data.toolDefinitionsTokens }
|
|
88
|
+
: {}),
|
|
89
|
+
};
|
|
90
|
+
}
|
|
55
91
|
function normalizeSdkEvent(event) {
|
|
56
92
|
if (typeof event !== 'object' || event === null)
|
|
57
93
|
return {};
|
|
@@ -64,15 +100,114 @@ function normalizeSdkEvent(event) {
|
|
|
64
100
|
: {}),
|
|
65
101
|
};
|
|
66
102
|
}
|
|
103
|
+
function isChildModelRequest(context) {
|
|
104
|
+
if (context.interactionType === 'conversation-compaction')
|
|
105
|
+
return false;
|
|
106
|
+
if (context.interactionType === 'conversation-subagent'
|
|
107
|
+
|| context.interactionType === 'conversation-sampling') {
|
|
108
|
+
return true;
|
|
109
|
+
}
|
|
110
|
+
// Generic child traffic must carry both sides of its parent relationship.
|
|
111
|
+
// Treat incomplete or ambiguous provenance as parent traffic so uncertainty
|
|
112
|
+
// cannot bypass parent guards.
|
|
113
|
+
return context.agentId !== undefined && context.parentAgentId !== undefined;
|
|
114
|
+
}
|
|
115
|
+
function modelRequestPurpose(interactionType, child, retry) {
|
|
116
|
+
if (interactionType === 'conversation-compaction')
|
|
117
|
+
return 'compaction';
|
|
118
|
+
if (retry)
|
|
119
|
+
return 'retry';
|
|
120
|
+
return child ? 'child' : 'normal-turn';
|
|
121
|
+
}
|
|
122
|
+
function observedRequestBodyTelemetry(request) {
|
|
123
|
+
const contentLength = request.headers.get('content-length');
|
|
124
|
+
const parsedLength = contentLength === null ? undefined : Number(contentLength);
|
|
125
|
+
return Number.isFinite(parsedLength) && parsedLength !== undefined && parsedLength >= 0
|
|
126
|
+
? { observedRequestBodyBytes: parsedLength }
|
|
127
|
+
: {};
|
|
128
|
+
}
|
|
129
|
+
function createObservedRequestHandler(RequestHandler, WebSocketForwarder, observe, usageFor, refreshParentUsage, retryState, canDispatch) {
|
|
130
|
+
return new class extends RequestHandler {
|
|
131
|
+
parentObservationTail = Promise.resolve();
|
|
132
|
+
async sendRequest(request, context) {
|
|
133
|
+
let serialized = observedRequestBodyTelemetry(request);
|
|
134
|
+
try {
|
|
135
|
+
const body = await request.clone().arrayBuffer();
|
|
136
|
+
serialized = {
|
|
137
|
+
observedRequestBodyBytes: body.byteLength,
|
|
138
|
+
observedRequestBodySha256: (0, node_crypto_1.createHash)('sha256')
|
|
139
|
+
.update(new Uint8Array(body))
|
|
140
|
+
.digest('hex'),
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
catch {
|
|
144
|
+
// A bodyless or non-cloneable request still carries content-length when known.
|
|
145
|
+
}
|
|
146
|
+
await this.observeRequest(context, serialized);
|
|
147
|
+
return super.sendRequest(request, context);
|
|
148
|
+
}
|
|
149
|
+
async openWebSocket(context) {
|
|
150
|
+
await this.observeRequest(context, {});
|
|
151
|
+
return new WebSocketForwarder(context);
|
|
152
|
+
}
|
|
153
|
+
observeRequest(context, serialized) {
|
|
154
|
+
const child = isChildModelRequest(context);
|
|
155
|
+
if (child)
|
|
156
|
+
return this.observeRequestNow(context, serialized, true);
|
|
157
|
+
const previous = this.parentObservationTail;
|
|
158
|
+
const observation = previous.then(() => this.observeRequestNow(context, serialized, false));
|
|
159
|
+
this.parentObservationTail = observation.catch(() => undefined);
|
|
160
|
+
return observation;
|
|
161
|
+
}
|
|
162
|
+
async observeRequestNow(context, serialized, child) {
|
|
163
|
+
const compaction = context.interactionType === 'conversation-compaction';
|
|
164
|
+
const retryAgentId = child ? context.agentId : undefined;
|
|
165
|
+
const retry = compaction || (child && retryAgentId === undefined)
|
|
166
|
+
? false
|
|
167
|
+
: retryState(retryAgentId, false);
|
|
168
|
+
const purpose = modelRequestPurpose(context.interactionType, child, retry);
|
|
169
|
+
const childUsage = child && context.agentId !== undefined
|
|
170
|
+
? usageFor(context.agentId)
|
|
171
|
+
: undefined;
|
|
172
|
+
const measurement = child
|
|
173
|
+
? childUsage === undefined
|
|
174
|
+
? undefined
|
|
175
|
+
: { usage: childUsage, observedInterRequestGrowth: 0 }
|
|
176
|
+
: await refreshParentUsage(purpose);
|
|
177
|
+
const usage = measurement?.usage;
|
|
178
|
+
observe({
|
|
179
|
+
requestId: context.requestId,
|
|
180
|
+
sessionId: context.sessionId,
|
|
181
|
+
agentId: context.agentId,
|
|
182
|
+
parentAgentId: context.parentAgentId,
|
|
183
|
+
purpose,
|
|
184
|
+
interactionType: context.interactionType,
|
|
185
|
+
currentTokens: usage?.currentTokens,
|
|
186
|
+
tokenLimit: usage?.tokenLimit,
|
|
187
|
+
messagesLength: usage?.messagesLength,
|
|
188
|
+
systemTokens: usage?.systemTokens,
|
|
189
|
+
conversationTokens: usage?.conversationTokens,
|
|
190
|
+
toolDefinitionsTokens: usage?.toolDefinitionsTokens,
|
|
191
|
+
...serialized,
|
|
192
|
+
});
|
|
193
|
+
if (!canDispatch(context, purpose, child, measurement)) {
|
|
194
|
+
throw new Error(`Blocked ${purpose} model request ${context.requestId}: parent context headroom has not been restored`);
|
|
195
|
+
}
|
|
196
|
+
if (retry)
|
|
197
|
+
retryState(retryAgentId, true);
|
|
198
|
+
}
|
|
199
|
+
}();
|
|
200
|
+
}
|
|
67
201
|
const NAMED_SDK_EVENTS = new Set([
|
|
68
202
|
'assistant.message', 'assistant.message_delta',
|
|
69
203
|
'assistant.reasoning', 'assistant.reasoning_delta',
|
|
204
|
+
'assistant.turn_retry',
|
|
70
205
|
'assistant.intent', 'assistant.usage',
|
|
71
206
|
'tool.execution_start', 'tool.execution_complete',
|
|
72
207
|
'tool.execution_partial_result',
|
|
73
208
|
'session.idle', 'session.task_complete', 'session.error',
|
|
74
209
|
'model.call_failure', 'abort',
|
|
75
|
-
'session.compaction_start', 'session.compaction_complete',
|
|
210
|
+
'session.usage_info', 'session.compaction_start', 'session.compaction_complete',
|
|
76
211
|
'subagent.started', 'subagent.completed', 'subagent.failed',
|
|
77
212
|
'permission.requested',
|
|
78
213
|
]);
|
|
@@ -336,6 +471,10 @@ class SdkBackend {
|
|
|
336
471
|
mcpConfigPath;
|
|
337
472
|
configDirectory;
|
|
338
473
|
subagentRoster;
|
|
474
|
+
complementaryModelPolicy;
|
|
475
|
+
subagentsEnabled;
|
|
476
|
+
maxDepth;
|
|
477
|
+
maxConcurrency;
|
|
339
478
|
extraPathDirs;
|
|
340
479
|
pathTools;
|
|
341
480
|
logger;
|
|
@@ -345,6 +484,11 @@ class SdkBackend {
|
|
|
345
484
|
this.mcpConfigPath = options.mcpConfigPath;
|
|
346
485
|
this.configDirectory = options.configDir;
|
|
347
486
|
this.subagentRoster = options.subagentRoster;
|
|
487
|
+
this.complementaryModelPolicy = options.complementaryModelPolicy
|
|
488
|
+
?? subagents_1.DEFAULT_COMPLEMENTARY_MODEL_POLICY;
|
|
489
|
+
this.subagentsEnabled = options.subagentsEnabled ?? true;
|
|
490
|
+
this.maxDepth = options.maxDepth;
|
|
491
|
+
this.maxConcurrency = options.maxConcurrency;
|
|
348
492
|
this.extraPathDirs = options.extraPathDirs ?? [];
|
|
349
493
|
this.pathTools = options.pathTools ?? [];
|
|
350
494
|
this.logger = options.logger ?? types_1.NO_OP_LOGGER;
|
|
@@ -366,7 +510,7 @@ class SdkBackend {
|
|
|
366
510
|
if (cached)
|
|
367
511
|
return cached;
|
|
368
512
|
const resolution = this.resolveModelCapabilitiesUncached(client, model).catch((err) => {
|
|
369
|
-
this.logger.warn('Copilot model capability resolution failed;
|
|
513
|
+
this.logger.warn('Copilot model capability resolution failed; awaiting runtime context accounting', {
|
|
370
514
|
model,
|
|
371
515
|
reason: 'capability_resolution_failed',
|
|
372
516
|
error: err instanceof Error ? err.message : String(err),
|
|
@@ -382,7 +526,7 @@ class SdkBackend {
|
|
|
382
526
|
return {};
|
|
383
527
|
const selected = models.find(candidate => candidate.id === model);
|
|
384
528
|
if (!selected) {
|
|
385
|
-
this.logger.warn('Copilot model capability lookup failed;
|
|
529
|
+
this.logger.warn('Copilot model capability lookup failed; awaiting runtime context accounting', {
|
|
386
530
|
model,
|
|
387
531
|
reason: 'model_not_found',
|
|
388
532
|
});
|
|
@@ -402,7 +546,7 @@ class SdkBackend {
|
|
|
402
546
|
? contextWindowLimit
|
|
403
547
|
: undefined;
|
|
404
548
|
if (source === undefined || discoveredLimit === undefined) {
|
|
405
|
-
this.logger.warn('Copilot model capability lookup returned no usable token limit;
|
|
549
|
+
this.logger.warn('Copilot model capability lookup returned no usable token limit; awaiting runtime context accounting', {
|
|
406
550
|
model,
|
|
407
551
|
reason: 'invalid_limit',
|
|
408
552
|
maxPromptTokens: promptLimit,
|
|
@@ -410,28 +554,14 @@ class SdkBackend {
|
|
|
410
554
|
});
|
|
411
555
|
return {};
|
|
412
556
|
}
|
|
413
|
-
|
|
414
|
-
if (!isPositiveFiniteNumber(promptTokenCeiling)) {
|
|
415
|
-
this.logger.warn('Copilot model capability lookup produced no usable prompt-token ceiling; using proportional compaction thresholds only', {
|
|
416
|
-
model,
|
|
417
|
-
reason: 'invalid_computed_ceiling',
|
|
418
|
-
discoveredLimit,
|
|
419
|
-
limitSource: source,
|
|
420
|
-
});
|
|
421
|
-
return {};
|
|
422
|
-
}
|
|
423
|
-
this.logger.info('Resolved Copilot model prompt-token ceiling', {
|
|
557
|
+
this.logger.info('Resolved Copilot model prompt-token limit', {
|
|
424
558
|
model,
|
|
425
|
-
discoveredLimit,
|
|
559
|
+
reportedPromptTokenLimit: discoveredLimit,
|
|
426
560
|
limitSource: source,
|
|
427
|
-
promptTokenCeiling,
|
|
428
561
|
});
|
|
429
562
|
return {
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
max_prompt_tokens: promptTokenCeiling,
|
|
433
|
-
},
|
|
434
|
-
},
|
|
563
|
+
reportedPromptTokenLimit: Math.floor(discoveredLimit),
|
|
564
|
+
limitSource: source,
|
|
435
565
|
};
|
|
436
566
|
}
|
|
437
567
|
listModels(client) {
|
|
@@ -443,7 +573,7 @@ class SdkBackend {
|
|
|
443
573
|
return await client.listModels();
|
|
444
574
|
}
|
|
445
575
|
catch (err) {
|
|
446
|
-
this.logger.warn('Copilot model capability discovery failed;
|
|
576
|
+
this.logger.warn('Copilot model capability discovery failed; awaiting runtime context accounting', {
|
|
447
577
|
reason: 'list_models_failed',
|
|
448
578
|
error: err instanceof Error ? err.message : String(err),
|
|
449
579
|
});
|
|
@@ -456,7 +586,7 @@ class SdkBackend {
|
|
|
456
586
|
if (options?.signal?.aborted) {
|
|
457
587
|
throw new Error('Session creation aborted');
|
|
458
588
|
}
|
|
459
|
-
return new SdkSession(config, this.mcpConfigPath, this.configDirectory, this.subagentRoster, this.extraPathDirs, this.pathTools, this.logger, (client, model) => this.resolveModelCapabilities(client, model));
|
|
589
|
+
return new SdkSession(config, this.mcpConfigPath, this.configDirectory, this.subagentRoster, this.complementaryModelPolicy, this.subagentsEnabled, this.maxDepth, this.maxConcurrency, this.extraPathDirs, this.pathTools, this.logger, (client, model) => this.resolveModelCapabilities(client, model));
|
|
460
590
|
}
|
|
461
591
|
}
|
|
462
592
|
exports.SdkBackend = SdkBackend;
|
|
@@ -481,6 +611,10 @@ class SdkSession {
|
|
|
481
611
|
mcpConfigPath;
|
|
482
612
|
configDirectory;
|
|
483
613
|
backendRoster;
|
|
614
|
+
complementaryModelPolicy;
|
|
615
|
+
backendSubagentsEnabled;
|
|
616
|
+
backendMaxDepth;
|
|
617
|
+
backendMaxConcurrency;
|
|
484
618
|
extraPathDirs;
|
|
485
619
|
pathTools;
|
|
486
620
|
logger;
|
|
@@ -490,11 +624,23 @@ class SdkSession {
|
|
|
490
624
|
compactionTimer = null;
|
|
491
625
|
abortGraceTimer = null;
|
|
492
626
|
compactionInProgress = false;
|
|
627
|
+
parentCompactionGeneration = 0;
|
|
628
|
+
verifiedParentCompactionGeneration = 0;
|
|
629
|
+
compactionBaselinePromise;
|
|
630
|
+
proactiveCompactionPromise;
|
|
631
|
+
reportedPromptTokenLimit;
|
|
632
|
+
reportedPromptTokenLimitSource;
|
|
633
|
+
adaptiveCompactionPolicy;
|
|
634
|
+
previousParentRequestTokens;
|
|
635
|
+
largestObservedInterRequestGrowth = 0;
|
|
636
|
+
parentUsage;
|
|
637
|
+
latestContextDiagnostics;
|
|
638
|
+
childUsage = new Map();
|
|
639
|
+
retryingAgentIds = new Set();
|
|
640
|
+
retryingParent = false;
|
|
493
641
|
turnSettled = false;
|
|
494
|
-
compactionRecoveryInProgress = false;
|
|
495
642
|
aborted = false;
|
|
496
643
|
_turnText = new Map();
|
|
497
|
-
intentionalAborts = new WeakSet();
|
|
498
644
|
/**
|
|
499
645
|
* Maps toolCallId -> toolName for attributing tool_complete events.
|
|
500
646
|
* Populated from assistant.message toolRequests and tool.execution_start.
|
|
@@ -515,11 +661,57 @@ class SdkSession {
|
|
|
515
661
|
// SDK manages the CLI process internally; no direct PID access
|
|
516
662
|
return null;
|
|
517
663
|
}
|
|
518
|
-
|
|
664
|
+
history = {
|
|
665
|
+
summarizeForHandoff: async () => {
|
|
666
|
+
const sdkSession = this.activeSdkSession();
|
|
667
|
+
const result = await sdkSession.rpc.history.summarizeForHandoff();
|
|
668
|
+
this.logger.info('Copilot history handoff summary completed', {
|
|
669
|
+
summaryLength: result.summary.length,
|
|
670
|
+
});
|
|
671
|
+
return result.summary;
|
|
672
|
+
},
|
|
673
|
+
truncate: async (eventId) => {
|
|
674
|
+
const sdkSession = this.activeSdkSession();
|
|
675
|
+
const result = await sdkSession.rpc.history.truncate({ eventId });
|
|
676
|
+
this.logger.warn('Copilot history truncation completed', {
|
|
677
|
+
eventId,
|
|
678
|
+
eventsRemoved: result.eventsRemoved,
|
|
679
|
+
});
|
|
680
|
+
return result.eventsRemoved;
|
|
681
|
+
},
|
|
682
|
+
};
|
|
683
|
+
metadata = {
|
|
684
|
+
getContextAttribution: async () => {
|
|
685
|
+
const result = await this.activeSdkSession().rpc.metadata.getContextAttribution();
|
|
686
|
+
return result.contextAttribution ?? null;
|
|
687
|
+
},
|
|
688
|
+
getContextHeaviestMessages: async (limit) => {
|
|
689
|
+
return this.activeSdkSession().rpc.metadata.getContextHeaviestMessages(limit === undefined ? {} : { limit });
|
|
690
|
+
},
|
|
691
|
+
recomputeContextTokens: async (modelId = this.config.model) => {
|
|
692
|
+
return this.activeSdkSession().rpc.metadata.recomputeContextTokens({ modelId });
|
|
693
|
+
},
|
|
694
|
+
};
|
|
695
|
+
usage = {
|
|
696
|
+
getMetrics: async () => {
|
|
697
|
+
return this.activeSdkSession().rpc.usage.getMetrics();
|
|
698
|
+
},
|
|
699
|
+
};
|
|
700
|
+
activeSdkSession() {
|
|
701
|
+
const sdkSession = this._sdkSession;
|
|
702
|
+
if (!sdkSession)
|
|
703
|
+
throw new Error('SDK session is not active');
|
|
704
|
+
return sdkSession;
|
|
705
|
+
}
|
|
706
|
+
constructor(config, mcpConfigPath, configDirectory, backendRoster, complementaryModelPolicy, backendSubagentsEnabled, backendMaxDepth, backendMaxConcurrency, extraPathDirs, pathTools, logger, resolveModelCapabilities) {
|
|
519
707
|
this.config = config;
|
|
520
708
|
this.mcpConfigPath = mcpConfigPath;
|
|
521
709
|
this.configDirectory = configDirectory;
|
|
522
710
|
this.backendRoster = backendRoster;
|
|
711
|
+
this.complementaryModelPolicy = complementaryModelPolicy;
|
|
712
|
+
this.backendSubagentsEnabled = backendSubagentsEnabled;
|
|
713
|
+
this.backendMaxDepth = backendMaxDepth;
|
|
714
|
+
this.backendMaxConcurrency = backendMaxConcurrency;
|
|
523
715
|
this.extraPathDirs = extraPathDirs;
|
|
524
716
|
this.pathTools = pathTools;
|
|
525
717
|
this.logger = logger;
|
|
@@ -542,6 +734,16 @@ class SdkSession {
|
|
|
542
734
|
this._toolCallNames.clear();
|
|
543
735
|
this._callIdToParent.clear();
|
|
544
736
|
this._pendingPartials.clear();
|
|
737
|
+
this.parentUsage = undefined;
|
|
738
|
+
this.latestContextDiagnostics = undefined;
|
|
739
|
+
this.adaptiveCompactionPolicy = undefined;
|
|
740
|
+
this.previousParentRequestTokens = undefined;
|
|
741
|
+
this.largestObservedInterRequestGrowth = 0;
|
|
742
|
+
this.childUsage.clear();
|
|
743
|
+
this.retryingAgentIds.clear();
|
|
744
|
+
this.retryingParent = false;
|
|
745
|
+
this.compactionBaselinePromise = undefined;
|
|
746
|
+
this.proactiveCompactionPromise = undefined;
|
|
545
747
|
this.turnSettled = false;
|
|
546
748
|
this._run(prompt).catch((err) => {
|
|
547
749
|
this.fail(err instanceof Error ? err : new Error(String(err)), 'session.run');
|
|
@@ -558,7 +760,7 @@ class SdkSession {
|
|
|
558
760
|
const sdkModuleName = '@github/copilot-sdk';
|
|
559
761
|
// eslint-disable-next-line @typescript-eslint/no-implied-eval
|
|
560
762
|
const dynamicImport = new Function('specifier', 'return import(specifier)');
|
|
561
|
-
const { CopilotClient, RuntimeConnection, approveAll } = await dynamicImport(sdkModuleName);
|
|
763
|
+
const { CopilotClient, CopilotRequestHandler, CopilotWebSocketForwarder, RuntimeConnection, approveAll, } = await dynamicImport(sdkModuleName);
|
|
562
764
|
// ---------------------------------------------------------------
|
|
563
765
|
// Build hardened environment (strip NODE_OPTIONS, extend PATH)
|
|
564
766
|
// ---------------------------------------------------------------
|
|
@@ -593,6 +795,55 @@ class SdkSession {
|
|
|
593
795
|
connection: RuntimeConnection.forStdio(),
|
|
594
796
|
env,
|
|
595
797
|
logLevel: 'warning',
|
|
798
|
+
requestHandler: createObservedRequestHandler(CopilotRequestHandler, CopilotWebSocketForwarder, (telemetry) => this.logModelRequest(telemetry), (agentId) => agentId ? this.childUsage.get(agentId) : this.parentUsage, (purpose) => {
|
|
799
|
+
const sdkSession = this._sdkSession;
|
|
800
|
+
return sdkSession && (purpose === 'compaction'
|
|
801
|
+
|| this.config.compactionMode === 'adaptive'
|
|
802
|
+
|| this.shouldCaptureRequestDiagnostics(purpose))
|
|
803
|
+
? this.refreshParentUsageBeforeRequest(sdkSession, purpose)
|
|
804
|
+
: Promise.resolve(this.parentUsage === undefined
|
|
805
|
+
? undefined
|
|
806
|
+
: { usage: this.parentUsage, observedInterRequestGrowth: 0 });
|
|
807
|
+
}, (agentId, consume) => {
|
|
808
|
+
if (agentId) {
|
|
809
|
+
const retry = this.retryingAgentIds.has(agentId);
|
|
810
|
+
if (retry && consume)
|
|
811
|
+
this.retryingAgentIds.delete(agentId);
|
|
812
|
+
return retry;
|
|
813
|
+
}
|
|
814
|
+
const retry = this.retryingParent;
|
|
815
|
+
if (retry && consume)
|
|
816
|
+
this.retryingParent = false;
|
|
817
|
+
return retry;
|
|
818
|
+
},
|
|
819
|
+
// Capability discovery also uses this handler before createSession has
|
|
820
|
+
// returned a session handle. Scope the allowance to traffic that has no
|
|
821
|
+
// session provenance so a delayed createSession cannot open a parent turn.
|
|
822
|
+
(context, purpose, child, measurement) => {
|
|
823
|
+
if (this.aborted || this.turnSettled)
|
|
824
|
+
return false;
|
|
825
|
+
if (this._sdkSession === null) {
|
|
826
|
+
return context.sessionId === undefined && context.agentId === undefined;
|
|
827
|
+
}
|
|
828
|
+
if (child)
|
|
829
|
+
return true;
|
|
830
|
+
const usage = measurement?.usage;
|
|
831
|
+
if (purpose === 'compaction') {
|
|
832
|
+
return usage !== undefined && usage.currentTokens < usage.tokenLimit;
|
|
833
|
+
}
|
|
834
|
+
if (this.parentCompactionGeneration > this.verifiedParentCompactionGeneration
|
|
835
|
+
|| this.compactionInProgress)
|
|
836
|
+
return false;
|
|
837
|
+
if (measurement === undefined)
|
|
838
|
+
return false;
|
|
839
|
+
const measuredUsage = measurement.usage;
|
|
840
|
+
if (measuredUsage.currentTokens + measurement.observedInterRequestGrowth
|
|
841
|
+
>= measuredUsage.tokenLimit)
|
|
842
|
+
return false;
|
|
843
|
+
if (this.config.compactionMode !== 'adaptive')
|
|
844
|
+
return true;
|
|
845
|
+
return this.canDispatchParentRequest(measuredUsage);
|
|
846
|
+
}),
|
|
596
847
|
});
|
|
597
848
|
this._client = client;
|
|
598
849
|
// ---------------------------------------------------------------
|
|
@@ -617,14 +868,66 @@ class SdkSession {
|
|
|
617
868
|
else {
|
|
618
869
|
roster = this.backendRoster;
|
|
619
870
|
}
|
|
620
|
-
const {
|
|
871
|
+
const { reportedPromptTokenLimit, limitSource, } = await this.resolveModelCapabilities(client, this.config.model);
|
|
872
|
+
this.reportedPromptTokenLimit = reportedPromptTokenLimit;
|
|
873
|
+
this.reportedPromptTokenLimitSource = limitSource;
|
|
874
|
+
const subagentsEnabled = this.config.subagentsEnabled ?? this.backendSubagentsEnabled;
|
|
875
|
+
const tieredCustomAgents = subagentsEnabled
|
|
876
|
+
? (0, subagents_1.resolveTieredCustomAgents)(this.config.model, this.complementaryModelPolicy)
|
|
877
|
+
: undefined;
|
|
878
|
+
if (tieredCustomAgents !== undefined) {
|
|
879
|
+
const resolution = tieredCustomAgents.reviewResolution;
|
|
880
|
+
const fields = {
|
|
881
|
+
subagent: 'review',
|
|
882
|
+
sessionModel: resolution.sourceModel,
|
|
883
|
+
resolvedModel: resolution.resolvedModel,
|
|
884
|
+
usedCliFallback: resolution.usedCliFallback,
|
|
885
|
+
...(resolution.source !== undefined
|
|
886
|
+
? { sourceLab: resolution.source.lab, sourceTier: resolution.source.tier }
|
|
887
|
+
: {}),
|
|
888
|
+
...(resolution.complement !== undefined
|
|
889
|
+
? { resolvedLab: resolution.complement.lab, resolvedTier: resolution.complement.tier }
|
|
890
|
+
: {}),
|
|
891
|
+
};
|
|
892
|
+
if (resolution.usedCliFallback) {
|
|
893
|
+
this.logger.warn('Falling back to CLI complementary custom-agent model resolution', fields);
|
|
894
|
+
}
|
|
895
|
+
else {
|
|
896
|
+
this.logger.info('Resolved complementary custom-agent model', fields);
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
const customAgents = new Map();
|
|
900
|
+
for (const agent of tieredCustomAgents?.agents ?? [])
|
|
901
|
+
customAgents.set(agent.name, agent);
|
|
902
|
+
for (const agent of this.config.customAgents ?? []) {
|
|
903
|
+
if (!subagentsEnabled || customAgents.has(agent.name))
|
|
904
|
+
customAgents.set(agent.name, agent);
|
|
905
|
+
}
|
|
906
|
+
const excludedBuiltinAgents = subagentsEnabled
|
|
907
|
+
? [...new Set(['explore', 'research', ...(this.config.excludedBuiltinAgents ?? [])])]
|
|
908
|
+
: this.config.excludedBuiltinAgents !== undefined
|
|
909
|
+
? [...this.config.excludedBuiltinAgents]
|
|
910
|
+
: undefined;
|
|
911
|
+
const permissionHandler = subagentsEnabled
|
|
912
|
+
? approveAll
|
|
913
|
+
: (request, invocation) => {
|
|
914
|
+
const toolName = request.kind === 'custom-tool'
|
|
915
|
+
|| request.kind === 'mcp'
|
|
916
|
+
|| request.kind === 'hook'
|
|
917
|
+
? request.toolName
|
|
918
|
+
: undefined;
|
|
919
|
+
if (toolName === 'task') {
|
|
920
|
+
return {
|
|
921
|
+
kind: 'reject',
|
|
922
|
+
feedback: 'Model-issued sub-agent dispatch is disabled for this session.',
|
|
923
|
+
};
|
|
924
|
+
}
|
|
925
|
+
return approveAll(request, invocation);
|
|
926
|
+
};
|
|
621
927
|
const sessionConfig = {
|
|
622
928
|
model: this.config.model,
|
|
623
929
|
streaming: true,
|
|
624
|
-
|
|
625
|
-
// settings surface cannot carry a capability override to child sessions.
|
|
626
|
-
...(modelCapabilities !== undefined ? { modelCapabilities } : {}),
|
|
627
|
-
onPermissionRequest: approveAll,
|
|
930
|
+
onPermissionRequest: permissionHandler,
|
|
628
931
|
workingDirectory: this.config.cwd,
|
|
629
932
|
reasoningEffort: this.config.thinkingBudget,
|
|
630
933
|
...(this.config.contextTier !== undefined
|
|
@@ -639,11 +942,14 @@ class SdkSession {
|
|
|
639
942
|
...(this.config.availableTools !== undefined
|
|
640
943
|
? { availableTools: [...this.config.availableTools] }
|
|
641
944
|
: {}),
|
|
642
|
-
...(
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
? {
|
|
945
|
+
...(() => {
|
|
946
|
+
const excludedTools = new Set(this.config.excludedTools ?? []);
|
|
947
|
+
if (!subagentsEnabled)
|
|
948
|
+
excludedTools.add('task');
|
|
949
|
+
return excludedTools.size > 0 ? { excludedTools: [...excludedTools] } : {};
|
|
950
|
+
})(),
|
|
951
|
+
...(customAgents.size > 0
|
|
952
|
+
? { customAgents: [...customAgents.values()].map(toSdkCustomAgent) }
|
|
647
953
|
: {}),
|
|
648
954
|
...(this.config.defaultAgent !== undefined
|
|
649
955
|
? {
|
|
@@ -654,22 +960,27 @@ class SdkSession {
|
|
|
654
960
|
},
|
|
655
961
|
}
|
|
656
962
|
: {}),
|
|
657
|
-
...(
|
|
658
|
-
? { excludedBuiltinAgents
|
|
963
|
+
...(excludedBuiltinAgents !== undefined
|
|
964
|
+
? { excludedBuiltinAgents }
|
|
659
965
|
: {}),
|
|
660
966
|
...(mcpServers !== undefined ? { mcpServers } : {}),
|
|
661
967
|
// The SDK defaults this on, so every commit an agent composes carries a
|
|
662
968
|
// Co-authored-by trailer. Output should read as the repository owner's work.
|
|
663
969
|
coauthorEnabled: false,
|
|
664
|
-
//
|
|
665
|
-
//
|
|
666
|
-
//
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
970
|
+
// Stock documented settings are the default and do not override model
|
|
971
|
+
// capabilities. The reduced legacy thresholds remain available for
|
|
972
|
+
// comparative diagnostics; the measured adaptive controller is opt-in.
|
|
973
|
+
infiniteSessions: this.config.compactionMode === 'aggressive'
|
|
974
|
+
? {
|
|
975
|
+
enabled: true,
|
|
976
|
+
backgroundCompactionThreshold: 0.60,
|
|
977
|
+
bufferExhaustionThreshold: 0.75,
|
|
978
|
+
}
|
|
979
|
+
: {
|
|
980
|
+
enabled: true,
|
|
981
|
+
backgroundCompactionThreshold: 0.80,
|
|
982
|
+
bufferExhaustionThreshold: 0.95,
|
|
983
|
+
},
|
|
673
984
|
// Registered before createSession issues its RPC, closing the early-event
|
|
674
985
|
// gap for session.start and *_loaded events.
|
|
675
986
|
onEvent: (event) => this.handleEarlyEvent(normalizeSdkEvent(event)),
|
|
@@ -683,16 +994,50 @@ class SdkSession {
|
|
|
683
994
|
return;
|
|
684
995
|
}
|
|
685
996
|
this._sdkSession = sdkSession;
|
|
997
|
+
const resolvedRoster = roster === undefined || roster === false
|
|
998
|
+
? roster
|
|
999
|
+
: (0, subagents_1.resolveSubagentRosterModels)(roster, this.config.model, this.complementaryModelPolicy);
|
|
1000
|
+
if (resolvedRoster !== undefined && resolvedRoster !== false) {
|
|
1001
|
+
for (const resolution of resolvedRoster.resolutions) {
|
|
1002
|
+
const fields = {
|
|
1003
|
+
subagent: resolution.subagent,
|
|
1004
|
+
sessionModel: resolution.sourceModel,
|
|
1005
|
+
resolvedModel: resolution.resolvedModel,
|
|
1006
|
+
usedCliFallback: resolution.usedCliFallback,
|
|
1007
|
+
...(resolution.source !== undefined
|
|
1008
|
+
? { sourceLab: resolution.source.lab, sourceTier: resolution.source.tier }
|
|
1009
|
+
: {}),
|
|
1010
|
+
...(resolution.complement !== undefined
|
|
1011
|
+
? { resolvedLab: resolution.complement.lab, resolvedTier: resolution.complement.tier }
|
|
1012
|
+
: {}),
|
|
1013
|
+
};
|
|
1014
|
+
if (resolution.usedCliFallback) {
|
|
1015
|
+
this.logger.warn('Falling back to CLI complementary sub-agent model resolution', fields);
|
|
1016
|
+
}
|
|
1017
|
+
else {
|
|
1018
|
+
this.logger.info('Resolved complementary sub-agent model', fields);
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
1022
|
+
const maxDepth = this.config.maxDepth ?? this.backendMaxDepth;
|
|
1023
|
+
const maxConcurrency = this.config.maxConcurrency ?? this.backendMaxConcurrency;
|
|
1024
|
+
const subagentSettings = {
|
|
1025
|
+
...(resolvedRoster !== undefined && resolvedRoster !== false
|
|
1026
|
+
? { agents: resolvedRoster.roster }
|
|
1027
|
+
: {}),
|
|
1028
|
+
...(maxDepth !== undefined ? { maxDepth } : {}),
|
|
1029
|
+
...(maxConcurrency !== undefined ? { maxConcurrency } : {}),
|
|
1030
|
+
};
|
|
686
1031
|
// Apply the live override before any prompt can dispatch a subagent.
|
|
687
1032
|
// This experimental RPC degrades safely if the installed CLI rejects it.
|
|
688
|
-
if (
|
|
1033
|
+
if (Object.keys(subagentSettings).length > 0) {
|
|
689
1034
|
try {
|
|
690
1035
|
await sdkSession.rpc.tools.updateSubagentSettings({
|
|
691
|
-
subagents:
|
|
1036
|
+
subagents: subagentSettings,
|
|
692
1037
|
});
|
|
693
1038
|
}
|
|
694
1039
|
catch (err) {
|
|
695
|
-
this.logger.warn('Failed to apply Copilot subagent
|
|
1040
|
+
this.logger.warn('Failed to apply Copilot subagent settings; using default settings', {
|
|
696
1041
|
error: err instanceof Error ? err.message : String(err),
|
|
697
1042
|
});
|
|
698
1043
|
}
|
|
@@ -722,6 +1067,9 @@ class SdkSession {
|
|
|
722
1067
|
// ---------------------------------------------------------------
|
|
723
1068
|
// Send the prompt (fire-and-forget; events stream via handlers)
|
|
724
1069
|
// ---------------------------------------------------------------
|
|
1070
|
+
if (this.config.compactionMode === 'adaptive'
|
|
1071
|
+
&& !(await this.ensureCompactionHeadroom(sdkSession, 'pre-send')))
|
|
1072
|
+
return;
|
|
725
1073
|
await sdkSession.send({ prompt });
|
|
726
1074
|
}
|
|
727
1075
|
/**
|
|
@@ -874,7 +1222,7 @@ class SdkSession {
|
|
|
874
1222
|
});
|
|
875
1223
|
// --- Session idle (agent finished all work) ---
|
|
876
1224
|
sdkSession.on('session.idle', (e) => {
|
|
877
|
-
if (!isActive()
|
|
1225
|
+
if (!isActive())
|
|
878
1226
|
return;
|
|
879
1227
|
this.resetHeartbeat();
|
|
880
1228
|
if (!e.agentId)
|
|
@@ -882,7 +1230,7 @@ class SdkSession {
|
|
|
882
1230
|
});
|
|
883
1231
|
// --- task_complete → idle (some models fire this instead of session.idle) ---
|
|
884
1232
|
sdkSession.on('session.task_complete', (e) => {
|
|
885
|
-
if (!isActive()
|
|
1233
|
+
if (!isActive())
|
|
886
1234
|
return;
|
|
887
1235
|
this.resetHeartbeat();
|
|
888
1236
|
if (e.agentId)
|
|
@@ -904,13 +1252,11 @@ class SdkSession {
|
|
|
904
1252
|
// The CLI normally follows an abort event with session.idle. If that
|
|
905
1253
|
// terminal event is lost, fail quickly rather than waiting for heartbeat.
|
|
906
1254
|
sdkSession.on('abort', (e) => {
|
|
907
|
-
if (!isActive()
|
|
1255
|
+
if (!isActive())
|
|
908
1256
|
return;
|
|
909
1257
|
this.resetHeartbeat();
|
|
910
1258
|
if (this.logAgentScopedFailure(e))
|
|
911
1259
|
return;
|
|
912
|
-
if (this.intentionalAborts.delete(sdkSession))
|
|
913
|
-
return;
|
|
914
1260
|
if (this.abortGraceTimer)
|
|
915
1261
|
clearTimeout(this.abortGraceTimer);
|
|
916
1262
|
this.abortGraceTimer = setTimeout(() => {
|
|
@@ -963,7 +1309,34 @@ class SdkSession {
|
|
|
963
1309
|
Object.assign(error, { statusCode });
|
|
964
1310
|
if (errorCode !== undefined)
|
|
965
1311
|
Object.assign(error, { errorCode });
|
|
966
|
-
|
|
1312
|
+
const compactionFailed = data?.initiator === 'compaction'
|
|
1313
|
+
|| data?.source === 'compaction'
|
|
1314
|
+
|| this.compactionInProgress
|
|
1315
|
+
|| this.proactiveCompactionPromise !== undefined;
|
|
1316
|
+
this.fail(error, compactionFailed ? 'compaction.model.call_failure' : 'model.call_failure', {
|
|
1317
|
+
...data,
|
|
1318
|
+
...(compactionFailed ? { requestPurpose: 'compaction' } : {}),
|
|
1319
|
+
...(this.parentUsage !== undefined
|
|
1320
|
+
? {
|
|
1321
|
+
currentTokens: this.parentUsage.currentTokens,
|
|
1322
|
+
tokenLimit: this.parentUsage.tokenLimit,
|
|
1323
|
+
messagesLength: this.parentUsage.messagesLength,
|
|
1324
|
+
}
|
|
1325
|
+
: {}),
|
|
1326
|
+
});
|
|
1327
|
+
});
|
|
1328
|
+
sdkSession.on('session.usage_info', (e) => {
|
|
1329
|
+
if (!isActive())
|
|
1330
|
+
return;
|
|
1331
|
+
this.handleContextUsage(sdkSession, e);
|
|
1332
|
+
});
|
|
1333
|
+
sdkSession.on('assistant.turn_retry', (e) => {
|
|
1334
|
+
if (!isActive())
|
|
1335
|
+
return;
|
|
1336
|
+
if (e.agentId)
|
|
1337
|
+
this.retryingAgentIds.add(e.agentId);
|
|
1338
|
+
else
|
|
1339
|
+
this.retryingParent = true;
|
|
967
1340
|
});
|
|
968
1341
|
// --- Context compaction (infinite sessions) ---
|
|
969
1342
|
// During compaction the model goes silent. SUSPEND the heartbeat entirely
|
|
@@ -971,10 +1344,12 @@ class SdkSession {
|
|
|
971
1344
|
sdkSession.on('session.compaction_start', (e) => {
|
|
972
1345
|
if (!isActive())
|
|
973
1346
|
return;
|
|
974
|
-
this.resetHeartbeat();
|
|
975
1347
|
if (e.agentId)
|
|
976
1348
|
return;
|
|
1349
|
+
this.resetHeartbeat();
|
|
977
1350
|
this.compactionInProgress = true;
|
|
1351
|
+
this.parentCompactionGeneration += 1;
|
|
1352
|
+
this.compactionBaselinePromise = this.captureContextDiagnostics(sdkSession, 'pre-compaction');
|
|
978
1353
|
// SUSPEND heartbeat — compaction silence is expected
|
|
979
1354
|
if (this.heartbeatTimer) {
|
|
980
1355
|
clearTimeout(this.heartbeatTimer);
|
|
@@ -1005,82 +1380,71 @@ class SdkSession {
|
|
|
1005
1380
|
return;
|
|
1006
1381
|
// If force-compact works, compaction_complete will fire naturally
|
|
1007
1382
|
}
|
|
1008
|
-
catch {
|
|
1009
|
-
// Re-check guards after await
|
|
1383
|
+
catch (err) {
|
|
1010
1384
|
if (!isActive())
|
|
1011
1385
|
return;
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
clearTimeout(this.abortGraceTimer);
|
|
1030
|
-
this.abortGraceTimer = null;
|
|
1031
|
-
}
|
|
1032
|
-
// WeakSet entries are removed when the delayed intentional abort event
|
|
1033
|
-
// arrives. The handle itself becomes collectible after session cleanup.
|
|
1034
|
-
// Wait briefly for the abort to settle before re-sending.
|
|
1035
|
-
// The SDK resolves abort() on acknowledgement, not on idle.
|
|
1036
|
-
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
1037
|
-
if (!isActive())
|
|
1038
|
-
return;
|
|
1039
|
-
await session.send({ prompt: 'Continue from where you left off.' });
|
|
1040
|
-
this.compactionRecoveryInProgress = false;
|
|
1041
|
-
this.resetHeartbeat();
|
|
1042
|
-
}
|
|
1043
|
-
catch {
|
|
1044
|
-
this.compactionRecoveryInProgress = false;
|
|
1045
|
-
try {
|
|
1046
|
-
process.stderr.write('[SdkBackend] Recovery failed — restarting heartbeat as safety net\n');
|
|
1047
|
-
}
|
|
1048
|
-
catch { /* */ }
|
|
1049
|
-
this.resetHeartbeat(); // Detect if model is truly dead instead of waiting for hours-long hard timeout
|
|
1050
|
-
}
|
|
1386
|
+
const usage = this.parentUsage;
|
|
1387
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
1388
|
+
const fields = {
|
|
1389
|
+
requestPurpose: 'compaction',
|
|
1390
|
+
error: detail,
|
|
1391
|
+
...(usage !== undefined
|
|
1392
|
+
? {
|
|
1393
|
+
currentTokens: usage.currentTokens,
|
|
1394
|
+
tokenLimit: usage.tokenLimit,
|
|
1395
|
+
messagesLength: usage.messagesLength,
|
|
1396
|
+
systemTokens: usage.systemTokens,
|
|
1397
|
+
conversationTokens: usage.conversationTokens,
|
|
1398
|
+
toolDefinitionsTokens: usage.toolDefinitionsTokens,
|
|
1399
|
+
}
|
|
1400
|
+
: {}),
|
|
1401
|
+
};
|
|
1402
|
+
this.fail(new Error(`Compaction recovery request failed: ${detail}`), 'compaction.recovery_failure', fields);
|
|
1051
1403
|
}
|
|
1052
1404
|
}, 3 * 60 * 1000);
|
|
1053
1405
|
});
|
|
1054
1406
|
sdkSession.on('session.compaction_complete', (e) => {
|
|
1055
1407
|
if (!isActive())
|
|
1056
1408
|
return;
|
|
1057
|
-
if (e.agentId)
|
|
1058
|
-
this.resetHeartbeat();
|
|
1409
|
+
if (e.agentId)
|
|
1059
1410
|
return;
|
|
1060
|
-
}
|
|
1061
1411
|
this.compactionInProgress = false;
|
|
1062
1412
|
if (this.compactionTimer) {
|
|
1063
1413
|
clearTimeout(this.compactionTimer);
|
|
1064
1414
|
this.compactionTimer = null;
|
|
1065
1415
|
}
|
|
1066
|
-
// Restart heartbeat
|
|
1416
|
+
// Restart heartbeat while the asynchronous verification RPC runs. Any
|
|
1417
|
+
// ordinary parent request remains guarded at the request-handler seam.
|
|
1067
1418
|
this.resetHeartbeat();
|
|
1068
1419
|
const data = e.data;
|
|
1069
|
-
// Check if compaction actually succeeded
|
|
1070
1420
|
if (data?.success === false) {
|
|
1071
1421
|
const errMsg = typeof data.error === 'string' ? data.error : 'unknown reason';
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1422
|
+
const rawStatusCode = data.statusCode;
|
|
1423
|
+
const statusCode = typeof rawStatusCode === 'number'
|
|
1424
|
+
? ` (HTTP ${rawStatusCode})`
|
|
1425
|
+
: '';
|
|
1426
|
+
const usage = this.parentUsage;
|
|
1427
|
+
const fields = {
|
|
1428
|
+
...data,
|
|
1429
|
+
requestPurpose: 'compaction',
|
|
1430
|
+
...(usage !== undefined
|
|
1431
|
+
? {
|
|
1432
|
+
currentTokens: usage.currentTokens,
|
|
1433
|
+
tokenLimit: usage.tokenLimit,
|
|
1434
|
+
messagesLength: usage.messagesLength,
|
|
1435
|
+
systemTokens: usage.systemTokens,
|
|
1436
|
+
conversationTokens: usage.conversationTokens,
|
|
1437
|
+
toolDefinitionsTokens: usage.toolDefinitionsTokens,
|
|
1438
|
+
}
|
|
1439
|
+
: {}),
|
|
1440
|
+
};
|
|
1441
|
+
this.fail(new Error(`Compaction model call failed${statusCode}: ${errMsg}`), 'compaction.call_failure', fields);
|
|
1442
|
+
return;
|
|
1076
1443
|
}
|
|
1077
|
-
const
|
|
1078
|
-
const
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
? `${pre} → ${post} tokens${removed != null ? ` (saved ${removed})` : ''}`
|
|
1082
|
-
: '';
|
|
1083
|
-
this.emit('compaction', 'complete', summary);
|
|
1444
|
+
const generation = this.parentCompactionGeneration;
|
|
1445
|
+
const baselinePromise = this.compactionBaselinePromise;
|
|
1446
|
+
this.compactionBaselinePromise = undefined;
|
|
1447
|
+
void this.verifyCompletedCompaction(sdkSession, data, generation, baselinePromise);
|
|
1084
1448
|
});
|
|
1085
1449
|
// --- Subagent lifecycle ---
|
|
1086
1450
|
// Sub-agents use their own event path (subagent_start/subagent_end).
|
|
@@ -1101,7 +1465,18 @@ class SdkSession {
|
|
|
1101
1465
|
this.resetHeartbeat();
|
|
1102
1466
|
const data = e.data;
|
|
1103
1467
|
const name = String(data?.agentDisplayName ?? data?.agentName ?? 'agent');
|
|
1468
|
+
const agentName = String(data?.agentName ?? name);
|
|
1469
|
+
const model = typeof data?.model === 'string' ? data.model : undefined;
|
|
1470
|
+
const totalTokens = nonNegativeFiniteNumber(data?.totalTokens);
|
|
1104
1471
|
const toolCallId = typeof data?.toolCallId === 'string' ? data.toolCallId : '';
|
|
1472
|
+
const fields = {
|
|
1473
|
+
agentName,
|
|
1474
|
+
model,
|
|
1475
|
+
cumulativeTokensConsumed: totalTokens,
|
|
1476
|
+
totalToolCalls: nonNegativeFiniteNumber(data?.totalToolCalls),
|
|
1477
|
+
durationMs: nonNegativeFiniteNumber(data?.durationMs),
|
|
1478
|
+
};
|
|
1479
|
+
this.logger.info('Copilot sub-agent cumulative token consumption', fields);
|
|
1105
1480
|
this.emit('subagent_end', name, { ...data, toolCallId });
|
|
1106
1481
|
});
|
|
1107
1482
|
sdkSession.on('subagent.failed', (e) => {
|
|
@@ -1265,6 +1640,10 @@ class SdkSession {
|
|
|
1265
1640
|
// Only the pre-handle creation window is handled here.
|
|
1266
1641
|
if (this._sdkSession)
|
|
1267
1642
|
return;
|
|
1643
|
+
if (e.type === 'session.usage_info') {
|
|
1644
|
+
this.handleContextUsage(undefined, e);
|
|
1645
|
+
return;
|
|
1646
|
+
}
|
|
1268
1647
|
const eventClass = (0, lifecycle_events_1.classifySdkEvent)(e.type);
|
|
1269
1648
|
if (eventClass === 'informational')
|
|
1270
1649
|
return;
|
|
@@ -1350,6 +1729,409 @@ class SdkSession {
|
|
|
1350
1729
|
|| typeof data.error === 'string'
|
|
1351
1730
|
|| (data.error != null && typeof data.error === 'object');
|
|
1352
1731
|
}
|
|
1732
|
+
canDispatchParentRequest(usage) {
|
|
1733
|
+
const policy = this.adaptiveCompactionPolicy;
|
|
1734
|
+
if (usage === undefined || policy === undefined) {
|
|
1735
|
+
if (usage !== undefined) {
|
|
1736
|
+
this.failUnsafeContext('pre-send', usage, policy, 0, 'fresh parent context accounting was unavailable at the request-handler seam');
|
|
1737
|
+
}
|
|
1738
|
+
else {
|
|
1739
|
+
this.fail(new Error('Blocked parent model request: fresh context accounting was unavailable'), 'pre-send-context-accounting');
|
|
1740
|
+
}
|
|
1741
|
+
return false;
|
|
1742
|
+
}
|
|
1743
|
+
if (usage.currentTokens < policy.threshold)
|
|
1744
|
+
return true;
|
|
1745
|
+
this.failUnsafeContext('pre-send', usage, policy, 0, 'the runtime attempted a parent model request before the required adaptive headroom was restored');
|
|
1746
|
+
return false;
|
|
1747
|
+
}
|
|
1748
|
+
async captureContextDiagnostics(sdkSession, reason) {
|
|
1749
|
+
try {
|
|
1750
|
+
const [attributionResult, heaviestMessages, recomputed, usageMetrics] = await Promise.all([
|
|
1751
|
+
sdkSession.rpc.metadata.getContextAttribution(),
|
|
1752
|
+
sdkSession.rpc.metadata.getContextHeaviestMessages({}),
|
|
1753
|
+
sdkSession.rpc.metadata.recomputeContextTokens({ modelId: this.config.model }),
|
|
1754
|
+
sdkSession.rpc.usage.getMetrics(),
|
|
1755
|
+
]);
|
|
1756
|
+
const attribution = attributionResult.contextAttribution ?? null;
|
|
1757
|
+
const fixedPromptOverhead = attribution
|
|
1758
|
+
? Math.max(0, attribution.totalTokens - recomputed.messagesTokenCount)
|
|
1759
|
+
: recomputed.systemTokenCount;
|
|
1760
|
+
const diagnostics = {
|
|
1761
|
+
attribution,
|
|
1762
|
+
heaviestMessages,
|
|
1763
|
+
recomputed,
|
|
1764
|
+
usageMetrics,
|
|
1765
|
+
fixedPromptOverhead,
|
|
1766
|
+
};
|
|
1767
|
+
this.logger.info('Captured Copilot parent context diagnostics', {
|
|
1768
|
+
reason,
|
|
1769
|
+
successfulCompactions: attribution?.compactions.count,
|
|
1770
|
+
fixedPromptOverhead,
|
|
1771
|
+
contextAttribution: attribution,
|
|
1772
|
+
heaviestMessages,
|
|
1773
|
+
recomputedContextTokens: recomputed,
|
|
1774
|
+
usageMetrics,
|
|
1775
|
+
});
|
|
1776
|
+
return diagnostics;
|
|
1777
|
+
}
|
|
1778
|
+
catch (err) {
|
|
1779
|
+
this.logger.warn('Failed to capture Copilot parent context diagnostics', {
|
|
1780
|
+
reason,
|
|
1781
|
+
error: err instanceof Error ? err.message : String(err),
|
|
1782
|
+
});
|
|
1783
|
+
return undefined;
|
|
1784
|
+
}
|
|
1785
|
+
}
|
|
1786
|
+
async measureParentUsage(sdkSession, reason) {
|
|
1787
|
+
const previous = this.parentUsage;
|
|
1788
|
+
try {
|
|
1789
|
+
const [contextResult, diagnostics] = await Promise.all([
|
|
1790
|
+
sdkSession.rpc.metadata.contextInfo({
|
|
1791
|
+
promptTokenLimit: previous?.tokenLimit ?? this.reportedPromptTokenLimit ?? 0,
|
|
1792
|
+
outputTokenLimit: 0,
|
|
1793
|
+
selectedModel: this.config.model,
|
|
1794
|
+
}),
|
|
1795
|
+
this.captureContextDiagnostics(sdkSession, reason),
|
|
1796
|
+
]);
|
|
1797
|
+
const contextInfo = contextResult.contextInfo;
|
|
1798
|
+
if (!contextInfo || !diagnostics) {
|
|
1799
|
+
this.logger.warn('Copilot parent context measurement returned incomplete context information', {
|
|
1800
|
+
reason,
|
|
1801
|
+
previousCurrentTokens: previous?.currentTokens,
|
|
1802
|
+
hasContextInfo: contextInfo !== null && contextInfo !== undefined,
|
|
1803
|
+
hasDiagnostics: diagnostics !== undefined,
|
|
1804
|
+
});
|
|
1805
|
+
return undefined;
|
|
1806
|
+
}
|
|
1807
|
+
const completeDiagnostics = diagnostics;
|
|
1808
|
+
this.latestContextDiagnostics = completeDiagnostics;
|
|
1809
|
+
const usage = {
|
|
1810
|
+
currentTokens: diagnostics.attribution?.totalTokens ?? diagnostics.recomputed.totalTokens,
|
|
1811
|
+
tokenLimit: contextInfo.promptTokenLimit,
|
|
1812
|
+
messagesLength: previous?.messagesLength ?? 0,
|
|
1813
|
+
systemTokens: diagnostics.recomputed.systemTokenCount,
|
|
1814
|
+
conversationTokens: diagnostics.recomputed.messagesTokenCount,
|
|
1815
|
+
toolDefinitionsTokens: contextInfo.toolDefinitionsTokens,
|
|
1816
|
+
};
|
|
1817
|
+
this.parentUsage = usage;
|
|
1818
|
+
this.updateAdaptiveCompactionPolicy(usage, completeDiagnostics);
|
|
1819
|
+
this.logger.info('Measured Copilot parent context', {
|
|
1820
|
+
reason,
|
|
1821
|
+
...usage,
|
|
1822
|
+
successfulCompactions: diagnostics.attribution?.compactions.count,
|
|
1823
|
+
fixedPromptOverhead: diagnostics.fixedPromptOverhead,
|
|
1824
|
+
contextAttribution: diagnostics.attribution,
|
|
1825
|
+
heaviestMessages: diagnostics.heaviestMessages,
|
|
1826
|
+
recomputedContextTokens: diagnostics.recomputed,
|
|
1827
|
+
usageMetrics: diagnostics.usageMetrics,
|
|
1828
|
+
...this.adaptivePolicyFields(),
|
|
1829
|
+
});
|
|
1830
|
+
return usage;
|
|
1831
|
+
}
|
|
1832
|
+
catch (err) {
|
|
1833
|
+
this.logger.warn('Failed to measure Copilot parent context', {
|
|
1834
|
+
reason,
|
|
1835
|
+
error: err instanceof Error ? err.message : String(err),
|
|
1836
|
+
previousCurrentTokens: previous?.currentTokens,
|
|
1837
|
+
});
|
|
1838
|
+
return undefined;
|
|
1839
|
+
}
|
|
1840
|
+
}
|
|
1841
|
+
shouldCaptureRequestDiagnostics(purpose) {
|
|
1842
|
+
if (purpose === 'compaction')
|
|
1843
|
+
return false;
|
|
1844
|
+
const usage = this.parentUsage;
|
|
1845
|
+
if (!usage)
|
|
1846
|
+
return true;
|
|
1847
|
+
const contextInfoThreshold = this.config.compactionMode === 'aggressive'
|
|
1848
|
+
? usage.tokenLimit * 0.60
|
|
1849
|
+
: usage.tokenLimit * 0.80;
|
|
1850
|
+
return usage.currentTokens >= contextInfoThreshold;
|
|
1851
|
+
}
|
|
1852
|
+
async refreshParentUsageBeforeRequest(sdkSession, purpose) {
|
|
1853
|
+
const usage = await this.measureParentUsage(sdkSession, 'pre-request');
|
|
1854
|
+
if (!usage)
|
|
1855
|
+
return undefined;
|
|
1856
|
+
const previousRequestTokens = this.previousParentRequestTokens;
|
|
1857
|
+
const observedInterRequestGrowth = previousRequestTokens === undefined
|
|
1858
|
+
? 0
|
|
1859
|
+
: Math.max(0, usage.currentTokens - previousRequestTokens);
|
|
1860
|
+
if (observedInterRequestGrowth > this.largestObservedInterRequestGrowth) {
|
|
1861
|
+
this.largestObservedInterRequestGrowth = observedInterRequestGrowth;
|
|
1862
|
+
if (this.latestContextDiagnostics) {
|
|
1863
|
+
this.updateAdaptiveCompactionPolicy(usage);
|
|
1864
|
+
}
|
|
1865
|
+
}
|
|
1866
|
+
this.previousParentRequestTokens = usage.currentTokens;
|
|
1867
|
+
this.logger.info('Refreshed Copilot parent context before model request', {
|
|
1868
|
+
purpose,
|
|
1869
|
+
...usage,
|
|
1870
|
+
previousRequestTokens,
|
|
1871
|
+
...(previousRequestTokens === undefined ? {} : { observedInterRequestGrowth }),
|
|
1872
|
+
...this.adaptivePolicyFields(),
|
|
1873
|
+
});
|
|
1874
|
+
return { usage, observedInterRequestGrowth };
|
|
1875
|
+
}
|
|
1876
|
+
updateAdaptiveCompactionPolicy(usage, diagnostics = this.latestContextDiagnostics) {
|
|
1877
|
+
const reportedLimit = this.reportedPromptTokenLimit;
|
|
1878
|
+
const tokenLimit = reportedLimit === undefined
|
|
1879
|
+
? usage.tokenLimit
|
|
1880
|
+
: Math.min(reportedLimit, usage.tokenLimit);
|
|
1881
|
+
if (!isPositiveFiniteNumber(tokenLimit)) {
|
|
1882
|
+
this.adaptiveCompactionPolicy = undefined;
|
|
1883
|
+
return;
|
|
1884
|
+
}
|
|
1885
|
+
// Bootstrap has no tuned token seed. Prefer the SDK's source attribution so
|
|
1886
|
+
// fixed context costs are separated from conversation growth. Before that
|
|
1887
|
+
// diagnostic surface initializes, reserve the first exact recomputation.
|
|
1888
|
+
const fixedPromptOverhead = diagnostics?.fixedPromptOverhead ?? 0;
|
|
1889
|
+
const existingPolicy = this.adaptiveCompactionPolicy;
|
|
1890
|
+
const bootstrapSource = existingPolicy?.bootstrapSource
|
|
1891
|
+
?? (fixedPromptOverhead > 0 ? 'context-attribution' : 'first-recomputed-request');
|
|
1892
|
+
const initialBootstrapHeadroom = fixedPromptOverhead > 0
|
|
1893
|
+
? fixedPromptOverhead
|
|
1894
|
+
: diagnostics?.recomputed.totalTokens ?? usage.currentTokens;
|
|
1895
|
+
const bootstrapHeadroom = Math.max(existingPolicy?.bootstrapHeadroom ?? 0, initialBootstrapHeadroom);
|
|
1896
|
+
const headroom = bootstrapHeadroom + this.largestObservedInterRequestGrowth;
|
|
1897
|
+
this.adaptiveCompactionPolicy = {
|
|
1898
|
+
tokenLimit,
|
|
1899
|
+
threshold: Math.max(0, tokenLimit - headroom),
|
|
1900
|
+
headroom,
|
|
1901
|
+
bootstrapHeadroom,
|
|
1902
|
+
bootstrapSource,
|
|
1903
|
+
largestObservedInterRequestGrowth: this.largestObservedInterRequestGrowth,
|
|
1904
|
+
};
|
|
1905
|
+
}
|
|
1906
|
+
adaptivePolicyFields() {
|
|
1907
|
+
const policy = this.adaptiveCompactionPolicy;
|
|
1908
|
+
return {
|
|
1909
|
+
reportedPromptTokenLimit: this.reportedPromptTokenLimit,
|
|
1910
|
+
reportedPromptTokenLimitSource: this.reportedPromptTokenLimitSource,
|
|
1911
|
+
adaptiveCompactionThreshold: policy?.threshold,
|
|
1912
|
+
adaptiveCompactionHeadroom: policy?.headroom,
|
|
1913
|
+
adaptiveBootstrapHeadroom: policy?.bootstrapHeadroom,
|
|
1914
|
+
adaptiveBootstrapSource: policy?.bootstrapSource,
|
|
1915
|
+
largestObservedInterRequestGrowth: policy?.largestObservedInterRequestGrowth
|
|
1916
|
+
?? this.largestObservedInterRequestGrowth,
|
|
1917
|
+
};
|
|
1918
|
+
}
|
|
1919
|
+
logModelRequest(telemetry) {
|
|
1920
|
+
const fields = {
|
|
1921
|
+
...telemetry,
|
|
1922
|
+
...this.adaptivePolicyFields(),
|
|
1923
|
+
};
|
|
1924
|
+
this.logger.info('Copilot model request dispatch', fields);
|
|
1925
|
+
try {
|
|
1926
|
+
process.stderr.write(`[SdkBackend] MODEL REQUEST requestId=${telemetry.requestId} sessionId=${telemetry.sessionId ?? 'unknown'} agentId=${telemetry.agentId ?? 'parent'} parentAgentId=${telemetry.parentAgentId ?? 'none'} purpose=${telemetry.purpose} interactionType=${telemetry.interactionType ?? 'unknown'} currentTokens=${telemetry.currentTokens ?? 'unknown'} tokenLimit=${telemetry.tokenLimit ?? 'unknown'} messagesLength=${telemetry.messagesLength ?? 'unknown'} systemTokens=${telemetry.systemTokens ?? 'unknown'} conversationTokens=${telemetry.conversationTokens ?? 'unknown'} toolDefinitionsTokens=${telemetry.toolDefinitionsTokens ?? 'unknown'} adaptiveHeadroom=${this.adaptiveCompactionPolicy?.headroom ?? 'unknown'} largestInterRequestGrowth=${this.largestObservedInterRequestGrowth} observedRequestBodyBytes=${telemetry.observedRequestBodyBytes ?? 'unknown'} observedRequestBodyTokens=${telemetry.observedRequestBodyTokens ?? 'unavailable'} observedRequestBodySha256=${telemetry.observedRequestBodySha256 ?? 'unknown'}\n`);
|
|
1927
|
+
}
|
|
1928
|
+
catch { /* closed stream */ }
|
|
1929
|
+
}
|
|
1930
|
+
handleContextUsage(sdkSession, e) {
|
|
1931
|
+
const usage = parseContextUsage(e.data);
|
|
1932
|
+
if (!usage) {
|
|
1933
|
+
this.logger.warn('Copilot context usage event was missing valid accounting fields', {
|
|
1934
|
+
agentId: e.agentId,
|
|
1935
|
+
data: e.data,
|
|
1936
|
+
});
|
|
1937
|
+
return;
|
|
1938
|
+
}
|
|
1939
|
+
if (e.agentId) {
|
|
1940
|
+
this.childUsage.set(e.agentId, usage);
|
|
1941
|
+
}
|
|
1942
|
+
else {
|
|
1943
|
+
this.parentUsage = usage;
|
|
1944
|
+
if (this.latestContextDiagnostics) {
|
|
1945
|
+
this.updateAdaptiveCompactionPolicy(usage);
|
|
1946
|
+
}
|
|
1947
|
+
}
|
|
1948
|
+
const fields = {
|
|
1949
|
+
agentId: e.agentId,
|
|
1950
|
+
...usage,
|
|
1951
|
+
...(!e.agentId ? this.adaptivePolicyFields() : {}),
|
|
1952
|
+
};
|
|
1953
|
+
const scope = e.agentId ? `subagent:${e.agentId}` : 'parent';
|
|
1954
|
+
this.logger.info(e.agentId ? 'Copilot sub-agent context usage' : 'Copilot parent context usage', fields);
|
|
1955
|
+
try {
|
|
1956
|
+
process.stderr.write(`[SdkBackend] CONTEXT USAGE scope=${scope} currentTokens=${usage.currentTokens} tokenLimit=${usage.tokenLimit} messagesLength=${usage.messagesLength} systemTokens=${usage.systemTokens ?? 'unknown'} conversationTokens=${usage.conversationTokens ?? 'unknown'} toolDefinitionsTokens=${usage.toolDefinitionsTokens ?? 'unknown'} ceiling=${!e.agentId ? this.adaptiveCompactionPolicy?.threshold ?? 'unknown' : 'isolated'} headroom=${!e.agentId ? this.adaptiveCompactionPolicy?.headroom ?? 'unknown' : 'isolated'}\n`);
|
|
1957
|
+
}
|
|
1958
|
+
catch { /* closed stream */ }
|
|
1959
|
+
if (e.agentId)
|
|
1960
|
+
return;
|
|
1961
|
+
const policy = this.adaptiveCompactionPolicy;
|
|
1962
|
+
if (this.config.compactionMode === 'adaptive'
|
|
1963
|
+
&& sdkSession
|
|
1964
|
+
&& policy
|
|
1965
|
+
&& usage.currentTokens >= policy.threshold) {
|
|
1966
|
+
void this.ensureCompactionHeadroom(sdkSession, 'usage-threshold');
|
|
1967
|
+
}
|
|
1968
|
+
}
|
|
1969
|
+
ensureCompactionHeadroom(sdkSession, source) {
|
|
1970
|
+
if (this.proactiveCompactionPromise)
|
|
1971
|
+
return this.proactiveCompactionPromise;
|
|
1972
|
+
const policy = this.adaptiveCompactionPolicy;
|
|
1973
|
+
const usage = this.parentUsage;
|
|
1974
|
+
if (policy === undefined
|
|
1975
|
+
|| usage === undefined
|
|
1976
|
+
|| usage.currentTokens < policy.threshold) {
|
|
1977
|
+
return Promise.resolve(true);
|
|
1978
|
+
}
|
|
1979
|
+
const compaction = this.compactForHeadroom(sdkSession, source, usage, policy);
|
|
1980
|
+
this.proactiveCompactionPromise = compaction;
|
|
1981
|
+
void compaction.then(() => {
|
|
1982
|
+
if (this.proactiveCompactionPromise === compaction) {
|
|
1983
|
+
this.proactiveCompactionPromise = undefined;
|
|
1984
|
+
}
|
|
1985
|
+
});
|
|
1986
|
+
return compaction;
|
|
1987
|
+
}
|
|
1988
|
+
async compactForHeadroom(sdkSession, source, before, policy) {
|
|
1989
|
+
if (this.aborted || this.turnSettled || this._sdkSession !== sdkSession)
|
|
1990
|
+
return false;
|
|
1991
|
+
if (before.messagesLength < MIN_MESSAGES_FOR_COMPACTION) {
|
|
1992
|
+
return this.failUnsafeContext(source, before, policy, 0, `only ${before.messagesLength} messages are present; the CLI requires ${MIN_MESSAGES_FOR_COMPACTION}, so the latest oversized context addition cannot be compacted`);
|
|
1993
|
+
}
|
|
1994
|
+
this.logger.warn('Copilot parent context crossed the adaptive compaction ceiling', {
|
|
1995
|
+
source,
|
|
1996
|
+
...before,
|
|
1997
|
+
...this.adaptivePolicyFields(),
|
|
1998
|
+
});
|
|
1999
|
+
try {
|
|
2000
|
+
const beforeDiagnostics = await this.captureContextDiagnostics(sdkSession, 'pre-compaction');
|
|
2001
|
+
const result = await sdkSession.rpc.history.compact();
|
|
2002
|
+
if (this.aborted || this.turnSettled || this._sdkSession !== sdkSession)
|
|
2003
|
+
return false;
|
|
2004
|
+
const afterUsage = await this.measureParentUsage(sdkSession, 'post-compaction');
|
|
2005
|
+
const afterDiagnostics = this.latestContextDiagnostics;
|
|
2006
|
+
if (!result.success || !beforeDiagnostics || !afterUsage || !afterDiagnostics) {
|
|
2007
|
+
return this.failUnsafeContext(source, afterUsage ?? before, policy, result.tokensRemoved, result.success
|
|
2008
|
+
? 'the compaction result could not be verified with exact token recomputation and attribution counts'
|
|
2009
|
+
: 'the history compaction RPC reported failure');
|
|
2010
|
+
}
|
|
2011
|
+
const beforeCompactions = beforeDiagnostics.attribution?.compactions.count;
|
|
2012
|
+
const afterCompactions = afterDiagnostics.attribution?.compactions.count;
|
|
2013
|
+
const afterPolicy = this.adaptiveCompactionPolicy ?? policy;
|
|
2014
|
+
this.logger.info('Verified Copilot parent context compaction', {
|
|
2015
|
+
source,
|
|
2016
|
+
beforeTokens: beforeDiagnostics.recomputed.totalTokens,
|
|
2017
|
+
afterTokens: afterDiagnostics.recomputed.totalTokens,
|
|
2018
|
+
beforeSuccessfulCompactions: beforeCompactions,
|
|
2019
|
+
afterSuccessfulCompactions: afterCompactions,
|
|
2020
|
+
tokensRemoved: result.tokensRemoved,
|
|
2021
|
+
messagesRemoved: result.messagesRemoved,
|
|
2022
|
+
messagesLength: afterUsage.messagesLength,
|
|
2023
|
+
contextAttribution: afterDiagnostics.attribution,
|
|
2024
|
+
heaviestMessages: afterDiagnostics.heaviestMessages,
|
|
2025
|
+
usageMetrics: afterDiagnostics.usageMetrics,
|
|
2026
|
+
...this.adaptivePolicyFields(),
|
|
2027
|
+
});
|
|
2028
|
+
if (beforeCompactions === undefined
|
|
2029
|
+
|| afterCompactions === undefined
|
|
2030
|
+
|| afterCompactions <= beforeCompactions) {
|
|
2031
|
+
return this.failUnsafeContext(source, afterUsage, afterPolicy, result.tokensRemoved, 'the successful compaction count did not advance');
|
|
2032
|
+
}
|
|
2033
|
+
if (afterDiagnostics.recomputed.totalTokens >= beforeDiagnostics.recomputed.totalTokens) {
|
|
2034
|
+
return this.failUnsafeContext(source, afterUsage, afterPolicy, result.tokensRemoved, 'exact token recomputation did not decrease after compaction; automatic truncation is unsafe without a caller-approved history boundary');
|
|
2035
|
+
}
|
|
2036
|
+
if (afterUsage.currentTokens >= afterPolicy.threshold) {
|
|
2037
|
+
return this.failUnsafeContext(source, afterUsage, afterPolicy, result.tokensRemoved, 'verified compaction reduced the context but did not restore the required adaptive headroom; automatic truncation is unsafe without a caller-approved history boundary');
|
|
2038
|
+
}
|
|
2039
|
+
return true;
|
|
2040
|
+
}
|
|
2041
|
+
catch (err) {
|
|
2042
|
+
if (this.aborted || this.turnSettled || this._sdkSession !== sdkSession)
|
|
2043
|
+
return false;
|
|
2044
|
+
return this.failUnsafeContext(source, before, policy, 0, `history compaction RPC failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
2045
|
+
}
|
|
2046
|
+
}
|
|
2047
|
+
async verifyCompletedCompaction(sdkSession, data, generation, baselinePromise) {
|
|
2048
|
+
const beforeUsage = this.parentUsage;
|
|
2049
|
+
const beforeDiagnostics = await baselinePromise;
|
|
2050
|
+
const after = await this.measureParentUsage(sdkSession, 'post-compaction');
|
|
2051
|
+
if (this.aborted
|
|
2052
|
+
|| this.turnSettled
|
|
2053
|
+
|| this._sdkSession !== sdkSession
|
|
2054
|
+
|| generation !== this.parentCompactionGeneration)
|
|
2055
|
+
return;
|
|
2056
|
+
const afterDiagnostics = this.latestContextDiagnostics;
|
|
2057
|
+
const policy = this.adaptiveCompactionPolicy;
|
|
2058
|
+
const beforeCompactions = beforeDiagnostics?.attribution?.compactions.count;
|
|
2059
|
+
const afterCompactions = afterDiagnostics?.attribution?.compactions.count;
|
|
2060
|
+
if (!beforeUsage
|
|
2061
|
+
|| !after
|
|
2062
|
+
|| !beforeDiagnostics
|
|
2063
|
+
|| !afterDiagnostics
|
|
2064
|
+
|| beforeCompactions === undefined
|
|
2065
|
+
|| afterCompactions === undefined) {
|
|
2066
|
+
const usage = after ?? beforeUsage;
|
|
2067
|
+
if (usage) {
|
|
2068
|
+
this.failUnsafeContext('compaction-complete', usage, policy, nonNegativeFiniteNumber(data?.tokensRemoved) ?? 0, 'successful compaction could not be verified with exact token recomputation and attribution counts');
|
|
2069
|
+
}
|
|
2070
|
+
else {
|
|
2071
|
+
this.fail(new Error('Successful compaction could not be re-measured'), 'compaction.verification_failure', data);
|
|
2072
|
+
}
|
|
2073
|
+
return;
|
|
2074
|
+
}
|
|
2075
|
+
if (afterCompactions <= beforeCompactions) {
|
|
2076
|
+
this.failUnsafeContext('compaction-complete', after, policy, nonNegativeFiniteNumber(data?.tokensRemoved) ?? 0, 'the successful compaction count did not advance');
|
|
2077
|
+
return;
|
|
2078
|
+
}
|
|
2079
|
+
if (afterDiagnostics.recomputed.totalTokens >= beforeDiagnostics.recomputed.totalTokens) {
|
|
2080
|
+
this.failUnsafeContext('compaction-complete', after, policy, nonNegativeFiniteNumber(data?.tokensRemoved) ?? 0, 'exact token recomputation did not decrease after compaction');
|
|
2081
|
+
return;
|
|
2082
|
+
}
|
|
2083
|
+
if (this.config.compactionMode === 'adaptive'
|
|
2084
|
+
&& policy
|
|
2085
|
+
&& after.currentTokens >= policy.threshold) {
|
|
2086
|
+
this.failUnsafeContext('compaction-complete', after, policy, nonNegativeFiniteNumber(data?.tokensRemoved) ?? 0, 'successful compaction did not restore the required adaptive headroom');
|
|
2087
|
+
return;
|
|
2088
|
+
}
|
|
2089
|
+
const pre = nonNegativeFiniteNumber(data?.preCompactionTokens);
|
|
2090
|
+
const post = nonNegativeFiniteNumber(data?.postCompactionTokens);
|
|
2091
|
+
const removed = nonNegativeFiniteNumber(data?.tokensRemoved);
|
|
2092
|
+
this.logger.info('Verified completed Copilot parent compaction', {
|
|
2093
|
+
eventPreCompactionTokens: pre,
|
|
2094
|
+
eventPostCompactionTokens: post,
|
|
2095
|
+
measuredPreCompactionTokens: beforeDiagnostics.recomputed.totalTokens,
|
|
2096
|
+
measuredPostCompactionTokens: afterDiagnostics.recomputed.totalTokens,
|
|
2097
|
+
beforeSuccessfulCompactions: beforeCompactions,
|
|
2098
|
+
afterSuccessfulCompactions: afterCompactions,
|
|
2099
|
+
tokensRemoved: removed,
|
|
2100
|
+
contextAttribution: afterDiagnostics.attribution,
|
|
2101
|
+
heaviestMessages: afterDiagnostics.heaviestMessages,
|
|
2102
|
+
...this.adaptivePolicyFields(),
|
|
2103
|
+
});
|
|
2104
|
+
const summary = `${beforeDiagnostics.recomputed.totalTokens} → ${afterDiagnostics.recomputed.totalTokens} exact tokens (compactions ${beforeCompactions} → ${afterCompactions})`;
|
|
2105
|
+
this.verifiedParentCompactionGeneration = generation;
|
|
2106
|
+
this.emit('compaction', 'complete', summary);
|
|
2107
|
+
}
|
|
2108
|
+
failUnsafeContext(source, usage, policy, tokensRemoved, reason) {
|
|
2109
|
+
const fields = {
|
|
2110
|
+
source,
|
|
2111
|
+
currentTokens: usage.currentTokens,
|
|
2112
|
+
tokenLimit: policy?.tokenLimit ?? usage.tokenLimit,
|
|
2113
|
+
adaptiveCompactionThreshold: policy?.threshold,
|
|
2114
|
+
adaptiveCompactionHeadroom: policy?.headroom,
|
|
2115
|
+
adaptiveBootstrapHeadroom: policy?.bootstrapHeadroom,
|
|
2116
|
+
adaptiveBootstrapSource: policy?.bootstrapSource,
|
|
2117
|
+
largestObservedInterRequestGrowth: policy?.largestObservedInterRequestGrowth
|
|
2118
|
+
?? this.largestObservedInterRequestGrowth,
|
|
2119
|
+
tokensRemoved,
|
|
2120
|
+
messagesLength: usage.messagesLength,
|
|
2121
|
+
systemTokens: usage.systemTokens,
|
|
2122
|
+
conversationTokens: usage.conversationTokens,
|
|
2123
|
+
toolDefinitionsTokens: usage.toolDefinitionsTokens,
|
|
2124
|
+
reason,
|
|
2125
|
+
};
|
|
2126
|
+
const message = `Unsafe parent context: currentTokens=${usage.currentTokens}, tokenLimit=${fields.tokenLimit}, ceiling=${policy?.threshold ?? 'unknown'}, headroom=${policy?.headroom ?? 'unknown'}, tokensRemoved=${tokensRemoved}, messagesLength=${usage.messagesLength}; ${reason}`;
|
|
2127
|
+
this.logger.error('Copilot parent context could not be compacted safely', fields);
|
|
2128
|
+
try {
|
|
2129
|
+
process.stderr.write(`[SdkBackend] ADAPTIVE COMPACTION TERMINAL: ${message}\n`);
|
|
2130
|
+
}
|
|
2131
|
+
catch { /* closed stream */ }
|
|
2132
|
+
this.fail(new Error(message), 'adaptive-compaction', fields);
|
|
2133
|
+
return false;
|
|
2134
|
+
}
|
|
1353
2135
|
async resolvePendingRequest(sdkSession, e) {
|
|
1354
2136
|
if (this.aborted || this._sdkSession !== sdkSession)
|
|
1355
2137
|
return;
|
|
@@ -1417,7 +2199,10 @@ class SdkSession {
|
|
|
1417
2199
|
this.abortGraceTimer = null;
|
|
1418
2200
|
}
|
|
1419
2201
|
this.compactionInProgress = false;
|
|
1420
|
-
this.
|
|
2202
|
+
this.parentCompactionGeneration = 0;
|
|
2203
|
+
this.verifiedParentCompactionGeneration = 0;
|
|
2204
|
+
this.compactionBaselinePromise = undefined;
|
|
2205
|
+
this.proactiveCompactionPromise = undefined;
|
|
1421
2206
|
}
|
|
1422
2207
|
}
|
|
1423
2208
|
//# sourceMappingURL=sdk-backend.js.map
|