talon-agent 3.34.0 → 3.34.1
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/package.json +4 -2
- package/src/backend/claude-sdk/handler.ts +428 -415
- package/src/backend/codex/handler/message.ts +275 -366
- package/src/backend/codex/handler/rollout-accounting.ts +137 -0
- package/src/backend/openai-agents/handler/message.ts +282 -356
- package/src/backend/remote-server/chat-turn.ts +66 -122
- package/src/backend/remote-server/index.ts +4 -0
- package/src/backend/remote-server/mcp.ts +73 -9
- package/src/backend/remote-server/model-catalog/presentation.ts +269 -228
- package/src/backend/remote-server/sessions.ts +2 -2
- package/src/backend/shared/cache-telemetry.ts +17 -1
- package/src/backend/shared/handler-to-events.ts +11 -16
- package/src/backend/shared/index.ts +14 -15
- package/src/backend/shared/result-events.ts +30 -0
- package/src/backend/shared/turn-phases.ts +277 -0
- package/src/core/background/triggers/exit.ts +19 -2
- package/src/core/background/triggers/resume.ts +14 -7
- package/src/core/background/triggers/state.ts +9 -0
- package/src/core/mcp-hub/child-transport.ts +215 -0
- package/src/core/mcp-hub/children.ts +72 -13
- package/src/core/mcp-hub/index.ts +32 -13
- package/src/core/models/active-model.ts +29 -6
- package/src/core/weaver/shuttle.ts +4 -0
- package/src/frontend/telegram/admin.ts +75 -52
- package/src/storage/trigger-store.ts +8 -0
- package/src/util/watchdog.ts +32 -7
|
@@ -115,261 +115,302 @@ export interface RemoteModelPresentation {
|
|
|
115
115
|
): string;
|
|
116
116
|
}
|
|
117
117
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
): RemoteModelPresentation {
|
|
121
|
-
const {
|
|
122
|
-
label,
|
|
123
|
-
getCatalog,
|
|
124
|
-
maxCallbackIdLength,
|
|
125
|
-
allowCallbackSeparators,
|
|
126
|
-
quickPickLimit,
|
|
127
|
-
groupThreshold = 60,
|
|
128
|
-
} = options;
|
|
118
|
+
/** `RemotePresentationOptions` with defaults applied — what every renderer takes. */
|
|
119
|
+
type Knobs = Required<RemotePresentationOptions>;
|
|
129
120
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
if (allowCallbackSeparators) return true;
|
|
133
|
-
return !modelID.includes(":") && !modelID.includes("/");
|
|
134
|
-
}
|
|
121
|
+
const LIST_PREVIEW = 24;
|
|
122
|
+
const FREE_PREVIEW = 8;
|
|
135
123
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
)
|
|
140
|
-
|
|
141
|
-
const seen = new Set<string>();
|
|
124
|
+
function isCallbackSafeModelID(knobs: Knobs, modelID: string): boolean {
|
|
125
|
+
if (modelID.length > knobs.maxCallbackIdLength) return false;
|
|
126
|
+
if (knobs.allowCallbackSeparators) return true;
|
|
127
|
+
return !modelID.includes(":") && !modelID.includes("/");
|
|
128
|
+
}
|
|
142
129
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
130
|
+
/** `• <selection> — <name> (<provider> · <price> · <ctx> · <availability>)` */
|
|
131
|
+
function describeModel(
|
|
132
|
+
model: RemoteModelCatalogEntry,
|
|
133
|
+
catalog: RemoteModelCatalog,
|
|
134
|
+
): string {
|
|
135
|
+
const tags = [
|
|
136
|
+
model.providerName,
|
|
137
|
+
model.free ? "free" : `$${model.costInput}/${model.costOutput}`,
|
|
138
|
+
`${formatCtxWindow(model.contextWindow)} ctx`,
|
|
139
|
+
getAvailabilityLabel(model),
|
|
140
|
+
];
|
|
141
|
+
return `• ${getRemoteModelSelectionValue(model, catalog)} — ${model.name} (${tags.join(" · ")})`;
|
|
142
|
+
}
|
|
149
143
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
}
|
|
157
|
-
|
|
144
|
+
function describeProvider(
|
|
145
|
+
provider: RemoteModelCatalog["providers"][number],
|
|
146
|
+
): string {
|
|
147
|
+
const detail = provider.connected
|
|
148
|
+
? "connected"
|
|
149
|
+
: provider.loginRequired
|
|
150
|
+
? `login: ${provider.authMethods.join(", ")}`
|
|
151
|
+
: provider.envRequired
|
|
152
|
+
? `env: ${provider.envKeys.join(", ")}`
|
|
153
|
+
: provider.source;
|
|
154
|
+
return `• ${provider.name} (${provider.id}) — ${detail} · ${provider.modelCount} models`;
|
|
155
|
+
}
|
|
158
156
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
157
|
+
function getQuickPickModels(
|
|
158
|
+
knobs: Knobs,
|
|
159
|
+
catalog: RemoteModelCatalog,
|
|
160
|
+
currentModelID?: string,
|
|
161
|
+
): Array<RemoteModelCatalogEntry> {
|
|
162
|
+
const picks: Array<RemoteModelCatalogEntry> = [];
|
|
163
|
+
const seen = new Set<string>();
|
|
164
|
+
|
|
165
|
+
const tryAdd = (model: RemoteModelCatalogEntry | undefined) => {
|
|
166
|
+
if (!model || seen.has(model.id) || !isCallbackSafeModelID(knobs, model.id))
|
|
167
|
+
return;
|
|
168
|
+
picks.push(model);
|
|
169
|
+
seen.add(model.id);
|
|
170
|
+
};
|
|
163
171
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
172
|
+
if (currentModelID) {
|
|
173
|
+
const currentModel = resolveRemoteModelInput(currentModelID, catalog);
|
|
174
|
+
if (currentModel.kind === "exact") {
|
|
175
|
+
tryAdd(currentModel.model);
|
|
176
|
+
} else if (currentModel.kind === "ambiguous") {
|
|
177
|
+
tryAdd(currentModel.matches[0]);
|
|
169
178
|
}
|
|
179
|
+
}
|
|
170
180
|
|
|
171
|
-
|
|
181
|
+
for (const model of catalog.connectedFreeModels) {
|
|
182
|
+
if (picks.length >= knobs.quickPickLimit) break;
|
|
183
|
+
tryAdd(model);
|
|
172
184
|
}
|
|
173
185
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
const catalog = await getCatalog();
|
|
181
|
-
const current = getRemoteModelInfo(catalog, activeModel);
|
|
186
|
+
if (picks.length < knobs.quickPickLimit) {
|
|
187
|
+
for (const model of catalog.connectedModels) {
|
|
188
|
+
if (picks.length >= knobs.quickPickLimit) break;
|
|
189
|
+
tryAdd(model);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
182
192
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
isCallbackSafeModelID(m.id),
|
|
186
|
-
);
|
|
187
|
-
const freeCount = selectable.filter((m) => m.free).length;
|
|
188
|
-
const scoped = selectable.filter(
|
|
189
|
-
(m) =>
|
|
190
|
-
(filter === "all" || m.free) &&
|
|
191
|
-
(!options.provider || m.providerID === options.provider),
|
|
192
|
-
);
|
|
193
|
+
return picks;
|
|
194
|
+
}
|
|
193
195
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
scoped.length > groupThreshold;
|
|
196
|
+
/** One page of the picker: provider chips when the flat list is unreadable, models otherwise. */
|
|
197
|
+
type PickerPage = {
|
|
198
|
+
asGroups: boolean;
|
|
199
|
+
page: number;
|
|
200
|
+
totalPages: number;
|
|
201
|
+
providers: RemoteModelCatalog["connectedProviders"];
|
|
202
|
+
models: Array<RemoteModelCatalogEntry>;
|
|
203
|
+
};
|
|
203
204
|
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
205
|
+
function pagePicker(
|
|
206
|
+
knobs: Knobs,
|
|
207
|
+
catalog: RemoteModelCatalog,
|
|
208
|
+
scoped: Array<RemoteModelCatalogEntry>,
|
|
209
|
+
options: RemotePickerOptions,
|
|
210
|
+
): PickerPage {
|
|
211
|
+
// A remote catalog can run to hundreds of models, and only the first
|
|
212
|
+
// page's worth ever fit on screen — so offer the provider list as the
|
|
213
|
+
// way in when nothing narrower was asked for and the flat list would be
|
|
214
|
+
// unreadable anyway.
|
|
215
|
+
const providers = catalog.connectedProviders;
|
|
216
|
+
const asGroups =
|
|
217
|
+
!options.provider &&
|
|
218
|
+
providers.length > 1 &&
|
|
219
|
+
scoped.length > knobs.groupThreshold;
|
|
220
|
+
const pageSize = Math.max(1, options.pageSize ?? knobs.quickPickLimit);
|
|
221
|
+
const source = asGroups ? providers : scoped;
|
|
222
|
+
const totalPages = Math.max(1, Math.ceil(source.length / pageSize));
|
|
223
|
+
const page = Math.min(Math.max(1, options.page ?? 1), totalPages);
|
|
224
|
+
const from = (page - 1) * pageSize;
|
|
225
|
+
return {
|
|
226
|
+
asGroups,
|
|
227
|
+
page,
|
|
228
|
+
totalPages,
|
|
229
|
+
providers: asGroups ? providers.slice(from, from + pageSize) : [],
|
|
230
|
+
models: asGroups ? [] : scoped.slice(from, from + pageSize),
|
|
231
|
+
};
|
|
232
|
+
}
|
|
209
233
|
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
+
function pickerButtons(
|
|
235
|
+
paged: PickerPage,
|
|
236
|
+
current: RemoteModelCatalogEntry | undefined,
|
|
237
|
+
callbackPrefix: string,
|
|
238
|
+
navPrefix: string,
|
|
239
|
+
): Array<ModelButton> {
|
|
240
|
+
if (paged.asGroups) {
|
|
241
|
+
return paged.providers.map((p) => ({
|
|
242
|
+
text: `${p.name} (${p.modelCount})`,
|
|
243
|
+
callback_data: `${navPrefix}:provider:${p.id}`,
|
|
244
|
+
}));
|
|
245
|
+
}
|
|
246
|
+
const buttons = paged.models.map((m) => {
|
|
247
|
+
const btnLabel =
|
|
248
|
+
m.id.length <= 20 ? m.id : m.name.length <= 20 ? m.name : m.id;
|
|
249
|
+
const txt = m.free ? `${btnLabel} ★` : btnLabel;
|
|
250
|
+
const sel =
|
|
251
|
+
current && m.id === current.id && m.providerID === current.providerID;
|
|
252
|
+
return {
|
|
253
|
+
text: sel ? `✓ ${txt}` : txt,
|
|
254
|
+
callback_data: `${callbackPrefix}${m.id}`,
|
|
255
|
+
};
|
|
256
|
+
});
|
|
257
|
+
buttons.push({ text: "Reset", callback_data: `${callbackPrefix}reset` });
|
|
258
|
+
return buttons;
|
|
259
|
+
}
|
|
234
260
|
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
);
|
|
243
|
-
}
|
|
244
|
-
const np = catalog.connectedProviders.length;
|
|
245
|
-
const nm = catalog.connectedModels.length;
|
|
261
|
+
function pickerDetails(
|
|
262
|
+
knobs: Knobs,
|
|
263
|
+
catalog: RemoteModelCatalog,
|
|
264
|
+
current: RemoteModelCatalogEntry | undefined,
|
|
265
|
+
): Array<string> {
|
|
266
|
+
const details: Array<string> = [];
|
|
267
|
+
if (current) {
|
|
246
268
|
details.push(
|
|
247
|
-
|
|
269
|
+
`Provider: ${current.providerName} · ${getAvailabilityLabel(current)}`,
|
|
270
|
+
);
|
|
271
|
+
details.push(
|
|
272
|
+
`Context: ${formatCtxWindow(current.contextWindow)} · reasoning ${current.reasoning ? "yes" : "no"} · tools ${current.toolcall ? "yes" : "no"}`,
|
|
248
273
|
);
|
|
249
|
-
if (catalog.loginProviders.length > 0) {
|
|
250
|
-
const preview = catalog.loginProviders
|
|
251
|
-
.slice(0, 4)
|
|
252
|
-
.map((p) => p.name)
|
|
253
|
-
.join(", ");
|
|
254
|
-
details.push(
|
|
255
|
-
`Login available: ${preview}${catalog.loginProviders.length > 4 ? "…" : ""}`,
|
|
256
|
-
);
|
|
257
|
-
}
|
|
258
|
-
details.push("Hint: use /model <name> to switch.");
|
|
259
|
-
return {
|
|
260
|
-
modelButtons,
|
|
261
|
-
modelDetails: details,
|
|
262
|
-
view: asGroups ? "groups" : "models",
|
|
263
|
-
page,
|
|
264
|
-
totalPages,
|
|
265
|
-
filter,
|
|
266
|
-
freeCount,
|
|
267
|
-
totalCount: selectable.length,
|
|
268
|
-
...(options.provider ? { provider: options.provider } : {}),
|
|
269
|
-
};
|
|
270
274
|
}
|
|
275
|
+
const np = catalog.connectedProviders.length;
|
|
276
|
+
const nm = catalog.connectedModels.length;
|
|
277
|
+
details.push(
|
|
278
|
+
`${knobs.label}: ${np} provider${np === 1 ? "" : "s"} connected · ${nm} model${nm === 1 ? "" : "s"} usable`,
|
|
279
|
+
);
|
|
280
|
+
if (catalog.loginProviders.length > 0) {
|
|
281
|
+
const preview = catalog.loginProviders
|
|
282
|
+
.slice(0, 4)
|
|
283
|
+
.map((p) => p.name)
|
|
284
|
+
.join(", ");
|
|
285
|
+
details.push(
|
|
286
|
+
`Login available: ${preview}${catalog.loginProviders.length > 4 ? "…" : ""}`,
|
|
287
|
+
);
|
|
288
|
+
}
|
|
289
|
+
details.push("Hint: use /model <name> to switch.");
|
|
290
|
+
return details;
|
|
291
|
+
}
|
|
271
292
|
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
? getRemoteModelSelectionValue(current, catalog)
|
|
282
|
-
: activeModel;
|
|
283
|
-
const freePreview = catalog.connectedFreeModels.slice(0, 8);
|
|
293
|
+
async function getSettingsPresentation(
|
|
294
|
+
knobs: Knobs,
|
|
295
|
+
activeModel: string,
|
|
296
|
+
options: RemotePickerOptions = {},
|
|
297
|
+
): Promise<RemotePickerResult> {
|
|
298
|
+
const callbackPrefix = options.callbackPrefix ?? "settings:model:";
|
|
299
|
+
const navPrefix = options.navCallbackPrefix ?? "settings:models";
|
|
300
|
+
const catalog = await knobs.getCatalog();
|
|
301
|
+
const current = getRemoteModelInfo(catalog, activeModel);
|
|
284
302
|
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
303
|
+
const filter = options.filter === "free" ? "free" : "all";
|
|
304
|
+
const selectable = catalog.connectedModels.filter((m) =>
|
|
305
|
+
isCallbackSafeModelID(knobs, m.id),
|
|
306
|
+
);
|
|
307
|
+
const freeCount = selectable.filter((m) => m.free).length;
|
|
308
|
+
const scoped = selectable.filter(
|
|
309
|
+
(m) =>
|
|
310
|
+
(filter === "all" || m.free) &&
|
|
311
|
+
(!options.provider || m.providerID === options.provider),
|
|
312
|
+
);
|
|
313
|
+
const paged = pagePicker(knobs, catalog, scoped, options);
|
|
314
|
+
return {
|
|
315
|
+
modelButtons: pickerButtons(paged, current, callbackPrefix, navPrefix),
|
|
316
|
+
modelDetails: pickerDetails(knobs, catalog, current),
|
|
317
|
+
view: paged.asGroups ? "groups" : "models",
|
|
318
|
+
page: paged.page,
|
|
319
|
+
totalPages: paged.totalPages,
|
|
320
|
+
filter,
|
|
321
|
+
freeCount,
|
|
322
|
+
totalCount: selectable.length,
|
|
323
|
+
...(options.provider ? { provider: options.provider } : {}),
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
async function renderModelSummary(
|
|
328
|
+
knobs: Knobs,
|
|
329
|
+
activeModel: string,
|
|
330
|
+
defaultModel: string,
|
|
331
|
+
): Promise<{ text: string; quickButtons: Array<ModelButton> }> {
|
|
332
|
+
const { modelButtons, modelDetails } = await getSettingsPresentation(
|
|
333
|
+
knobs,
|
|
334
|
+
activeModel,
|
|
335
|
+
);
|
|
336
|
+
const catalog = await knobs.getCatalog();
|
|
337
|
+
const current = getRemoteModelInfo(catalog, activeModel);
|
|
338
|
+
const currentLabel = current
|
|
339
|
+
? getRemoteModelSelectionValue(current, catalog)
|
|
340
|
+
: activeModel;
|
|
341
|
+
const freePreview = catalog.connectedFreeModels.slice(0, FREE_PREVIEW);
|
|
342
|
+
|
|
343
|
+
const lines = [
|
|
344
|
+
`Model: ${currentLabel}${activeModel === defaultModel ? " (default)" : ""}`,
|
|
345
|
+
...modelDetails,
|
|
346
|
+
];
|
|
347
|
+
if (freePreview.length > 0) {
|
|
348
|
+
lines.push("", "Free now");
|
|
349
|
+
for (const m of freePreview) lines.push(describeModel(m, catalog));
|
|
304
350
|
}
|
|
351
|
+
return { text: lines.join("\n"), quickButtons: modelButtons };
|
|
352
|
+
}
|
|
305
353
|
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
: p.loginRequired
|
|
316
|
-
? `login: ${p.authMethods.join(", ")}`
|
|
317
|
-
: p.envRequired
|
|
318
|
-
? `env: ${p.envKeys.join(", ")}`
|
|
319
|
-
: p.source;
|
|
320
|
-
lines.push(
|
|
321
|
-
`• ${p.name} (${p.id}) — ${detail} · ${p.modelCount} models`,
|
|
322
|
-
);
|
|
323
|
-
}
|
|
324
|
-
if (catalog.providers.length > 24)
|
|
325
|
-
lines.push(`…and ${catalog.providers.length - 24} more`);
|
|
326
|
-
return lines.join("\n");
|
|
327
|
-
}
|
|
328
|
-
const source =
|
|
329
|
-
mode === "free" ? catalog.connectedFreeModels : catalog.connectedModels;
|
|
330
|
-
const title =
|
|
331
|
-
mode === "free" ? "Connected Free Models" : "Connected Models";
|
|
332
|
-
const lines = [title];
|
|
333
|
-
for (const m of source.slice(0, 24)) {
|
|
334
|
-
const tags = [
|
|
335
|
-
m.providerName,
|
|
336
|
-
m.free ? "free" : `$${m.costInput}/${m.costOutput}`,
|
|
337
|
-
`${formatCtxWindow(m.contextWindow)} ctx`,
|
|
338
|
-
getAvailabilityLabel(m),
|
|
339
|
-
];
|
|
340
|
-
lines.push(
|
|
341
|
-
`• ${getRemoteModelSelectionValue(m, catalog)} — ${m.name} (${tags.join(" · ")})`,
|
|
342
|
-
);
|
|
354
|
+
async function renderModelList(
|
|
355
|
+
knobs: Knobs,
|
|
356
|
+
mode: "free" | "all" | "providers",
|
|
357
|
+
): Promise<string> {
|
|
358
|
+
const catalog = await knobs.getCatalog();
|
|
359
|
+
if (mode === "providers") {
|
|
360
|
+
const lines = [`${knobs.label} Providers`];
|
|
361
|
+
for (const p of catalog.providers.slice(0, LIST_PREVIEW)) {
|
|
362
|
+
lines.push(describeProvider(p));
|
|
343
363
|
}
|
|
344
|
-
if (
|
|
364
|
+
if (catalog.providers.length > LIST_PREVIEW)
|
|
365
|
+
lines.push(`…and ${catalog.providers.length - LIST_PREVIEW} more`);
|
|
345
366
|
return lines.join("\n");
|
|
346
367
|
}
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
if (resolution.kind === "missing")
|
|
354
|
-
return `No ${label} model matched "${input}".`;
|
|
355
|
-
const preview = resolution.matches
|
|
356
|
-
.slice(0, 6)
|
|
357
|
-
.map((m) => {
|
|
358
|
-
const provider =
|
|
359
|
-
m.providerName === m.providerID
|
|
360
|
-
? m.providerName
|
|
361
|
-
: `${m.providerName} / ${m.providerID}`;
|
|
362
|
-
return `${getRemoteModelSelectionValue(m, catalog)} — ${provider} (${getAvailabilityLabel(m)})`;
|
|
363
|
-
})
|
|
364
|
-
.join(", ");
|
|
365
|
-
return `Model query "${input}" is ambiguous. Try one of: ${preview}`;
|
|
368
|
+
const source =
|
|
369
|
+
mode === "free" ? catalog.connectedFreeModels : catalog.connectedModels;
|
|
370
|
+
const title = mode === "free" ? "Connected Free Models" : "Connected Models";
|
|
371
|
+
const lines = [title];
|
|
372
|
+
for (const m of source.slice(0, LIST_PREVIEW)) {
|
|
373
|
+
lines.push(describeModel(m, catalog));
|
|
366
374
|
}
|
|
375
|
+
if (source.length > LIST_PREVIEW)
|
|
376
|
+
lines.push(`…and ${source.length - LIST_PREVIEW} more`);
|
|
377
|
+
return lines.join("\n");
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
function formatSelectionError(
|
|
381
|
+
knobs: Knobs,
|
|
382
|
+
input: string,
|
|
383
|
+
resolution: Exclude<RemoteModelResolution, { kind: "exact" }>,
|
|
384
|
+
catalog: RemoteModelCatalog,
|
|
385
|
+
): string {
|
|
386
|
+
if (resolution.kind === "missing")
|
|
387
|
+
return `No ${knobs.label} model matched "${input}".`;
|
|
388
|
+
const preview = resolution.matches
|
|
389
|
+
.slice(0, 6)
|
|
390
|
+
.map((m) => {
|
|
391
|
+
const provider =
|
|
392
|
+
m.providerName === m.providerID
|
|
393
|
+
? m.providerName
|
|
394
|
+
: `${m.providerName} / ${m.providerID}`;
|
|
395
|
+
return `${getRemoteModelSelectionValue(m, catalog)} — ${provider} (${getAvailabilityLabel(m)})`;
|
|
396
|
+
})
|
|
397
|
+
.join(", ");
|
|
398
|
+
return `Model query "${input}" is ambiguous. Try one of: ${preview}`;
|
|
399
|
+
}
|
|
367
400
|
|
|
401
|
+
export function createRemoteModelPresentation(
|
|
402
|
+
options: RemotePresentationOptions,
|
|
403
|
+
): RemoteModelPresentation {
|
|
404
|
+
const knobs: Knobs = { groupThreshold: 60, ...options };
|
|
368
405
|
return {
|
|
369
|
-
getQuickPickModels,
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
406
|
+
getQuickPickModels: (catalog, currentModelID) =>
|
|
407
|
+
getQuickPickModels(knobs, catalog, currentModelID),
|
|
408
|
+
getSettingsPresentation: (activeModel, pickerOptions) =>
|
|
409
|
+
getSettingsPresentation(knobs, activeModel, pickerOptions),
|
|
410
|
+
renderModelSummary: (activeModel, defaultModel) =>
|
|
411
|
+
renderModelSummary(knobs, activeModel, defaultModel),
|
|
412
|
+
renderModelList: (mode) => renderModelList(knobs, mode),
|
|
413
|
+
formatSelectionError: (input, resolution, catalog) =>
|
|
414
|
+
formatSelectionError(knobs, input, resolution, catalog),
|
|
374
415
|
};
|
|
375
416
|
}
|
|
@@ -21,7 +21,7 @@ import {
|
|
|
21
21
|
TALON_MCP_SERVER_NAME,
|
|
22
22
|
TALON_PLUGIN_MCP_SERVER_NAME,
|
|
23
23
|
getChatMcpServerName,
|
|
24
|
-
|
|
24
|
+
getPluginMcpServerPrefix,
|
|
25
25
|
} from "./mcp.js";
|
|
26
26
|
|
|
27
27
|
/**
|
|
@@ -52,7 +52,7 @@ import {
|
|
|
52
52
|
*/
|
|
53
53
|
export function buildPermissionRuleset(chatId: string): RemotePermissionRule[] {
|
|
54
54
|
const ourServerName = getChatMcpServerName(chatId);
|
|
55
|
-
const ourPluginPrefix =
|
|
55
|
+
const ourPluginPrefix = getPluginMcpServerPrefix(chatId);
|
|
56
56
|
return [
|
|
57
57
|
{ permission: "tool", pattern: `${ourServerName}_*`, action: "allow" },
|
|
58
58
|
{
|
|
@@ -249,10 +249,23 @@ function estimateTokens(text: string): number {
|
|
|
249
249
|
return Math.ceil(text.length / 4);
|
|
250
250
|
}
|
|
251
251
|
|
|
252
|
+
/**
|
|
253
|
+
* (label, model) pairs already warned about this process. A recurring
|
|
254
|
+
* caller (heartbeat, dream) builds the same prompt on every run, so the
|
|
255
|
+
* first warning says everything the rest would.
|
|
256
|
+
*/
|
|
257
|
+
const warnedCacheMinimums = new Set<string>();
|
|
258
|
+
|
|
259
|
+
/** Test seam. */
|
|
260
|
+
export function resetCacheMinimumWarnings(): void {
|
|
261
|
+
warnedCacheMinimums.clear();
|
|
262
|
+
}
|
|
263
|
+
|
|
252
264
|
/**
|
|
253
265
|
* Warn when a prompt is too small to be cacheable on its model. No-op when
|
|
254
266
|
* the model's floor is unknown or the prompt clears it. `label` names the
|
|
255
|
-
* caller (e.g. `"dream"`) so the warning points somewhere.
|
|
267
|
+
* caller (e.g. `"dream"`) so the warning points somewhere. Warns once per
|
|
268
|
+
* (label, model) per process.
|
|
256
269
|
*/
|
|
257
270
|
export function warnIfBelowCacheMinimum(
|
|
258
271
|
label: string,
|
|
@@ -263,6 +276,9 @@ export function warnIfBelowCacheMinimum(
|
|
|
263
276
|
if (min === undefined) return;
|
|
264
277
|
const estimated = estimateTokens(prompt);
|
|
265
278
|
if (estimated >= min) return;
|
|
279
|
+
const key = `${label}\0${model}`;
|
|
280
|
+
if (warnedCacheMinimums.has(key)) return;
|
|
281
|
+
warnedCacheMinimums.add(key);
|
|
266
282
|
logWarn(
|
|
267
283
|
"agent",
|
|
268
284
|
`[${label}] prompt ~${estimated} tokens is below ${model}'s ${min}-token ` +
|
|
@@ -23,6 +23,7 @@ import {
|
|
|
23
23
|
import { classify } from "../../core/errors.js";
|
|
24
24
|
import type { ChatRunParams } from "../../core/agent-runtime/capabilities.js";
|
|
25
25
|
import type { QueryParams, QueryResult } from "./handler-types.js";
|
|
26
|
+
import { buildResultEvents } from "./result-events.js";
|
|
26
27
|
|
|
27
28
|
const SENTINEL = Symbol("handler-to-events:sentinel");
|
|
28
29
|
|
|
@@ -205,21 +206,15 @@ export async function* handlerToEvents(
|
|
|
205
206
|
return;
|
|
206
207
|
}
|
|
207
208
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
yield {
|
|
217
|
-
type: "completed",
|
|
218
|
-
result: {
|
|
219
|
-
text: result.text,
|
|
220
|
-
durationMs: result.durationMs,
|
|
221
|
-
usage,
|
|
222
|
-
modelId: params.model.id,
|
|
209
|
+
yield* buildResultEvents({
|
|
210
|
+
text: result.text,
|
|
211
|
+
durationMs: result.durationMs,
|
|
212
|
+
usage: {
|
|
213
|
+
inputTokens: result.inputTokens,
|
|
214
|
+
outputTokens: result.outputTokens,
|
|
215
|
+
cacheRead: result.cacheRead,
|
|
216
|
+
cacheWrite: result.cacheWrite,
|
|
223
217
|
},
|
|
224
|
-
|
|
218
|
+
modelId: params.model.id,
|
|
219
|
+
});
|
|
225
220
|
}
|