tinker-agent 2.2.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 +36 -1
- package/README.md +18 -8
- package/package.json +1 -1
- package/src/agent/loop.ts +1 -1
- package/src/agent/runtime-session.ts +279 -28
- package/src/agent/session-ledger.ts +10 -3
- package/src/cli/config.ts +8 -12
- package/src/cli/tui-runner.tsx +3 -1
- 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/image/image-asset-store.ts +21 -8
- package/src/memory/contracts.ts +31 -0
- package/src/memory/memory-coordinator.ts +7 -0
- package/src/memory/memory-extractor.ts +26 -5
- package/src/model/fake-model-client.ts +7 -0
- package/src/model/model-client.ts +11 -0
- package/src/model/openai-chat-model-client.ts +7 -1
- package/src/model/openai-responses-model-client.ts +8 -1
- package/src/observation/observation-builder.ts +36 -0
- package/src/session/resume-projection.ts +8 -6
- package/src/session/session-catalog.ts +18 -3
- package/src/session/session-last-response-reader.ts +10 -6
- package/src/session/session-store.ts +340 -8
- package/src/session/workspace-storage.ts +84 -0
- package/src/tools/bash-task.ts +14 -12
- package/src/tools/context-maintenance.ts +266 -0
- package/src/tools/registry.ts +20 -1
- package/src/tools/types.ts +83 -1
- package/src/tui/event-store.ts +15 -0
|
@@ -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
|
+
}
|
package/src/tools/registry.ts
CHANGED
|
@@ -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
|
: {
|
|
@@ -153,6 +163,7 @@ export function createDefaultTooling(options: {
|
|
|
153
163
|
workspaceRoot: string;
|
|
154
164
|
runtimeSession: RuntimeSessionContext;
|
|
155
165
|
historyReader: SessionHistoryReader;
|
|
166
|
+
homeRoot?: string;
|
|
156
167
|
maxReadContentBytes?: number;
|
|
157
168
|
exaApiKey?: string;
|
|
158
169
|
webFetchRefiner?: Refiner;
|
|
@@ -187,6 +198,7 @@ export function createDefaultTooling(options: {
|
|
|
187
198
|
cwdState,
|
|
188
199
|
runtimeSession,
|
|
189
200
|
stopGraceMs: options.taskStopGraceMs,
|
|
201
|
+
...(options.homeRoot === undefined ? {} : { homeRoot: options.homeRoot }),
|
|
190
202
|
});
|
|
191
203
|
|
|
192
204
|
registry.register(
|
|
@@ -226,6 +238,9 @@ export function createDefaultTooling(options: {
|
|
|
226
238
|
registry.register(
|
|
227
239
|
createRecallGetToolExecutor({ historyReader: options.historyReader }),
|
|
228
240
|
);
|
|
241
|
+
registry.register(createContextStatusToolExecutor());
|
|
242
|
+
registry.register(createContextSwapCandidatesToolExecutor());
|
|
243
|
+
registry.register(createContextSwapToolExecutor());
|
|
229
244
|
if (options.memorySearch !== undefined) {
|
|
230
245
|
registry.register(options.memorySearch);
|
|
231
246
|
}
|
|
@@ -302,7 +317,11 @@ export function createDefaultTooling(options: {
|
|
|
302
317
|
|
|
303
318
|
return {
|
|
304
319
|
registry,
|
|
305
|
-
runtime: new ToolRuntime(
|
|
320
|
+
runtime: new ToolRuntime(
|
|
321
|
+
registry,
|
|
322
|
+
options.bashGuard,
|
|
323
|
+
options.runtimeSession.contextMaintenance,
|
|
324
|
+
),
|
|
306
325
|
snapshots,
|
|
307
326
|
taskManager,
|
|
308
327
|
...(turnUndoManager === undefined ? {} : { turnUndoManager }),
|
package/src/tools/types.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { ToolCall } from "../agent/types";
|
|
2
2
|
import type { ImageAssetRef } from "../image/image-types";
|
|
3
|
-
import type { SessionId } from "../ids/runtime-id";
|
|
3
|
+
import type { MessageId, SessionId } from "../ids/runtime-id";
|
|
4
|
+
import type { ContextUsageSource } from "../model/model-request-preflight";
|
|
4
5
|
import type {
|
|
5
6
|
RecallGetPage,
|
|
6
7
|
RecallSearchFilters,
|
|
@@ -310,6 +311,85 @@ export type RecallGetRawResult =
|
|
|
310
311
|
|
|
311
312
|
export type RecallRawResult = RecallSearchRawResult | RecallGetRawResult;
|
|
312
313
|
|
|
314
|
+
export type ContextMaintenancePressure = "normal" | "high" | "critical";
|
|
315
|
+
|
|
316
|
+
export type ContextSwapCandidate = {
|
|
317
|
+
candidateId: MessageId;
|
|
318
|
+
label: string;
|
|
319
|
+
ordinal: number;
|
|
320
|
+
savingsBytes: number;
|
|
321
|
+
};
|
|
322
|
+
|
|
323
|
+
export type ContextSwapScheduledCandidate = {
|
|
324
|
+
candidateId: MessageId;
|
|
325
|
+
savingsBytes: number;
|
|
326
|
+
};
|
|
327
|
+
|
|
328
|
+
export type ContextSwapRejectedCandidate = {
|
|
329
|
+
candidateId: MessageId;
|
|
330
|
+
reason: string;
|
|
331
|
+
};
|
|
332
|
+
|
|
333
|
+
type ContextMaintenanceFailure<TOperation extends "status" | "candidates" | "swap"> = {
|
|
334
|
+
ok: false;
|
|
335
|
+
operation: TOperation;
|
|
336
|
+
error: string;
|
|
337
|
+
};
|
|
338
|
+
|
|
339
|
+
export type ContextStatusRawResult =
|
|
340
|
+
| {
|
|
341
|
+
ok: true;
|
|
342
|
+
operation: "status";
|
|
343
|
+
usedInputTokens: number;
|
|
344
|
+
inputBudgetTokens: number;
|
|
345
|
+
pressure: ContextMaintenancePressure;
|
|
346
|
+
triggerTokens: number;
|
|
347
|
+
source: ContextUsageSource;
|
|
348
|
+
}
|
|
349
|
+
| ContextMaintenanceFailure<"status">;
|
|
350
|
+
|
|
351
|
+
export type ContextSwapCandidatesRawResult =
|
|
352
|
+
| {
|
|
353
|
+
ok: true;
|
|
354
|
+
operation: "candidates";
|
|
355
|
+
total: number;
|
|
356
|
+
candidates: readonly ContextSwapCandidate[];
|
|
357
|
+
}
|
|
358
|
+
| ContextMaintenanceFailure<"candidates">;
|
|
359
|
+
|
|
360
|
+
export type ContextSwapRawResult =
|
|
361
|
+
| {
|
|
362
|
+
ok: true;
|
|
363
|
+
operation: "swap";
|
|
364
|
+
scheduled: readonly ContextSwapScheduledCandidate[];
|
|
365
|
+
rejected: readonly ContextSwapRejectedCandidate[];
|
|
366
|
+
note: string;
|
|
367
|
+
}
|
|
368
|
+
| {
|
|
369
|
+
ok: false;
|
|
370
|
+
operation: "swap";
|
|
371
|
+
scheduled: readonly ContextSwapScheduledCandidate[];
|
|
372
|
+
rejected: readonly ContextSwapRejectedCandidate[];
|
|
373
|
+
error?: string;
|
|
374
|
+
};
|
|
375
|
+
|
|
376
|
+
export type ContextMaintenanceRawResult =
|
|
377
|
+
| ContextStatusRawResult
|
|
378
|
+
| ContextSwapCandidatesRawResult
|
|
379
|
+
| ContextSwapRawResult;
|
|
380
|
+
|
|
381
|
+
export type ContextMaintenanceHandle = {
|
|
382
|
+
status(call: ToolCall): Promise<ContextStatusRawResult>;
|
|
383
|
+
candidates(
|
|
384
|
+
call: ToolCall,
|
|
385
|
+
input: { readonly limit: number; readonly offset: number },
|
|
386
|
+
): Promise<ContextSwapCandidatesRawResult>;
|
|
387
|
+
swap(
|
|
388
|
+
call: ToolCall,
|
|
389
|
+
input: { readonly candidateIds: readonly MessageId[] },
|
|
390
|
+
): Promise<ContextSwapRawResult>;
|
|
391
|
+
};
|
|
392
|
+
|
|
313
393
|
export type MemorySearchRawResult =
|
|
314
394
|
| {
|
|
315
395
|
ok: true;
|
|
@@ -425,6 +505,7 @@ export type ToolRawResultByKind = {
|
|
|
425
505
|
web_search: WebSearchRawResult;
|
|
426
506
|
web_fetch: WebFetchRawResult;
|
|
427
507
|
recall: RecallRawResult;
|
|
508
|
+
context_maintenance: ContextMaintenanceRawResult;
|
|
428
509
|
memory_search: MemorySearchRawResult;
|
|
429
510
|
memory_get: MemoryGetRawResult;
|
|
430
511
|
wait: WaitRawResult;
|
|
@@ -472,6 +553,7 @@ export function defineToolExecutor<TKind extends ToolRawResultKind>(
|
|
|
472
553
|
|
|
473
554
|
export type ToolExecutionContext = {
|
|
474
555
|
signal: AbortSignal;
|
|
556
|
+
contextMaintenance?: ContextMaintenanceHandle;
|
|
475
557
|
confirmBashCommand?: (request: {
|
|
476
558
|
command: string;
|
|
477
559
|
reason: string;
|
package/src/tui/event-store.ts
CHANGED
|
@@ -883,6 +883,19 @@ function toolRawResultSummary(name: string, args: unknown, raw: ToolRawResult):
|
|
|
883
883
|
return raw.mode === "search"
|
|
884
884
|
? `${base} -> ${raw.page.hits.length} historical match${raw.page.hits.length === 1 ? "" : "es"}`
|
|
885
885
|
: `${base} -> ${raw.page.returnedBytes} historical bytes`;
|
|
886
|
+
case "context_maintenance":
|
|
887
|
+
if (!raw.ok) {
|
|
888
|
+
return raw.operation === "swap"
|
|
889
|
+
? `${base} -> 0 scheduled, ${raw.rejected.length} rejected`
|
|
890
|
+
: base;
|
|
891
|
+
}
|
|
892
|
+
if (raw.operation === "status") {
|
|
893
|
+
return `${base} -> ${raw.pressure}, ${raw.usedInputTokens}/${raw.inputBudgetTokens} tokens`;
|
|
894
|
+
}
|
|
895
|
+
if (raw.operation === "candidates") {
|
|
896
|
+
return `${base} -> ${raw.candidates.length}/${raw.total} candidates`;
|
|
897
|
+
}
|
|
898
|
+
return `${base} -> ${raw.scheduled.length} scheduled, ${raw.rejected.length} rejected`;
|
|
886
899
|
case "memory_search":
|
|
887
900
|
if (!raw.ok) {
|
|
888
901
|
return base;
|
|
@@ -980,6 +993,7 @@ function toolRawResultBashDetail(raw: ToolRawResult): Pick<TimelineItem, "bash">
|
|
|
980
993
|
case "web_search":
|
|
981
994
|
case "web_fetch":
|
|
982
995
|
case "recall":
|
|
996
|
+
case "context_maintenance":
|
|
983
997
|
case "memory_search":
|
|
984
998
|
case "memory_get":
|
|
985
999
|
case "wait":
|
|
@@ -1021,6 +1035,7 @@ function toolRawResultDiff(
|
|
|
1021
1035
|
case "web_search":
|
|
1022
1036
|
case "web_fetch":
|
|
1023
1037
|
case "recall":
|
|
1038
|
+
case "context_maintenance":
|
|
1024
1039
|
case "memory_search":
|
|
1025
1040
|
case "memory_get":
|
|
1026
1041
|
case "wait":
|