tinker-agent 2.3.0 → 2.4.0
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 +22 -1
- package/package.json +1 -1
- package/src/agent/loop.ts +1 -1
- package/src/agent/runtime-session.ts +259 -16
- package/src/agent/session-ledger.ts +10 -3
- package/src/context/context-manager.ts +185 -8
- package/src/context/context-policy.ts +1 -1
- package/src/context/context-revision-compiler.ts +7 -2
- package/src/context/context-swap-label.ts +132 -0
- package/src/context/swap-planner.ts +219 -56
- package/src/events/stdout-event-printer.ts +1 -0
- package/src/events/types.ts +3 -3
- package/src/observation/observation-builder.ts +36 -0
- package/src/session/session-store.ts +300 -0
- package/src/tools/context-maintenance.ts +266 -0
- package/src/tools/registry.ts +18 -1
- package/src/tools/types.ts +83 -1
- package/src/tui/event-store.ts +15 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { ContextBuilder } from "../agent/context-builder";
|
|
2
2
|
import type { ContextUsageSnapshot } from "../agent/context-meter";
|
|
3
|
-
import type { TurnId } from "../ids/runtime-id";
|
|
3
|
+
import type { MessageId, TurnId } from "../ids/runtime-id";
|
|
4
4
|
import type { ModelClient, PreparedModelRequest } from "../model/model-client";
|
|
5
5
|
import {
|
|
6
6
|
promptPrefixFingerprint,
|
|
@@ -39,7 +39,11 @@ import type {
|
|
|
39
39
|
ToolResultRecord,
|
|
40
40
|
} from "./protocol-frame";
|
|
41
41
|
|
|
42
|
-
export type SwapPlanningTrigger =
|
|
42
|
+
export type SwapPlanningTrigger =
|
|
43
|
+
| "manual"
|
|
44
|
+
| "runtime_pressure"
|
|
45
|
+
| "benchmark_forced"
|
|
46
|
+
| "model_directed";
|
|
43
47
|
|
|
44
48
|
export type SwapPlanningOutcome =
|
|
45
49
|
| "below_trigger"
|
|
@@ -86,6 +90,7 @@ export type SwapPlanningInput = {
|
|
|
86
90
|
readonly consumedThroughOrdinal: number;
|
|
87
91
|
};
|
|
88
92
|
readonly forcedTargetTokens?: number;
|
|
93
|
+
readonly selectedMessageIds?: readonly MessageId[];
|
|
89
94
|
};
|
|
90
95
|
|
|
91
96
|
export type SwapPlanningResult = {
|
|
@@ -123,14 +128,17 @@ export class SwapPlanStaleError extends Error {
|
|
|
123
128
|
|
|
124
129
|
type ModelPreparer = Pick<ModelClient, "prepare">;
|
|
125
130
|
|
|
126
|
-
type
|
|
131
|
+
export type EligibleSwapCandidate = {
|
|
127
132
|
readonly override: SwapOverride;
|
|
128
133
|
readonly rawKind: SwappableRawKind;
|
|
134
|
+
readonly message: Extract<CanonicalMessageRecord, { role: "tool" }>;
|
|
135
|
+
readonly result: ToolResultRecord;
|
|
129
136
|
};
|
|
130
137
|
|
|
131
|
-
type
|
|
132
|
-
readonly eligible: readonly
|
|
138
|
+
export type SwapCandidateScan = {
|
|
139
|
+
readonly eligible: readonly EligibleSwapCandidate[];
|
|
133
140
|
readonly excludedByReason: Readonly<Record<string, number>>;
|
|
141
|
+
readonly excludedByMessageId: ReadonlyMap<MessageId, string>;
|
|
134
142
|
};
|
|
135
143
|
|
|
136
144
|
type Projection = {
|
|
@@ -172,7 +180,7 @@ export class SwapPlanner {
|
|
|
172
180
|
targetTokens,
|
|
173
181
|
);
|
|
174
182
|
}
|
|
175
|
-
if (guardedTokensBefore <= targetTokens) {
|
|
183
|
+
if (input.trigger !== "model_directed" && guardedTokensBefore <= targetTokens) {
|
|
176
184
|
return emptyResult(
|
|
177
185
|
input,
|
|
178
186
|
input.trigger === "runtime_pressure" ? "below_trigger" : "below_target",
|
|
@@ -189,6 +197,16 @@ export class SwapPlanner {
|
|
|
189
197
|
input.revision.keepFromOrdinal,
|
|
190
198
|
input.activeTurn,
|
|
191
199
|
);
|
|
200
|
+
if (input.trigger === "model_directed") {
|
|
201
|
+
return this.planSelected({
|
|
202
|
+
input,
|
|
203
|
+
scan,
|
|
204
|
+
activeFingerprint,
|
|
205
|
+
rawTokensBefore,
|
|
206
|
+
guardedTokensBefore,
|
|
207
|
+
targetTokens,
|
|
208
|
+
});
|
|
209
|
+
}
|
|
192
210
|
if (scan.eligible.length === 0) {
|
|
193
211
|
return {
|
|
194
212
|
...emptyResult(
|
|
@@ -208,47 +226,7 @@ export class SwapPlanner {
|
|
|
208
226
|
if (existing !== undefined) {
|
|
209
227
|
return existing;
|
|
210
228
|
}
|
|
211
|
-
const
|
|
212
|
-
.slice(0, count)
|
|
213
|
-
.map((entry) => entry.override);
|
|
214
|
-
let prepared: PreparedModelRequest;
|
|
215
|
-
try {
|
|
216
|
-
const compiled = this.compiler.compileProspective({
|
|
217
|
-
active: input.active,
|
|
218
|
-
canonical: input.canonical,
|
|
219
|
-
activeOverrides: input.activeOverrides,
|
|
220
|
-
addedOverrides,
|
|
221
|
-
activeSurface: input.surface,
|
|
222
|
-
});
|
|
223
|
-
const built = this.requestBuilder.build({
|
|
224
|
-
canonical: input.canonical,
|
|
225
|
-
revision: input.revision,
|
|
226
|
-
surface: input.surface,
|
|
227
|
-
activeOverrides: [...input.activeOverrides, ...addedOverrides],
|
|
228
|
-
compiled,
|
|
229
|
-
tools: input.tools,
|
|
230
|
-
});
|
|
231
|
-
prepared = this.model.prepare(built.request);
|
|
232
|
-
} catch (error) {
|
|
233
|
-
if (isCanonicalPlanningError(error)) {
|
|
234
|
-
throw error;
|
|
235
|
-
}
|
|
236
|
-
throw new SwapPlanningDiagnosticError(
|
|
237
|
-
"prepare",
|
|
238
|
-
"prospective_prepare_failed",
|
|
239
|
-
"Prospective request preparation failed.",
|
|
240
|
-
{ cause: error },
|
|
241
|
-
);
|
|
242
|
-
}
|
|
243
|
-
assertProspectiveConfiguration(input.activePrepared, prepared);
|
|
244
|
-
const breakdown = estimatePromptSegments(prepared.promptSegments);
|
|
245
|
-
const rawTokens = breakdown.totalTokens;
|
|
246
|
-
const projection = Object.freeze({
|
|
247
|
-
count,
|
|
248
|
-
prepared,
|
|
249
|
-
rawTokens,
|
|
250
|
-
guardedTokens: guardTokens(breakdown, input.activeUsage.correctionFactor),
|
|
251
|
-
});
|
|
229
|
+
const projection = this.projectCandidates(input, scan.eligible.slice(0, count));
|
|
252
230
|
projectionCache.set(count, projection);
|
|
253
231
|
return projection;
|
|
254
232
|
};
|
|
@@ -355,13 +333,147 @@ export class SwapPlanner {
|
|
|
355
333
|
});
|
|
356
334
|
}
|
|
357
335
|
|
|
358
|
-
private
|
|
336
|
+
private planSelected(input: {
|
|
337
|
+
input: SwapPlanningInput;
|
|
338
|
+
scan: SwapCandidateScan;
|
|
339
|
+
activeFingerprint: PromptPrefixFingerprint;
|
|
340
|
+
rawTokensBefore: number;
|
|
341
|
+
guardedTokensBefore: number;
|
|
342
|
+
targetTokens: number;
|
|
343
|
+
}): SwapPlanningResult {
|
|
344
|
+
const selectedMessageIds = input.input.selectedMessageIds;
|
|
345
|
+
if (selectedMessageIds === undefined || selectedMessageIds.length === 0) {
|
|
346
|
+
throw new SwapPlanningDiagnosticError(
|
|
347
|
+
"validate",
|
|
348
|
+
"missing_selected_candidates",
|
|
349
|
+
"Model-directed swap planning requires selected candidates.",
|
|
350
|
+
);
|
|
351
|
+
}
|
|
352
|
+
const eligibleById = new Map(
|
|
353
|
+
input.scan.eligible.map((candidate) => [candidate.message.messageId, candidate]),
|
|
354
|
+
);
|
|
355
|
+
const selected: EligibleSwapCandidate[] = [];
|
|
356
|
+
for (const messageId of selectedMessageIds) {
|
|
357
|
+
const candidate = eligibleById.get(messageId);
|
|
358
|
+
if (candidate === undefined) {
|
|
359
|
+
const reason = swapCandidateRejectionReason(
|
|
360
|
+
input.scan,
|
|
361
|
+
input.input.canonical,
|
|
362
|
+
messageId,
|
|
363
|
+
);
|
|
364
|
+
throw new SwapPlanningDiagnosticError(
|
|
365
|
+
"candidate",
|
|
366
|
+
`selected_candidate_${reason}`,
|
|
367
|
+
`A model-directed swap candidate is no longer eligible (${reason}).`,
|
|
368
|
+
);
|
|
369
|
+
}
|
|
370
|
+
selected.push(candidate);
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
const projection = this.projectCandidates(input.input, selected);
|
|
374
|
+
if (
|
|
375
|
+
projection.rawTokens >= input.rawTokensBefore ||
|
|
376
|
+
projection.guardedTokens >= input.guardedTokensBefore
|
|
377
|
+
) {
|
|
378
|
+
throw new SwapPlanningDiagnosticError(
|
|
379
|
+
"validate",
|
|
380
|
+
"no_token_reduction",
|
|
381
|
+
"Prospective request did not strictly reduce raw and guarded tokens.",
|
|
382
|
+
);
|
|
383
|
+
}
|
|
384
|
+
const addedOverrides = Object.freeze(
|
|
385
|
+
selected.map((candidate) => candidate.override),
|
|
386
|
+
);
|
|
387
|
+
const plan = createPlan({
|
|
388
|
+
input: input.input,
|
|
389
|
+
activeFingerprint: input.activeFingerprint,
|
|
390
|
+
projectedFingerprint: promptPrefixFingerprint(projection.prepared),
|
|
391
|
+
addedOverrides,
|
|
392
|
+
targetTokens: input.targetTokens,
|
|
393
|
+
rawTokensBefore: input.rawTokensBefore,
|
|
394
|
+
rawTokensAfter: projection.rawTokens,
|
|
395
|
+
guardedTokensBefore: input.guardedTokensBefore,
|
|
396
|
+
guardedTokensAfter: projection.guardedTokens,
|
|
397
|
+
});
|
|
398
|
+
assertPlanBaseCurrent(plan, {
|
|
399
|
+
active: input.input.active,
|
|
400
|
+
revision: input.input.revision,
|
|
401
|
+
activeOverrides: input.input.activeOverrides,
|
|
402
|
+
activePrepared: input.input.activePrepared,
|
|
403
|
+
});
|
|
404
|
+
return Object.freeze({
|
|
405
|
+
outcome:
|
|
406
|
+
projection.guardedTokens <= input.targetTokens
|
|
407
|
+
? "target_reached"
|
|
408
|
+
: "insufficient_candidates",
|
|
409
|
+
canonicalMessageCount: input.input.canonical.messages.length,
|
|
410
|
+
eligibleCandidateCount: input.scan.eligible.length,
|
|
411
|
+
excludedByReason: input.scan.excludedByReason,
|
|
412
|
+
selectedByRawKind: countRawKinds(selected),
|
|
413
|
+
originalObservationBytes: addedOverrides.reduce(
|
|
414
|
+
(total, override) => total + override.originalBytes,
|
|
415
|
+
0,
|
|
416
|
+
),
|
|
417
|
+
projectedObservationBytes: addedOverrides.reduce(
|
|
418
|
+
(total, override) => total + override.renderedBytes,
|
|
419
|
+
0,
|
|
420
|
+
),
|
|
421
|
+
rawTokensBefore: input.rawTokensBefore,
|
|
422
|
+
guardedTokensBefore: input.guardedTokensBefore,
|
|
423
|
+
targetTokens: input.targetTokens,
|
|
424
|
+
plan,
|
|
425
|
+
});
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
private projectCandidates(
|
|
429
|
+
input: SwapPlanningInput,
|
|
430
|
+
candidates: readonly EligibleSwapCandidate[],
|
|
431
|
+
): Projection {
|
|
432
|
+
const addedOverrides = candidates.map((candidate) => candidate.override);
|
|
433
|
+
let prepared: PreparedModelRequest;
|
|
434
|
+
try {
|
|
435
|
+
const compiled = this.compiler.compileProspective({
|
|
436
|
+
active: input.active,
|
|
437
|
+
canonical: input.canonical,
|
|
438
|
+
activeOverrides: input.activeOverrides,
|
|
439
|
+
addedOverrides,
|
|
440
|
+
activeSurface: input.surface,
|
|
441
|
+
});
|
|
442
|
+
const built = this.requestBuilder.build({
|
|
443
|
+
canonical: input.canonical,
|
|
444
|
+
revision: input.revision,
|
|
445
|
+
surface: input.surface,
|
|
446
|
+
activeOverrides: [...input.activeOverrides, ...addedOverrides],
|
|
447
|
+
compiled,
|
|
448
|
+
tools: input.tools,
|
|
449
|
+
});
|
|
450
|
+
prepared = this.model.prepare(built.request);
|
|
451
|
+
} catch (error) {
|
|
452
|
+
if (isCanonicalPlanningError(error)) throw error;
|
|
453
|
+
throw new SwapPlanningDiagnosticError(
|
|
454
|
+
"prepare",
|
|
455
|
+
"prospective_prepare_failed",
|
|
456
|
+
"Prospective request preparation failed.",
|
|
457
|
+
{ cause: error },
|
|
458
|
+
);
|
|
459
|
+
}
|
|
460
|
+
assertProspectiveConfiguration(input.activePrepared, prepared);
|
|
461
|
+
const breakdown = estimatePromptSegments(prepared.promptSegments);
|
|
462
|
+
return Object.freeze({
|
|
463
|
+
count: candidates.length,
|
|
464
|
+
prepared,
|
|
465
|
+
rawTokens: breakdown.totalTokens,
|
|
466
|
+
guardedTokens: guardTokens(breakdown, input.activeUsage.correctionFactor),
|
|
467
|
+
});
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
scanCandidates(
|
|
359
471
|
canonical: ProtocolContextView,
|
|
360
472
|
activeOverrides: readonly SwapOverride[],
|
|
361
473
|
policy: SwapOnlyPolicyV1,
|
|
362
474
|
keepFromOrdinal: number,
|
|
363
475
|
activeTurn: SwapPlanningInput["activeTurn"],
|
|
364
|
-
):
|
|
476
|
+
): SwapCandidateScan {
|
|
365
477
|
const alreadySwapped = new Set(
|
|
366
478
|
activeOverrides.map((override) => override.messageId),
|
|
367
479
|
);
|
|
@@ -373,19 +485,28 @@ export class SwapPlanner {
|
|
|
373
485
|
const resultsByMessage = new Map(
|
|
374
486
|
canonical.toolResults.map((result) => [result.toolMessageId, result] as const),
|
|
375
487
|
);
|
|
376
|
-
const eligible:
|
|
488
|
+
const eligible: EligibleSwapCandidate[] = [];
|
|
377
489
|
const exclusions = new Map<string, number>();
|
|
490
|
+
const excludedByMessageId = new Map<MessageId, string>();
|
|
491
|
+
|
|
492
|
+
const exclude = (
|
|
493
|
+
message: Extract<CanonicalMessageRecord, { role: "tool" }>,
|
|
494
|
+
reason: string,
|
|
495
|
+
) => {
|
|
496
|
+
increment(exclusions, reason);
|
|
497
|
+
excludedByMessageId.set(message.messageId, reason);
|
|
498
|
+
};
|
|
378
499
|
|
|
379
500
|
for (const message of canonical.messages) {
|
|
380
501
|
if (message.role !== "tool") {
|
|
381
502
|
continue;
|
|
382
503
|
}
|
|
383
504
|
if (message.ordinal < keepFromOrdinal) {
|
|
384
|
-
|
|
505
|
+
exclude(message, "retired_prefix");
|
|
385
506
|
continue;
|
|
386
507
|
}
|
|
387
508
|
if (alreadySwapped.has(message.messageId)) {
|
|
388
|
-
|
|
509
|
+
exclude(message, "already_swapped");
|
|
389
510
|
continue;
|
|
390
511
|
}
|
|
391
512
|
const reason = basicExclusionReason({
|
|
@@ -396,7 +517,7 @@ export class SwapPlanner {
|
|
|
396
517
|
minimumObservationBytes: policy.minimumObservationBytes,
|
|
397
518
|
});
|
|
398
519
|
if (reason !== undefined) {
|
|
399
|
-
|
|
520
|
+
exclude(message, reason);
|
|
400
521
|
continue;
|
|
401
522
|
}
|
|
402
523
|
const result = resultsByMessage.get(message.messageId);
|
|
@@ -415,6 +536,8 @@ export class SwapPlanner {
|
|
|
415
536
|
Object.freeze({
|
|
416
537
|
override: this.renderer.render({ message, result }),
|
|
417
538
|
rawKind: result.completion.raw.kind,
|
|
539
|
+
message,
|
|
540
|
+
result,
|
|
418
541
|
}),
|
|
419
542
|
);
|
|
420
543
|
} catch (error) {
|
|
@@ -429,7 +552,7 @@ export class SwapPlanner {
|
|
|
429
552
|
if (error.code === "source_hash_mismatch") {
|
|
430
553
|
throw new ContextRevisionError(error.message);
|
|
431
554
|
}
|
|
432
|
-
|
|
555
|
+
exclude(message, error.code);
|
|
433
556
|
}
|
|
434
557
|
}
|
|
435
558
|
|
|
@@ -437,10 +560,26 @@ export class SwapPlanner {
|
|
|
437
560
|
return Object.freeze({
|
|
438
561
|
eligible: Object.freeze(eligible),
|
|
439
562
|
excludedByReason: frozenCountRecord(exclusions),
|
|
563
|
+
excludedByMessageId,
|
|
440
564
|
});
|
|
441
565
|
}
|
|
442
566
|
}
|
|
443
567
|
|
|
568
|
+
export function swapCandidateRejectionReason(
|
|
569
|
+
scan: SwapCandidateScan,
|
|
570
|
+
canonical: ProtocolContextView,
|
|
571
|
+
messageId: MessageId,
|
|
572
|
+
): string {
|
|
573
|
+
if (scan.eligible.some((candidate) => candidate.message.messageId === messageId)) {
|
|
574
|
+
throw new Error("Eligible swap candidate has no rejection reason.");
|
|
575
|
+
}
|
|
576
|
+
const recorded = scan.excludedByMessageId.get(messageId);
|
|
577
|
+
if (recorded !== undefined) return recorded;
|
|
578
|
+
return canonical.messages.some((message) => message.messageId === messageId)
|
|
579
|
+
? "not_tool_message"
|
|
580
|
+
: "candidate_not_found";
|
|
581
|
+
}
|
|
582
|
+
|
|
444
583
|
export function assertPlanBaseCurrent(
|
|
445
584
|
plan: SwapRevisionPlan,
|
|
446
585
|
current: {
|
|
@@ -523,6 +662,27 @@ function validatePlanningInput(input: SwapPlanningInput): void {
|
|
|
523
662
|
"Forced swap target must be a non-negative safe integer.",
|
|
524
663
|
);
|
|
525
664
|
}
|
|
665
|
+
if (input.trigger === "model_directed") {
|
|
666
|
+
if (
|
|
667
|
+
input.activeTurn === undefined ||
|
|
668
|
+
input.selectedMessageIds === undefined ||
|
|
669
|
+
input.selectedMessageIds.length < 1 ||
|
|
670
|
+
input.selectedMessageIds.length > 16 ||
|
|
671
|
+
new Set(input.selectedMessageIds).size !== input.selectedMessageIds.length
|
|
672
|
+
) {
|
|
673
|
+
throw new SwapPlanningDiagnosticError(
|
|
674
|
+
"validate",
|
|
675
|
+
"invalid_selected_candidates",
|
|
676
|
+
"Model-directed swap planning requires 1 to 16 unique candidates and an active turn boundary.",
|
|
677
|
+
);
|
|
678
|
+
}
|
|
679
|
+
} else if (input.selectedMessageIds !== undefined) {
|
|
680
|
+
throw new SwapPlanningDiagnosticError(
|
|
681
|
+
"validate",
|
|
682
|
+
"unexpected_selected_candidates",
|
|
683
|
+
"Only model-directed swap planning accepts selected candidates.",
|
|
684
|
+
);
|
|
685
|
+
}
|
|
526
686
|
}
|
|
527
687
|
|
|
528
688
|
function assertActiveFingerprint(
|
|
@@ -647,7 +807,10 @@ function basicExclusionReason(input: {
|
|
|
647
807
|
return undefined;
|
|
648
808
|
}
|
|
649
809
|
|
|
650
|
-
function compareCandidates(
|
|
810
|
+
function compareCandidates(
|
|
811
|
+
left: EligibleSwapCandidate,
|
|
812
|
+
right: EligibleSwapCandidate,
|
|
813
|
+
): number {
|
|
651
814
|
return (
|
|
652
815
|
right.override.byteSavings - left.override.byteSavings ||
|
|
653
816
|
right.override.originalBytes - left.override.originalBytes ||
|
|
@@ -724,7 +887,7 @@ function createPlan(input: {
|
|
|
724
887
|
}
|
|
725
888
|
|
|
726
889
|
function countRawKinds(
|
|
727
|
-
candidates: readonly
|
|
890
|
+
candidates: readonly EligibleSwapCandidate[],
|
|
728
891
|
): Readonly<Record<string, number>> {
|
|
729
892
|
const counts = new Map<string, number>();
|
|
730
893
|
for (const candidate of candidates) {
|
package/src/events/types.ts
CHANGED
|
@@ -53,7 +53,7 @@ export type ContextUsageUpdatedData = {
|
|
|
53
53
|
export type ContextRevisionStartedData =
|
|
54
54
|
| {
|
|
55
55
|
strategy: "swap";
|
|
56
|
-
reason: "manual" | "runtime_pressure";
|
|
56
|
+
reason: "manual" | "runtime_pressure" | "model_directed";
|
|
57
57
|
policyVersion: "swap-only-v1";
|
|
58
58
|
rendererFormat: "swap-observation-v1";
|
|
59
59
|
qualificationId?: string;
|
|
@@ -81,7 +81,7 @@ export type ContextRevisionStartedData =
|
|
|
81
81
|
export type ContextRevisionFinishedData =
|
|
82
82
|
| {
|
|
83
83
|
strategy: "swap";
|
|
84
|
-
reason: "manual" | "runtime_pressure";
|
|
84
|
+
reason: "manual" | "runtime_pressure" | "model_directed";
|
|
85
85
|
policyVersion: "swap-only-v1";
|
|
86
86
|
outcome:
|
|
87
87
|
| "below_trigger"
|
|
@@ -162,7 +162,7 @@ export type ContextRevisionFinishedData =
|
|
|
162
162
|
export type ContextRevisionFailedData =
|
|
163
163
|
| {
|
|
164
164
|
strategy: "swap";
|
|
165
|
-
reason: "manual" | "runtime_pressure";
|
|
165
|
+
reason: "manual" | "runtime_pressure" | "model_directed";
|
|
166
166
|
stage: "snapshot" | "plan" | "validate" | "commit" | "activate";
|
|
167
167
|
errorCode: string;
|
|
168
168
|
error: string;
|
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
} from "../agent/tool-result-content";
|
|
7
7
|
import type {
|
|
8
8
|
BashRawResult,
|
|
9
|
+
ContextMaintenanceRawResult,
|
|
9
10
|
DeleteFileRawResult,
|
|
10
11
|
EditFileRawResult,
|
|
11
12
|
GenericToolRawResult,
|
|
@@ -48,6 +49,8 @@ export class ObservationBuilder {
|
|
|
48
49
|
return renderViewImageObservation(input.raw);
|
|
49
50
|
case "recall":
|
|
50
51
|
return textObservation(renderRecallObservation(input.raw));
|
|
52
|
+
case "context_maintenance":
|
|
53
|
+
return textObservation(renderContextMaintenanceObservation(input.raw));
|
|
51
54
|
case "memory_search":
|
|
52
55
|
return textObservation(renderMemorySearchObservation(input.raw));
|
|
53
56
|
case "memory_get":
|
|
@@ -117,6 +120,39 @@ function renderWaitObservation(raw: WaitRawResult): string {
|
|
|
117
120
|
: `Wait failed: ${raw.error}`;
|
|
118
121
|
}
|
|
119
122
|
|
|
123
|
+
function renderContextMaintenanceObservation(raw: ContextMaintenanceRawResult): string {
|
|
124
|
+
if (!raw.ok) {
|
|
125
|
+
if (raw.operation === "swap" && raw.rejected.length > 0) {
|
|
126
|
+
return JSON.stringify({ ok: false, rejected: raw.rejected });
|
|
127
|
+
}
|
|
128
|
+
return JSON.stringify({ ok: false, error: raw.error });
|
|
129
|
+
}
|
|
130
|
+
switch (raw.operation) {
|
|
131
|
+
case "status":
|
|
132
|
+
return JSON.stringify({
|
|
133
|
+
ok: true,
|
|
134
|
+
usedInputTokens: raw.usedInputTokens,
|
|
135
|
+
inputBudgetTokens: raw.inputBudgetTokens,
|
|
136
|
+
pressure: raw.pressure,
|
|
137
|
+
triggerTokens: raw.triggerTokens,
|
|
138
|
+
source: raw.source,
|
|
139
|
+
});
|
|
140
|
+
case "candidates":
|
|
141
|
+
return JSON.stringify({
|
|
142
|
+
ok: true,
|
|
143
|
+
total: raw.total,
|
|
144
|
+
candidates: raw.candidates,
|
|
145
|
+
});
|
|
146
|
+
case "swap":
|
|
147
|
+
return JSON.stringify({
|
|
148
|
+
ok: true,
|
|
149
|
+
scheduled: raw.scheduled,
|
|
150
|
+
rejected: raw.rejected,
|
|
151
|
+
note: raw.note,
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
120
156
|
function assertNever(value: never): never {
|
|
121
157
|
throw new Error(`Unhandled tool raw result: ${JSON.stringify(value)}`);
|
|
122
158
|
}
|