@shanepadgett/tau-agent 0.20.2 → 0.22.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/README.md +18 -0
- package/docs/extending-tau-agent.md +34 -2
- package/extensions/context-pruning/README.md +18 -114
- package/extensions/context-pruning/index.ts +72 -74
- package/extensions/context-pruning/projection.ts +38 -96
- package/extensions/context-pruning/prune.ts +135 -287
- package/extensions/context-pruning/render.ts +95 -35
- package/extensions/context-pruning/settings.ts +14 -15
- package/extensions/explore/read-cache.ts +31 -7
- package/extensions/handoff/README.md +7 -0
- package/extensions/handoff/index.ts +145 -0
- package/extensions/handoff/model.ts +71 -0
- package/extensions/image-gen/index.ts +24 -133
- package/extensions/subagent/index.ts +3 -1
- package/extensions/tau-help/help.md +5 -1
- package/package.json +9 -2
- package/schemas/tau.schema.json +15 -12
- package/shared/context-pruning-state.ts +69 -273
- package/shared/model-fallback/index.ts +1 -1
- package/{extensions/image-gen → src/image-generation}/client.ts +30 -20
- package/src/image-generation/index.ts +224 -0
- package/src/index.ts +7 -0
- package/extensions/context-pruning/file-evidence.ts +0 -265
- /package/{extensions/image-gen → src/image-generation}/constants.ts +0 -0
|
@@ -1,40 +1,38 @@
|
|
|
1
1
|
import type { SessionEntry } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
|
|
3
3
|
const CONTEXT_PRUNE_TOOL = "context_prune";
|
|
4
|
-
const AUTOREAD_MESSAGE_TYPE = "tau.autoread";
|
|
5
4
|
|
|
6
|
-
export interface
|
|
5
|
+
export interface ContextPruneRefreshedFileV2 {
|
|
7
6
|
path: string;
|
|
8
7
|
rowId: string;
|
|
9
8
|
servedHash: string;
|
|
9
|
+
autoreadDetails: Record<string, unknown>;
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
-
export interface
|
|
12
|
+
export interface ContextPruneDeferredFileV2 {
|
|
13
13
|
path: string;
|
|
14
14
|
reason: string;
|
|
15
15
|
relevantWhen: string;
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
-
export interface
|
|
19
|
-
v:
|
|
20
|
-
status: "applied" | "skipped";
|
|
18
|
+
export interface ContextPruneDetailsV2 {
|
|
19
|
+
v: 2;
|
|
21
20
|
anchorToolCallId: string;
|
|
22
|
-
|
|
23
|
-
|
|
21
|
+
prunedToolCallIds: string[];
|
|
22
|
+
prunedAutoreadRowIds: string[];
|
|
24
23
|
retainedToolCallIds: string[];
|
|
25
24
|
retainedAutoreadRowIds: string[];
|
|
26
|
-
refreshedFiles:
|
|
27
|
-
deferredFiles:
|
|
28
|
-
|
|
29
|
-
tokensAfter: number;
|
|
30
|
-
tokensReclaimed: number;
|
|
25
|
+
refreshedFiles: ContextPruneRefreshedFileV2[];
|
|
26
|
+
deferredFiles: ContextPruneDeferredFileV2[];
|
|
27
|
+
warnings: string[];
|
|
31
28
|
}
|
|
32
29
|
|
|
33
30
|
export interface ActiveContextPruningState {
|
|
34
31
|
latestAnchorToolCallId: string | undefined;
|
|
32
|
+
retainedToolCallIds: ReadonlySet<string>;
|
|
35
33
|
prunedToolCallIds: ReadonlySet<string>;
|
|
36
34
|
prunedAutoreadRowIds: ReadonlySet<string>;
|
|
37
|
-
deferredFiles: readonly
|
|
35
|
+
deferredFiles: readonly ContextPruneDeferredFileV2[];
|
|
38
36
|
}
|
|
39
37
|
|
|
40
38
|
const RUNTIME_ENABLED_KEY = Symbol.for("@earendil-works/tau-agent/context-pruning-enabled");
|
|
@@ -48,67 +46,35 @@ function isContextPruningEnabled(): boolean {
|
|
|
48
46
|
return runtimeState[RUNTIME_ENABLED_KEY] === true;
|
|
49
47
|
}
|
|
50
48
|
|
|
51
|
-
export function
|
|
52
|
-
if (
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
"anchorToolCallId",
|
|
57
|
-
"newlyPrunedToolCallIds",
|
|
58
|
-
"newlyPrunedAutoreadRowIds",
|
|
59
|
-
"retainedToolCallIds",
|
|
60
|
-
"retainedAutoreadRowIds",
|
|
61
|
-
"refreshedFiles",
|
|
62
|
-
"deferredFiles",
|
|
63
|
-
"tokensBefore",
|
|
64
|
-
"tokensAfter",
|
|
65
|
-
"tokensReclaimed",
|
|
66
|
-
])
|
|
67
|
-
)
|
|
68
|
-
return undefined;
|
|
69
|
-
if (value.v !== 1 || (value.status !== "applied" && value.status !== "skipped")) return undefined;
|
|
70
|
-
if (!isNonEmptyString(value.anchorToolCallId)) return undefined;
|
|
71
|
-
|
|
72
|
-
const newlyPrunedToolCallIds = parseUniqueStrings(value.newlyPrunedToolCallIds);
|
|
73
|
-
const newlyPrunedAutoreadRowIds = parseUniqueStrings(value.newlyPrunedAutoreadRowIds);
|
|
74
|
-
const retainedToolCallIds = parseUniqueStrings(value.retainedToolCallIds);
|
|
49
|
+
export function parseContextPruneDetailsV2(value: unknown): ContextPruneDetailsV2 | undefined {
|
|
50
|
+
if (!isRecord(value) || value.v !== 2 || !isNonEmptyString(value.anchorToolCallId)) return undefined;
|
|
51
|
+
const prunedToolCallIds = parseUniqueStrings(value.prunedToolCallIds);
|
|
52
|
+
const prunedAutoreadRowIds = parseUniqueStrings(value.prunedAutoreadRowIds);
|
|
53
|
+
const retainedToolCallIds = parseUniqueStrings(value.retainedToolCallIds, true);
|
|
75
54
|
const retainedAutoreadRowIds = parseUniqueStrings(value.retainedAutoreadRowIds);
|
|
76
55
|
const refreshedFiles = parseRefreshedFiles(value.refreshedFiles);
|
|
77
56
|
const deferredFiles = parseDeferredFiles(value.deferredFiles);
|
|
57
|
+
const warnings = parseStrings(value.warnings, true);
|
|
78
58
|
if (
|
|
79
|
-
!
|
|
80
|
-
!
|
|
59
|
+
!prunedToolCallIds ||
|
|
60
|
+
!prunedAutoreadRowIds ||
|
|
81
61
|
!retainedToolCallIds ||
|
|
82
62
|
!retainedAutoreadRowIds ||
|
|
83
63
|
!refreshedFiles ||
|
|
84
|
-
!deferredFiles
|
|
85
|
-
|
|
86
|
-
return undefined;
|
|
87
|
-
if (
|
|
88
|
-
!isNonNegativeFinite(value.tokensBefore) ||
|
|
89
|
-
!isNonNegativeFinite(value.tokensAfter) ||
|
|
90
|
-
!isFiniteNumber(value.tokensReclaimed) ||
|
|
91
|
-
value.tokensBefore - value.tokensAfter !== value.tokensReclaimed
|
|
64
|
+
!deferredFiles ||
|
|
65
|
+
!warnings
|
|
92
66
|
)
|
|
93
67
|
return undefined;
|
|
94
|
-
if (hasOverlap(newlyPrunedToolCallIds, retainedToolCallIds)) return undefined;
|
|
95
|
-
if (hasOverlap(newlyPrunedAutoreadRowIds, retainedAutoreadRowIds)) return undefined;
|
|
96
|
-
if (value.status === "skipped" && (newlyPrunedToolCallIds.length > 0 || newlyPrunedAutoreadRowIds.length > 0))
|
|
97
|
-
return undefined;
|
|
98
|
-
|
|
99
68
|
return {
|
|
100
|
-
v:
|
|
101
|
-
status: value.status,
|
|
69
|
+
v: 2,
|
|
102
70
|
anchorToolCallId: value.anchorToolCallId,
|
|
103
|
-
|
|
104
|
-
|
|
71
|
+
prunedToolCallIds,
|
|
72
|
+
prunedAutoreadRowIds,
|
|
105
73
|
retainedToolCallIds,
|
|
106
74
|
retainedAutoreadRowIds,
|
|
107
75
|
refreshedFiles,
|
|
108
76
|
deferredFiles,
|
|
109
|
-
|
|
110
|
-
tokensAfter: value.tokensAfter,
|
|
111
|
-
tokensReclaimed: value.tokensReclaimed,
|
|
77
|
+
warnings,
|
|
112
78
|
};
|
|
113
79
|
}
|
|
114
80
|
|
|
@@ -118,233 +84,71 @@ export function replayContextPruningState(
|
|
|
118
84
|
): ActiveContextPruningState {
|
|
119
85
|
const prunedToolCallIds = new Set<string>();
|
|
120
86
|
const prunedAutoreadRowIds = new Set<string>();
|
|
121
|
-
|
|
122
|
-
|
|
87
|
+
let latestAnchorToolCallId: string | undefined;
|
|
88
|
+
let retainedToolCallIds = new Set<string>();
|
|
89
|
+
let deferredFiles: readonly ContextPruneDeferredFileV2[] = [];
|
|
90
|
+
if (enabled) {
|
|
91
|
+
for (const entry of branch) {
|
|
92
|
+
if (entry.type !== "message" || entry.message.role !== "toolResult") continue;
|
|
93
|
+
const result = entry.message;
|
|
94
|
+
if (result.toolName !== CONTEXT_PRUNE_TOOL) continue;
|
|
95
|
+
const details = parseContextPruneDetailsV2(result.details);
|
|
96
|
+
if (!details || details.anchorToolCallId !== result.toolCallId) continue;
|
|
97
|
+
for (const id of details.prunedToolCallIds) prunedToolCallIds.add(id);
|
|
98
|
+
for (const id of details.prunedAutoreadRowIds) prunedAutoreadRowIds.add(id);
|
|
99
|
+
latestAnchorToolCallId = details.anchorToolCallId;
|
|
100
|
+
retainedToolCallIds = new Set(details.retainedToolCallIds);
|
|
101
|
+
deferredFiles = details.deferredFiles;
|
|
102
|
+
}
|
|
103
|
+
for (const id of retainedToolCallIds) prunedToolCallIds.delete(id);
|
|
104
|
+
}
|
|
105
|
+
return {
|
|
106
|
+
latestAnchorToolCallId,
|
|
107
|
+
retainedToolCallIds,
|
|
123
108
|
prunedToolCallIds,
|
|
124
109
|
prunedAutoreadRowIds,
|
|
125
|
-
deferredFiles
|
|
110
|
+
deferredFiles,
|
|
126
111
|
};
|
|
127
|
-
if (!enabled) return state;
|
|
128
|
-
|
|
129
|
-
const toolCalls = new Map<string, { count: number; name: string; index: number }>();
|
|
130
|
-
const toolResults = new Map<string, { count: number; name: string; index: number }>();
|
|
131
|
-
const autoreads = new Map<
|
|
132
|
-
string,
|
|
133
|
-
{ count: number; path: string; servedHash: string; source: string; batchId: string; index: number }
|
|
134
|
-
>();
|
|
135
|
-
for (let index = 0; index < branch.length; index += 1) {
|
|
136
|
-
const entry = branch[index];
|
|
137
|
-
if (entry.type === "message") {
|
|
138
|
-
const message = entry.message;
|
|
139
|
-
if (message.role === "assistant") {
|
|
140
|
-
for (const block of message.content) {
|
|
141
|
-
if (block.type !== "toolCall") continue;
|
|
142
|
-
const current = toolCalls.get(block.id);
|
|
143
|
-
toolCalls.set(block.id, { count: (current?.count ?? 0) + 1, name: block.name, index });
|
|
144
|
-
}
|
|
145
|
-
} else if (message.role === "toolResult") {
|
|
146
|
-
const current = toolResults.get(message.toolCallId);
|
|
147
|
-
toolResults.set(message.toolCallId, {
|
|
148
|
-
count: (current?.count ?? 0) + 1,
|
|
149
|
-
name: message.toolName,
|
|
150
|
-
index,
|
|
151
|
-
});
|
|
152
|
-
}
|
|
153
|
-
continue;
|
|
154
|
-
}
|
|
155
|
-
if (entry.type !== "custom_message" || entry.customType !== AUTOREAD_MESSAGE_TYPE) continue;
|
|
156
|
-
const autoread = parseAutoreadReference(entry.details);
|
|
157
|
-
if (!autoread) continue;
|
|
158
|
-
const current = autoreads.get(autoread.rowId);
|
|
159
|
-
autoreads.set(autoread.rowId, {
|
|
160
|
-
count: (current?.count ?? 0) + 1,
|
|
161
|
-
path: autoread.path,
|
|
162
|
-
servedHash: autoread.servedHash,
|
|
163
|
-
source: autoread.source,
|
|
164
|
-
batchId: autoread.batchId,
|
|
165
|
-
index,
|
|
166
|
-
});
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
for (let resultIndex = 0; resultIndex < branch.length; resultIndex += 1) {
|
|
170
|
-
const entry = branch[resultIndex];
|
|
171
|
-
if (entry.type !== "message" || entry.message.role !== "toolResult") continue;
|
|
172
|
-
const result = entry.message;
|
|
173
|
-
if (result.toolName !== CONTEXT_PRUNE_TOOL) continue;
|
|
174
|
-
const details = parseContextPruneDetailsV1(result.details);
|
|
175
|
-
if (!details || details.status !== "applied" || details.anchorToolCallId !== result.toolCallId) continue;
|
|
176
|
-
if (!isCompleteExchange(details.anchorToolCallId, CONTEXT_PRUNE_TOOL, toolCalls, toolResults)) continue;
|
|
177
|
-
const anchorCallIndex = toolCalls.get(details.anchorToolCallId)?.index;
|
|
178
|
-
if (anchorCallIndex === undefined || anchorCallIndex >= resultIndex) continue;
|
|
179
|
-
if (details.newlyPrunedToolCallIds.includes(details.anchorToolCallId)) continue;
|
|
180
|
-
if (details.retainedToolCallIds.includes(details.anchorToolCallId)) continue;
|
|
181
|
-
if (
|
|
182
|
-
!allCompleteExchanges(details.newlyPrunedToolCallIds, toolCalls, toolResults) ||
|
|
183
|
-
!allCompleteExchanges(details.retainedToolCallIds, toolCalls, toolResults) ||
|
|
184
|
-
!allAutoreads(details.newlyPrunedAutoreadRowIds, autoreads) ||
|
|
185
|
-
!allAutoreads(details.retainedAutoreadRowIds, autoreads)
|
|
186
|
-
)
|
|
187
|
-
continue;
|
|
188
|
-
if (
|
|
189
|
-
details.newlyPrunedToolCallIds.some((id) => !exchangePrecedes(id, anchorCallIndex, toolCalls, toolResults)) ||
|
|
190
|
-
details.retainedToolCallIds.some((id) => !exchangePrecedes(id, anchorCallIndex, toolCalls, toolResults)) ||
|
|
191
|
-
details.newlyPrunedAutoreadRowIds.some((id) => {
|
|
192
|
-
const row = autoreads.get(id);
|
|
193
|
-
return row === undefined || row.index >= anchorCallIndex;
|
|
194
|
-
})
|
|
195
|
-
)
|
|
196
|
-
continue;
|
|
197
|
-
if (
|
|
198
|
-
details.newlyPrunedToolCallIds.some((id) => prunedToolCallIds.has(id)) ||
|
|
199
|
-
details.retainedToolCallIds.some((id) => prunedToolCallIds.has(id)) ||
|
|
200
|
-
details.newlyPrunedAutoreadRowIds.some((id) => prunedAutoreadRowIds.has(id)) ||
|
|
201
|
-
details.retainedAutoreadRowIds.some((id) => prunedAutoreadRowIds.has(id))
|
|
202
|
-
)
|
|
203
|
-
continue;
|
|
204
|
-
if (
|
|
205
|
-
details.refreshedFiles.some((file) => {
|
|
206
|
-
const row = autoreads.get(file.rowId);
|
|
207
|
-
return (
|
|
208
|
-
row?.count !== 1 ||
|
|
209
|
-
row.path !== file.path ||
|
|
210
|
-
row.servedHash !== file.servedHash ||
|
|
211
|
-
row.source !== "context-pruning" ||
|
|
212
|
-
row.batchId !== details.anchorToolCallId ||
|
|
213
|
-
!details.retainedAutoreadRowIds.includes(file.rowId)
|
|
214
|
-
);
|
|
215
|
-
})
|
|
216
|
-
)
|
|
217
|
-
continue;
|
|
218
|
-
const refreshedRowIds = new Set(details.refreshedFiles.map((file) => file.rowId));
|
|
219
|
-
if (
|
|
220
|
-
details.retainedAutoreadRowIds.some((id) => {
|
|
221
|
-
const row = autoreads.get(id);
|
|
222
|
-
return row === undefined || (row.index >= anchorCallIndex && !refreshedRowIds.has(id));
|
|
223
|
-
})
|
|
224
|
-
)
|
|
225
|
-
continue;
|
|
226
|
-
|
|
227
|
-
for (const id of details.newlyPrunedToolCallIds) prunedToolCallIds.add(id);
|
|
228
|
-
for (const id of details.newlyPrunedAutoreadRowIds) prunedAutoreadRowIds.add(id);
|
|
229
|
-
state.latestAnchorToolCallId = details.anchorToolCallId;
|
|
230
|
-
state.deferredFiles = details.deferredFiles;
|
|
231
|
-
}
|
|
232
|
-
return state;
|
|
233
112
|
}
|
|
234
113
|
|
|
235
|
-
function parseRefreshedFiles(value: unknown):
|
|
114
|
+
function parseRefreshedFiles(value: unknown): ContextPruneRefreshedFileV2[] | undefined {
|
|
236
115
|
if (!Array.isArray(value)) return undefined;
|
|
237
|
-
const files:
|
|
238
|
-
const rowIds = new Set<string>();
|
|
116
|
+
const files: ContextPruneRefreshedFileV2[] = [];
|
|
239
117
|
for (const item of value) {
|
|
240
|
-
if (!
|
|
241
|
-
if (!isNonEmptyString(item.
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
118
|
+
if (!isRecord(item) || !isNonEmptyString(item.path) || !isNonEmptyString(item.rowId)) return undefined;
|
|
119
|
+
if (!isNonEmptyString(item.servedHash) || !isRecord(item.autoreadDetails)) return undefined;
|
|
120
|
+
files.push({
|
|
121
|
+
path: item.path,
|
|
122
|
+
rowId: item.rowId,
|
|
123
|
+
servedHash: item.servedHash,
|
|
124
|
+
autoreadDetails: { ...item.autoreadDetails },
|
|
125
|
+
});
|
|
246
126
|
}
|
|
247
127
|
return files;
|
|
248
128
|
}
|
|
249
129
|
|
|
250
|
-
function parseDeferredFiles(value: unknown):
|
|
130
|
+
function parseDeferredFiles(value: unknown): ContextPruneDeferredFileV2[] | undefined {
|
|
251
131
|
if (!Array.isArray(value)) return undefined;
|
|
252
|
-
const files:
|
|
132
|
+
const files: ContextPruneDeferredFileV2[] = [];
|
|
253
133
|
for (const item of value) {
|
|
254
|
-
if (!
|
|
255
|
-
if (
|
|
256
|
-
return undefined;
|
|
134
|
+
if (!isRecord(item) || typeof item.path !== "string" || typeof item.reason !== "string") return undefined;
|
|
135
|
+
if (typeof item.relevantWhen !== "string") return undefined;
|
|
257
136
|
files.push({ path: item.path, reason: item.reason, relevantWhen: item.relevantWhen });
|
|
258
137
|
}
|
|
259
138
|
return files;
|
|
260
139
|
}
|
|
261
140
|
|
|
262
|
-
function parseUniqueStrings(value: unknown): string[] | undefined {
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
const seen = new Set<string>();
|
|
266
|
-
for (const item of value) {
|
|
267
|
-
if (!isNonEmptyString(item) || seen.has(item)) return undefined;
|
|
268
|
-
seen.add(item);
|
|
269
|
-
strings.push(item);
|
|
270
|
-
}
|
|
141
|
+
function parseUniqueStrings(value: unknown, allowEmpty = false): string[] | undefined {
|
|
142
|
+
const strings = parseStrings(value, allowEmpty);
|
|
143
|
+
if (!strings || new Set(strings).size !== strings.length) return undefined;
|
|
271
144
|
return strings;
|
|
272
145
|
}
|
|
273
146
|
|
|
274
|
-
function
|
|
275
|
-
value
|
|
276
|
-
): { rowId: string; path: string; servedHash: string; source: string; batchId: string } | undefined {
|
|
277
|
-
if (!isRecord(value) || value.status !== "read") return undefined;
|
|
278
|
-
if (
|
|
279
|
-
!isNonEmptyString(value.rowId) ||
|
|
280
|
-
!isNonEmptyString(value.path) ||
|
|
281
|
-
!isNonEmptyString(value.cwd) ||
|
|
282
|
-
!isNonEmptyString(value.source) ||
|
|
283
|
-
!isNonEmptyString(value.batchId)
|
|
284
|
-
)
|
|
147
|
+
function parseStrings(value: unknown, allowEmpty = false): string[] | undefined {
|
|
148
|
+
if (!Array.isArray(value) || !value.every((item) => typeof item === "string" && (allowEmpty || item.length > 0))) {
|
|
285
149
|
return undefined;
|
|
286
|
-
|
|
287
|
-
return
|
|
288
|
-
rowId: value.rowId,
|
|
289
|
-
path: value.path,
|
|
290
|
-
servedHash: value.readCache.servedHash,
|
|
291
|
-
source: value.source,
|
|
292
|
-
batchId: value.batchId,
|
|
293
|
-
};
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
function allCompleteExchanges(
|
|
297
|
-
ids: readonly string[],
|
|
298
|
-
calls: ReadonlyMap<string, { count: number; name: string; index: number }>,
|
|
299
|
-
results: ReadonlyMap<string, { count: number; name: string; index: number }>,
|
|
300
|
-
): boolean {
|
|
301
|
-
return ids.every((id) => {
|
|
302
|
-
const call = calls.get(id);
|
|
303
|
-
return call !== undefined && isCompleteExchange(id, call.name, calls, results);
|
|
304
|
-
});
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
function isCompleteExchange(
|
|
308
|
-
id: string,
|
|
309
|
-
name: string,
|
|
310
|
-
calls: ReadonlyMap<string, { count: number; name: string; index: number }>,
|
|
311
|
-
results: ReadonlyMap<string, { count: number; name: string; index: number }>,
|
|
312
|
-
): boolean {
|
|
313
|
-
const call = calls.get(id);
|
|
314
|
-
const result = results.get(id);
|
|
315
|
-
return (
|
|
316
|
-
call?.count === 1 &&
|
|
317
|
-
result?.count === 1 &&
|
|
318
|
-
call.name === name &&
|
|
319
|
-
result.name === name &&
|
|
320
|
-
call.index < result.index
|
|
321
|
-
);
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
function exchangePrecedes(
|
|
325
|
-
id: string,
|
|
326
|
-
anchorCallIndex: number,
|
|
327
|
-
calls: ReadonlyMap<string, { index: number }>,
|
|
328
|
-
results: ReadonlyMap<string, { index: number }>,
|
|
329
|
-
): boolean {
|
|
330
|
-
const call = calls.get(id);
|
|
331
|
-
const result = results.get(id);
|
|
332
|
-
return call !== undefined && result !== undefined && call.index < result.index && result.index < anchorCallIndex;
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
function allAutoreads(ids: readonly string[], autoreads: ReadonlyMap<string, { count: number }>): boolean {
|
|
336
|
-
return ids.every((id) => autoreads.get(id)?.count === 1);
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
function hasOverlap(left: readonly string[], right: readonly string[]): boolean {
|
|
340
|
-
const rightSet = new Set(right);
|
|
341
|
-
return left.some((item) => rightSet.has(item));
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
function hasExactKeys(value: unknown, keys: readonly string[]): value is Record<string, unknown> {
|
|
345
|
-
if (!isRecord(value)) return false;
|
|
346
|
-
const actual = Object.keys(value);
|
|
347
|
-
return actual.length === keys.length && keys.every((key) => Object.hasOwn(value, key));
|
|
150
|
+
}
|
|
151
|
+
return [...value];
|
|
348
152
|
}
|
|
349
153
|
|
|
350
154
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
@@ -354,11 +158,3 @@ function isRecord(value: unknown): value is Record<string, unknown> {
|
|
|
354
158
|
function isNonEmptyString(value: unknown): value is string {
|
|
355
159
|
return typeof value === "string" && value.length > 0;
|
|
356
160
|
}
|
|
357
|
-
|
|
358
|
-
function isNonNegativeFinite(value: unknown): value is number {
|
|
359
|
-
return typeof value === "number" && Number.isFinite(value) && value >= 0;
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
function isFiniteNumber(value: unknown): value is number {
|
|
363
|
-
return typeof value === "number" && Number.isFinite(value);
|
|
364
|
-
}
|
|
@@ -24,7 +24,7 @@ interface ModelFallbackOptions {
|
|
|
24
24
|
|
|
25
25
|
export async function resolveCandidates(
|
|
26
26
|
ctx: Pick<ExtensionContext, "modelRegistry" | "model" | "cwd" | "isProjectTrusted">,
|
|
27
|
-
preferredModels: ReadonlyArray<{ provider: string; model: string; reasoning: ThinkingLevel }>,
|
|
27
|
+
preferredModels: ReadonlyArray<{ provider: string; model: string; reasoning: ThinkingLevel | undefined }>,
|
|
28
28
|
includeParentModel: boolean,
|
|
29
29
|
): Promise<ModelCandidate[]> {
|
|
30
30
|
const settings = await loadTauExtensionSettings(ctx, modelFallbackSettings);
|
|
@@ -24,7 +24,6 @@ export interface EditImage {
|
|
|
24
24
|
|
|
25
25
|
export interface GeneratedImage {
|
|
26
26
|
bytes: Buffer;
|
|
27
|
-
base64: string;
|
|
28
27
|
mimeType: EditImage["mimeType"];
|
|
29
28
|
}
|
|
30
29
|
|
|
@@ -58,7 +57,7 @@ export function resolveCodexAuth(token: string): CodexAuth {
|
|
|
58
57
|
const authClaim = payload["https://api.openai.com/auth"];
|
|
59
58
|
if (!isRecord(authClaim)) throw invalidCredential();
|
|
60
59
|
const accountId = authClaim.chatgpt_account_id;
|
|
61
|
-
if (typeof accountId !== "string" ||
|
|
60
|
+
if (typeof accountId !== "string" || !/^[\x21-\x7e]+$/.test(accountId.trim())) throw invalidCredential();
|
|
62
61
|
return { token, accountId: accountId.trim() };
|
|
63
62
|
}
|
|
64
63
|
|
|
@@ -87,7 +86,7 @@ async function boundedError(response: HttpResponse): Promise<string> {
|
|
|
87
86
|
.trim();
|
|
88
87
|
}
|
|
89
88
|
|
|
90
|
-
function serverErrorMessage(body: string,
|
|
89
|
+
function serverErrorMessage(body: string, secrets: readonly string[]): string {
|
|
91
90
|
let message = body;
|
|
92
91
|
try {
|
|
93
92
|
const value: unknown = JSON.parse(body);
|
|
@@ -98,7 +97,10 @@ function serverErrorMessage(body: string, token: string): string {
|
|
|
98
97
|
} catch {
|
|
99
98
|
// Plain-text error body.
|
|
100
99
|
}
|
|
101
|
-
|
|
100
|
+
for (const secret of secrets) {
|
|
101
|
+
if (secret) message = message.replaceAll(secret, "[redacted]");
|
|
102
|
+
}
|
|
103
|
+
return message.slice(0, MAX_ERROR_MESSAGE_LENGTH).trim();
|
|
102
104
|
}
|
|
103
105
|
|
|
104
106
|
export function detectImageMimeType(bytes: Buffer): GeneratedImage["mimeType"] | undefined {
|
|
@@ -137,7 +139,7 @@ function decodeImageResponse(value: unknown, service: "OpenAI Codex" | "xAI", pn
|
|
|
137
139
|
if (!mimeType || (pngOnly && mimeType !== "image/png")) {
|
|
138
140
|
throw new Error(`${service} returned unsupported image data`);
|
|
139
141
|
}
|
|
140
|
-
return { bytes,
|
|
142
|
+
return { bytes, mimeType };
|
|
141
143
|
}
|
|
142
144
|
|
|
143
145
|
async function requestOpenAIImage(
|
|
@@ -147,20 +149,28 @@ async function requestOpenAIImage(
|
|
|
147
149
|
signal?: AbortSignal,
|
|
148
150
|
): Promise<GeneratedImage> {
|
|
149
151
|
const route = operation === "generation" ? "generations" : "edits";
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
152
|
+
let response: HttpResponse;
|
|
153
|
+
try {
|
|
154
|
+
response = (await fetch(`${OPENAI_IMAGE_API_BASE_URL}/${route}`, {
|
|
155
|
+
method: "POST",
|
|
156
|
+
headers: {
|
|
157
|
+
Accept: "application/json",
|
|
158
|
+
Authorization: `Bearer ${auth.token}`,
|
|
159
|
+
"chatgpt-account-id": auth.accountId,
|
|
160
|
+
"Content-Type": "application/json",
|
|
161
|
+
originator: "pi",
|
|
162
|
+
},
|
|
163
|
+
body: JSON.stringify(body),
|
|
164
|
+
signal,
|
|
165
|
+
})) as HttpResponse;
|
|
166
|
+
} catch (error) {
|
|
167
|
+
if (signal?.aborted) throw signal.reason;
|
|
168
|
+
let message = error instanceof Error ? error.message : "OpenAI Codex image request failed";
|
|
169
|
+
for (const secret of [auth.token, auth.accountId]) message = message.replaceAll(secret, "[redacted]");
|
|
170
|
+
throw new Error(message.slice(0, MAX_ERROR_MESSAGE_LENGTH).trim() || "OpenAI Codex image request failed");
|
|
171
|
+
}
|
|
162
172
|
if (!response.ok) {
|
|
163
|
-
const message = serverErrorMessage(await boundedError(response), auth.token);
|
|
173
|
+
const message = serverErrorMessage(await boundedError(response), [auth.token, auth.accountId]);
|
|
164
174
|
throw new Error(
|
|
165
175
|
`OpenAI Codex image ${operation} failed with status ${response.status}${message ? `: ${message}` : ""}`,
|
|
166
176
|
);
|
|
@@ -232,7 +242,7 @@ async function requestXaiImage(
|
|
|
232
242
|
}
|
|
233
243
|
return decodeImageResponse(value, "xAI", false);
|
|
234
244
|
}
|
|
235
|
-
const message = serverErrorMessage(await boundedError(response), token);
|
|
245
|
+
const message = serverErrorMessage(await boundedError(response), [token]);
|
|
236
246
|
if (!retryable(response.status) || attempt === MAX_ATTEMPTS) {
|
|
237
247
|
throw new Error(
|
|
238
248
|
`xAI image ${operation} failed with status ${response.status}${message ? `: ${message}` : ""}`,
|
|
@@ -243,7 +253,7 @@ async function requestXaiImage(
|
|
|
243
253
|
throw new Error(`xAI image ${operation} failed`);
|
|
244
254
|
}
|
|
245
255
|
|
|
246
|
-
export function
|
|
256
|
+
export function requestGeneratedImage(
|
|
247
257
|
provider: ImageProvider,
|
|
248
258
|
prompt: string,
|
|
249
259
|
token: string,
|