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.
@@ -13,6 +13,7 @@ import {
13
13
  } from "node:fs/promises";
14
14
  import { randomUUID } from "node:crypto";
15
15
  import { Database } from "bun:sqlite";
16
+ import { parseMessageId } from "../ids/runtime-id";
16
17
  import type {
17
18
  ContextRevisionId,
18
19
  ContextSurfaceId,
@@ -5096,6 +5097,7 @@ export function decodeStoredToolRawResult(value: unknown): ToolRawResult {
5096
5097
  "web_search",
5097
5098
  "web_fetch",
5098
5099
  "recall",
5100
+ "context_maintenance",
5099
5101
  "memory_search",
5100
5102
  "memory_get",
5101
5103
  "wait",
@@ -5114,9 +5116,282 @@ export function decodeStoredToolRawResult(value: unknown): ToolRawResult {
5114
5116
  if (kind === "view_image") {
5115
5117
  return decodeStoredViewImageRawResult(raw);
5116
5118
  }
5119
+ if (kind === "context_maintenance") {
5120
+ return decodeStoredContextMaintenanceRawResult(raw);
5121
+ }
5117
5122
  return immutableCanonicalClone(raw) as ToolRawResult;
5118
5123
  }
5119
5124
 
5125
+ function decodeStoredContextMaintenanceRawResult(
5126
+ raw: Record<string, unknown>,
5127
+ ): Extract<ToolRawResult, { kind: "context_maintenance" }> {
5128
+ const operation = enumFromSql(
5129
+ raw.operation,
5130
+ ["status", "candidates", "swap"] as const,
5131
+ "context maintenance operation",
5132
+ );
5133
+ if (raw.ok === false) {
5134
+ if (operation !== "swap") {
5135
+ assertObjectKeys(
5136
+ raw,
5137
+ ["kind", "ok", "operation", "error"],
5138
+ ["kind", "ok", "operation", "error"],
5139
+ `failed context ${operation} result`,
5140
+ );
5141
+ return immutableRecord({
5142
+ kind: "context_maintenance" as const,
5143
+ ok: false as const,
5144
+ operation,
5145
+ error: nonEmptyStringFromJson(raw.error, `context ${operation} error`),
5146
+ });
5147
+ }
5148
+ assertObjectKeys(
5149
+ raw,
5150
+ ["kind", "ok", "operation", "scheduled", "rejected", "error"],
5151
+ ["kind", "ok", "operation", "scheduled", "rejected"],
5152
+ "failed context swap result",
5153
+ );
5154
+ if (!Array.isArray(raw.scheduled) || raw.scheduled.length !== 0) {
5155
+ throw new Error("Failed context swap result must schedule no candidates.");
5156
+ }
5157
+ const rejected = decodeContextSwapRejected(raw.rejected);
5158
+ const error =
5159
+ raw.error === undefined
5160
+ ? undefined
5161
+ : nonEmptyStringFromJson(raw.error, "context swap error");
5162
+ if (rejected.length === 0 && error === undefined) {
5163
+ throw new Error("Failed context swap result must explain its failure.");
5164
+ }
5165
+ return immutableRecord({
5166
+ kind: "context_maintenance" as const,
5167
+ ok: false as const,
5168
+ operation,
5169
+ scheduled: Object.freeze([]),
5170
+ rejected,
5171
+ ...(error === undefined ? {} : { error }),
5172
+ });
5173
+ }
5174
+ if (raw.ok !== true) {
5175
+ throw new Error("Context maintenance raw result ok must be a boolean.");
5176
+ }
5177
+ if (operation === "status") {
5178
+ assertObjectKeys(
5179
+ raw,
5180
+ [
5181
+ "kind",
5182
+ "ok",
5183
+ "operation",
5184
+ "usedInputTokens",
5185
+ "inputBudgetTokens",
5186
+ "pressure",
5187
+ "triggerTokens",
5188
+ "source",
5189
+ ],
5190
+ [
5191
+ "kind",
5192
+ "ok",
5193
+ "operation",
5194
+ "usedInputTokens",
5195
+ "inputBudgetTokens",
5196
+ "pressure",
5197
+ "triggerTokens",
5198
+ "source",
5199
+ ],
5200
+ "context status result",
5201
+ );
5202
+ return immutableRecord({
5203
+ kind: "context_maintenance" as const,
5204
+ ok: true as const,
5205
+ operation,
5206
+ usedInputTokens: nonNegativeJsonInteger(
5207
+ raw.usedInputTokens,
5208
+ "context status usedInputTokens",
5209
+ ),
5210
+ inputBudgetTokens: positiveJsonInteger(
5211
+ raw.inputBudgetTokens,
5212
+ "context status inputBudgetTokens",
5213
+ ),
5214
+ pressure: enumFromSql(
5215
+ raw.pressure,
5216
+ ["normal", "high", "critical"] as const,
5217
+ "context status pressure",
5218
+ ),
5219
+ triggerTokens: positiveJsonInteger(
5220
+ raw.triggerTokens,
5221
+ "context status triggerTokens",
5222
+ ),
5223
+ source: enumFromSql(
5224
+ raw.source,
5225
+ [
5226
+ "estimated_full",
5227
+ "provider_measured",
5228
+ "measured_plus_estimated_delta",
5229
+ ] as const,
5230
+ "context status source",
5231
+ ),
5232
+ });
5233
+ }
5234
+ if (operation === "candidates") {
5235
+ assertObjectKeys(
5236
+ raw,
5237
+ ["kind", "ok", "operation", "total", "candidates"],
5238
+ ["kind", "ok", "operation", "total", "candidates"],
5239
+ "context swap candidates result",
5240
+ );
5241
+ if (!Array.isArray(raw.candidates) || raw.candidates.length > 50) {
5242
+ throw new Error("Context swap candidates result has an invalid page.");
5243
+ }
5244
+ const candidates = raw.candidates.map((value, index) => {
5245
+ const candidate = recordFromSql(value, `context candidate ${index}`);
5246
+ assertObjectKeys(
5247
+ candidate,
5248
+ ["candidateId", "label", "ordinal", "savingsBytes"],
5249
+ ["candidateId", "label", "ordinal", "savingsBytes"],
5250
+ `context candidate ${index}`,
5251
+ );
5252
+ const label = stringFromSql(candidate.label, `context candidate ${index} label`);
5253
+ if (
5254
+ label === "" ||
5255
+ label !== label.replace(/[\p{Cc}\p{Cf}\s]+/gu, " ").trim() ||
5256
+ Buffer.byteLength(label, "utf8") > 80
5257
+ ) {
5258
+ throw new Error(`Context candidate ${index} label is invalid or too large.`);
5259
+ }
5260
+ return immutableRecord({
5261
+ candidateId: parseMessageId(
5262
+ stringFromSql(candidate.candidateId, `context candidate ${index} ID`),
5263
+ ),
5264
+ label,
5265
+ ordinal: positiveJsonInteger(
5266
+ candidate.ordinal,
5267
+ `context candidate ${index} ordinal`,
5268
+ ),
5269
+ savingsBytes: positiveJsonInteger(
5270
+ candidate.savingsBytes,
5271
+ `context candidate ${index} savingsBytes`,
5272
+ ),
5273
+ });
5274
+ });
5275
+ if (
5276
+ new Set(candidates.map((candidate) => candidate.candidateId)).size !==
5277
+ candidates.length ||
5278
+ candidates.some(
5279
+ (candidate, index) =>
5280
+ index > 0 &&
5281
+ candidate.ordinal <= (candidates[index - 1]?.ordinal ?? candidate.ordinal),
5282
+ )
5283
+ ) {
5284
+ throw new Error(
5285
+ "Context swap candidates must have unique IDs and ascending ordinals.",
5286
+ );
5287
+ }
5288
+ const total = nonNegativeJsonInteger(raw.total, "context candidates total");
5289
+ if (total < candidates.length) {
5290
+ throw new Error("Context candidates total is smaller than its page.");
5291
+ }
5292
+ return immutableRecord({
5293
+ kind: "context_maintenance" as const,
5294
+ ok: true as const,
5295
+ operation,
5296
+ total,
5297
+ candidates: Object.freeze(candidates),
5298
+ });
5299
+ }
5300
+
5301
+ assertObjectKeys(
5302
+ raw,
5303
+ ["kind", "ok", "operation", "scheduled", "rejected", "note"],
5304
+ ["kind", "ok", "operation", "scheduled", "rejected", "note"],
5305
+ "context swap result",
5306
+ );
5307
+ if (!Array.isArray(raw.scheduled) || raw.scheduled.length < 1) {
5308
+ throw new Error("Successful context swap result must schedule candidates.");
5309
+ }
5310
+ const scheduled = raw.scheduled.map((value, index) => {
5311
+ const candidate = recordFromSql(value, `scheduled context candidate ${index}`);
5312
+ assertObjectKeys(
5313
+ candidate,
5314
+ ["candidateId", "savingsBytes"],
5315
+ ["candidateId", "savingsBytes"],
5316
+ `scheduled context candidate ${index}`,
5317
+ );
5318
+ return immutableRecord({
5319
+ candidateId: parseMessageId(
5320
+ stringFromSql(candidate.candidateId, `scheduled candidate ${index} ID`),
5321
+ ),
5322
+ savingsBytes: positiveJsonInteger(
5323
+ candidate.savingsBytes,
5324
+ `scheduled candidate ${index} savingsBytes`,
5325
+ ),
5326
+ });
5327
+ });
5328
+ if (
5329
+ scheduled.length > 16 ||
5330
+ new Set(scheduled.map((candidate) => candidate.candidateId)).size !==
5331
+ scheduled.length
5332
+ ) {
5333
+ throw new Error("Successful context swap result has invalid scheduled IDs.");
5334
+ }
5335
+ const rejected = decodeContextSwapRejected(raw.rejected);
5336
+ if (
5337
+ scheduled.length + rejected.length > 16 ||
5338
+ scheduled.some((scheduledCandidate) =>
5339
+ rejected.some(
5340
+ (rejectedCandidate) =>
5341
+ rejectedCandidate.candidateId === scheduledCandidate.candidateId,
5342
+ ),
5343
+ )
5344
+ ) {
5345
+ throw new Error("Context swap result candidate partitions are invalid.");
5346
+ }
5347
+ const note = stringFromSql(raw.note, "context swap note");
5348
+ return immutableRecord({
5349
+ kind: "context_maintenance" as const,
5350
+ ok: true as const,
5351
+ operation,
5352
+ scheduled: Object.freeze(scheduled),
5353
+ rejected,
5354
+ note,
5355
+ });
5356
+ }
5357
+
5358
+ function decodeContextSwapRejected(value: unknown): readonly {
5359
+ readonly candidateId: MessageId;
5360
+ readonly reason: string;
5361
+ }[] {
5362
+ if (!Array.isArray(value) || value.length > 16) {
5363
+ throw new Error("Context swap rejected candidates must be an array of at most 16.");
5364
+ }
5365
+ const rejected = value.map((entry, index) => {
5366
+ const candidate = recordFromSql(entry, `rejected context candidate ${index}`);
5367
+ assertObjectKeys(
5368
+ candidate,
5369
+ ["candidateId", "reason"],
5370
+ ["candidateId", "reason"],
5371
+ `rejected context candidate ${index}`,
5372
+ );
5373
+ const reason = stringFromSql(
5374
+ candidate.reason,
5375
+ `rejected context candidate ${index} reason`,
5376
+ );
5377
+ if (!/^[a-z][a-z0-9_]{0,79}$/.test(reason)) {
5378
+ throw new Error(`Rejected context candidate ${index} reason is invalid.`);
5379
+ }
5380
+ return immutableRecord({
5381
+ candidateId: parseMessageId(
5382
+ stringFromSql(candidate.candidateId, `rejected candidate ${index} ID`),
5383
+ ),
5384
+ reason,
5385
+ });
5386
+ });
5387
+ if (
5388
+ new Set(rejected.map((candidate) => candidate.candidateId)).size !== rejected.length
5389
+ ) {
5390
+ throw new Error("Context swap rejected candidate IDs must be unique.");
5391
+ }
5392
+ return Object.freeze(rejected);
5393
+ }
5394
+
5120
5395
  function decodeStoredViewImageRawResult(
5121
5396
  raw: Record<string, unknown>,
5122
5397
  ): Extract<ToolRawResult, { kind: "view_image" }> {
@@ -6430,6 +6705,31 @@ function numberFromJson(value: unknown, name: string): number {
6430
6705
  return value as number;
6431
6706
  }
6432
6707
 
6708
+ function safeJsonInteger(value: unknown, name: string): number {
6709
+ if (!Number.isSafeInteger(value)) {
6710
+ throw new Error(`${name} must be a safe integer.`);
6711
+ }
6712
+ return value as number;
6713
+ }
6714
+
6715
+ function nonNegativeJsonInteger(value: unknown, name: string): number {
6716
+ const number = safeJsonInteger(value, name);
6717
+ if (number < 0) throw new Error(`${name} must be non-negative.`);
6718
+ return number;
6719
+ }
6720
+
6721
+ function positiveJsonInteger(value: unknown, name: string): number {
6722
+ const number = safeJsonInteger(value, name);
6723
+ if (number < 1) throw new Error(`${name} must be positive.`);
6724
+ return number;
6725
+ }
6726
+
6727
+ function nonEmptyStringFromJson(value: unknown, name: string): string {
6728
+ const text = stringFromSql(value, name);
6729
+ if (text.trim() === "") throw new Error(`${name} must not be empty.`);
6730
+ return text;
6731
+ }
6732
+
6433
6733
  function enumFromSql<const T extends readonly string[]>(
6434
6734
  value: unknown,
6435
6735
  values: T,
@@ -0,0 +1,266 @@
1
+ import { throwIfTurnCancelled } from "../agent/turn-cancellation";
2
+ import { parseMessageId, type MessageId } from "../ids/runtime-id";
3
+ import {
4
+ defineToolExecutor,
5
+ ToolExecutionFatalError,
6
+ type ContextMaintenanceHandle,
7
+ type ContextStatusRawResult,
8
+ type ContextSwapCandidatesRawResult,
9
+ type ContextSwapRawResult,
10
+ type ToolDefinition,
11
+ type ToolExecutionContext,
12
+ type ToolExecutor,
13
+ } from "./types";
14
+
15
+ const DEFAULT_CANDIDATE_LIMIT = 20;
16
+ const MAX_CANDIDATE_LIMIT = 50;
17
+ const MAX_SWAP_CANDIDATES = 16;
18
+
19
+ export const CONTEXT_STATUS_TOOL_DEFINITION: ToolDefinition = Object.freeze({
20
+ name: "ContextStatus",
21
+ description:
22
+ "Inspect the current model-input token pressure without changing context. When pressure is high or critical, use ContextSwapCandidates to review eligible historical tool observations before choosing what to swap.",
23
+ parameters: {
24
+ type: "object",
25
+ additionalProperties: false,
26
+ properties: {},
27
+ },
28
+ });
29
+
30
+ export const CONTEXT_SWAP_CANDIDATES_TOOL_DEFINITION: ToolDefinition = Object.freeze({
31
+ name: "ContextSwapCandidates",
32
+ description:
33
+ "List currently eligible historical tool observations that can be replaced by compact Recall-backed placeholders. Results are ordered oldest first. Use candidate IDs with ContextSwap; listing does not change context.",
34
+ parameters: {
35
+ type: "object",
36
+ additionalProperties: false,
37
+ properties: {
38
+ limit: {
39
+ type: "integer",
40
+ minimum: 1,
41
+ maximum: MAX_CANDIDATE_LIMIT,
42
+ default: DEFAULT_CANDIDATE_LIMIT,
43
+ },
44
+ offset: {
45
+ type: "integer",
46
+ minimum: 0,
47
+ default: 0,
48
+ },
49
+ },
50
+ },
51
+ });
52
+
53
+ export const CONTEXT_SWAP_TOOL_DEFINITION: ToolDefinition = Object.freeze({
54
+ name: "ContextSwap",
55
+ description:
56
+ "Schedule selected ContextSwapCandidates for replacement by compact Recall-backed placeholders. The swap runs after this iteration's tool frame closes and preserves canonical history for RecallGet. Pass only candidate IDs returned by ContextSwapCandidates.",
57
+ parameters: {
58
+ type: "object",
59
+ additionalProperties: false,
60
+ properties: {
61
+ candidate_ids: {
62
+ type: "array",
63
+ minItems: 1,
64
+ maxItems: MAX_SWAP_CANDIDATES,
65
+ uniqueItems: true,
66
+ items: {
67
+ type: "string",
68
+ pattern:
69
+ "^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$",
70
+ },
71
+ },
72
+ },
73
+ required: ["candidate_ids"],
74
+ },
75
+ });
76
+
77
+ export const CONTEXT_MAINTENANCE_TOOL_DEFINITIONS: readonly ToolDefinition[] =
78
+ Object.freeze([
79
+ CONTEXT_STATUS_TOOL_DEFINITION,
80
+ CONTEXT_SWAP_CANDIDATES_TOOL_DEFINITION,
81
+ CONTEXT_SWAP_TOOL_DEFINITION,
82
+ ]);
83
+
84
+ export function createContextStatusToolExecutor(): ToolExecutor {
85
+ return defineToolExecutor("context_maintenance", {
86
+ definition: CONTEXT_STATUS_TOOL_DEFINITION,
87
+ async execute(args, _call, context): Promise<ContextStatusRawResult> {
88
+ throwIfTurnCancelled(context.signal);
89
+ const parsed = parseEmptyArgs(args, "ContextStatus");
90
+ if (!parsed.ok) {
91
+ return { ok: false, operation: "status", error: parsed.error };
92
+ }
93
+ const result = await requireContextMaintenance(context).status(_call);
94
+ throwIfTurnCancelled(context.signal);
95
+ return result;
96
+ },
97
+ });
98
+ }
99
+
100
+ export function createContextSwapCandidatesToolExecutor(): ToolExecutor {
101
+ return defineToolExecutor("context_maintenance", {
102
+ definition: CONTEXT_SWAP_CANDIDATES_TOOL_DEFINITION,
103
+ async execute(args, call, context): Promise<ContextSwapCandidatesRawResult> {
104
+ throwIfTurnCancelled(context.signal);
105
+ const parsed = parseCandidatePageArgs(args);
106
+ if (!parsed.ok) {
107
+ return { ok: false, operation: "candidates", error: parsed.error };
108
+ }
109
+ const result = await requireContextMaintenance(context).candidates(call, {
110
+ limit: parsed.limit,
111
+ offset: parsed.offset,
112
+ });
113
+ throwIfTurnCancelled(context.signal);
114
+ return result;
115
+ },
116
+ });
117
+ }
118
+
119
+ export function createContextSwapToolExecutor(): ToolExecutor {
120
+ return defineToolExecutor("context_maintenance", {
121
+ definition: CONTEXT_SWAP_TOOL_DEFINITION,
122
+ async execute(args, call, context): Promise<ContextSwapRawResult> {
123
+ throwIfTurnCancelled(context.signal);
124
+ const parsed = parseSwapArgs(args);
125
+ if (!parsed.ok) {
126
+ return {
127
+ ok: false,
128
+ operation: "swap",
129
+ scheduled: [],
130
+ rejected: [],
131
+ error: parsed.error,
132
+ };
133
+ }
134
+ const result = await requireContextMaintenance(context).swap(call, {
135
+ candidateIds: parsed.candidateIds,
136
+ });
137
+ throwIfTurnCancelled(context.signal);
138
+ return result;
139
+ },
140
+ });
141
+ }
142
+
143
+ function requireContextMaintenance(
144
+ context: ToolExecutionContext,
145
+ ): ContextMaintenanceHandle {
146
+ if (context.contextMaintenance === undefined) {
147
+ throw new ToolExecutionFatalError(
148
+ "Context maintenance tools have no active runtime coordinator.",
149
+ );
150
+ }
151
+ return context.contextMaintenance;
152
+ }
153
+
154
+ function parseEmptyArgs(
155
+ args: unknown,
156
+ toolName: string,
157
+ ): { ok: true } | { ok: false; error: string } {
158
+ if (!isRecord(args)) {
159
+ return { ok: false, error: `${toolName} arguments must be an object.` };
160
+ }
161
+ const unexpected = Object.keys(args)[0];
162
+ return unexpected === undefined
163
+ ? { ok: true }
164
+ : {
165
+ ok: false,
166
+ error: `${toolName} received unexpected field: ${unexpected}.`,
167
+ };
168
+ }
169
+
170
+ function parseCandidatePageArgs(
171
+ args: unknown,
172
+ ): { ok: true; limit: number; offset: number } | { ok: false; error: string } {
173
+ if (!isRecord(args)) {
174
+ return {
175
+ ok: false,
176
+ error: "ContextSwapCandidates arguments must be an object.",
177
+ };
178
+ }
179
+ const unexpected = Object.keys(args).find(
180
+ (key) => key !== "limit" && key !== "offset",
181
+ );
182
+ if (unexpected !== undefined) {
183
+ return {
184
+ ok: false,
185
+ error: `ContextSwapCandidates received unexpected field: ${unexpected}.`,
186
+ };
187
+ }
188
+ const limit = args.limit ?? DEFAULT_CANDIDATE_LIMIT;
189
+ const offset = args.offset ?? 0;
190
+ if (!isIntegerInRange(limit, 1, MAX_CANDIDATE_LIMIT)) {
191
+ return {
192
+ ok: false,
193
+ error: `ContextSwapCandidates.limit must be an integer from 1 to ${MAX_CANDIDATE_LIMIT}.`,
194
+ };
195
+ }
196
+ if (!Number.isSafeInteger(offset) || (offset as number) < 0) {
197
+ return {
198
+ ok: false,
199
+ error: "ContextSwapCandidates.offset must be a non-negative integer.",
200
+ };
201
+ }
202
+ return { ok: true, limit: limit as number, offset: offset as number };
203
+ }
204
+
205
+ function parseSwapArgs(
206
+ args: unknown,
207
+ ): { ok: true; candidateIds: readonly MessageId[] } | { ok: false; error: string } {
208
+ if (!isRecord(args)) {
209
+ return { ok: false, error: "ContextSwap arguments must be an object." };
210
+ }
211
+ const unexpected = Object.keys(args).find((key) => key !== "candidate_ids");
212
+ if (unexpected !== undefined) {
213
+ return {
214
+ ok: false,
215
+ error: `ContextSwap received unexpected field: ${unexpected}.`,
216
+ };
217
+ }
218
+ if (
219
+ !Array.isArray(args.candidate_ids) ||
220
+ args.candidate_ids.length < 1 ||
221
+ args.candidate_ids.length > MAX_SWAP_CANDIDATES
222
+ ) {
223
+ return {
224
+ ok: false,
225
+ error: `ContextSwap.candidate_ids must contain 1 to ${MAX_SWAP_CANDIDATES} message IDs.`,
226
+ };
227
+ }
228
+
229
+ const candidateIds: MessageId[] = [];
230
+ const seen = new Set<string>();
231
+ for (const candidate of args.candidate_ids) {
232
+ if (typeof candidate !== "string") {
233
+ return {
234
+ ok: false,
235
+ error: "ContextSwap.candidate_ids must contain only message ID strings.",
236
+ };
237
+ }
238
+ let candidateId: MessageId;
239
+ try {
240
+ candidateId = parseMessageId(candidate);
241
+ } catch {
242
+ return {
243
+ ok: false,
244
+ error: "ContextSwap.candidate_ids contains an invalid message ID.",
245
+ };
246
+ }
247
+ if (!seen.has(candidateId)) {
248
+ seen.add(candidateId);
249
+ candidateIds.push(candidateId);
250
+ }
251
+ }
252
+ return { ok: true, candidateIds: Object.freeze(candidateIds) };
253
+ }
254
+
255
+ function isIntegerInRange(value: unknown, minimum: number, maximum: number): boolean {
256
+ return (
257
+ typeof value === "number" &&
258
+ Number.isSafeInteger(value) &&
259
+ value >= minimum &&
260
+ value <= maximum
261
+ );
262
+ }
263
+
264
+ function isRecord(value: unknown): value is Record<string, unknown> {
265
+ return typeof value === "object" && value !== null && !Array.isArray(value);
266
+ }
@@ -1,6 +1,11 @@
1
1
  import { createBashToolExecutor } from "./bash";
2
2
  import { ShellTaskManager } from "./bash-task";
3
3
  import { createCwdState } from "./cwd-state";
4
+ import {
5
+ createContextStatusToolExecutor,
6
+ createContextSwapCandidatesToolExecutor,
7
+ createContextSwapToolExecutor,
8
+ } from "./context-maintenance";
4
9
  import { createDeleteToolExecutor } from "./delete";
5
10
  import { createEditToolExecutor } from "./edit";
6
11
  import { createGlobToolExecutor } from "./glob";
@@ -26,6 +31,7 @@ import type {
26
31
  } from "../agent/runtime-session";
27
32
  import type {
28
33
  FileSnapshotStore,
34
+ ContextMaintenanceHandle,
29
35
  ToolDefinition,
30
36
  ToolExecutionContext,
31
37
  ToolExecutor,
@@ -79,6 +85,7 @@ export class ToolRuntime {
79
85
  signal: AbortSignal,
80
86
  ): Promise<"allow" | "deny">;
81
87
  },
88
+ private readonly contextMaintenance?: ContextMaintenanceHandle,
82
89
  ) {}
83
90
 
84
91
  async execute(call: ToolCall, context: ToolExecutionContext): Promise<ToolRawResult> {
@@ -107,6 +114,9 @@ export class ToolRuntime {
107
114
  try {
108
115
  return await tool.execute(call.args, call, {
109
116
  ...context,
117
+ ...(this.contextMaintenance === undefined
118
+ ? {}
119
+ : { contextMaintenance: this.contextMaintenance }),
110
120
  ...(this.bashGuard === undefined
111
121
  ? {}
112
122
  : {
@@ -228,6 +238,9 @@ export function createDefaultTooling(options: {
228
238
  registry.register(
229
239
  createRecallGetToolExecutor({ historyReader: options.historyReader }),
230
240
  );
241
+ registry.register(createContextStatusToolExecutor());
242
+ registry.register(createContextSwapCandidatesToolExecutor());
243
+ registry.register(createContextSwapToolExecutor());
231
244
  if (options.memorySearch !== undefined) {
232
245
  registry.register(options.memorySearch);
233
246
  }
@@ -304,7 +317,11 @@ export function createDefaultTooling(options: {
304
317
 
305
318
  return {
306
319
  registry,
307
- runtime: new ToolRuntime(registry, options.bashGuard),
320
+ runtime: new ToolRuntime(
321
+ registry,
322
+ options.bashGuard,
323
+ options.runtimeSession.contextMaintenance,
324
+ ),
308
325
  snapshots,
309
326
  taskManager,
310
327
  ...(turnUndoManager === undefined ? {} : { turnUndoManager }),