talon-agent 3.34.0 → 3.35.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/LICENSE +1 -1
- package/README.md +0 -10
- package/package.json +6 -4
- package/src/backend/claude-sdk/handler.ts +428 -415
- package/src/backend/codex/auth.ts +40 -1
- package/src/backend/codex/handler/message.ts +290 -366
- package/src/backend/codex/handler/rollout-accounting.ts +137 -0
- package/src/backend/codex/plan-usage.ts +27 -2
- package/src/backend/kilo/handler/message.ts +2 -0
- package/src/backend/kilo/server.ts +1 -0
- package/src/backend/openai-agents/handler/message.ts +282 -356
- package/src/backend/opencode/handler/message.ts +2 -0
- package/src/backend/opencode/server.ts +1 -0
- package/src/backend/remote-server/chat-turn.ts +76 -122
- package/src/backend/remote-server/index.ts +4 -0
- package/src/backend/remote-server/lifecycle.ts +37 -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/server-bindings.ts +12 -0
- package/src/backend/remote-server/sessions.ts +2 -2
- package/src/backend/remote-server/state.ts +8 -0
- package/src/backend/remote-server/turn.ts +93 -23
- 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/bootstrap.ts +24 -10
- package/src/core/auth/expiry-monitor.ts +89 -0
- package/src/core/auth/login-flow.ts +247 -0
- package/src/core/auth/status.ts +193 -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/engine/gateway-actions/history.ts +12 -1
- package/src/core/errors.ts +1 -1
- 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/tools/history.ts +10 -1
- package/src/core/weaver/shuttle.ts +4 -0
- package/src/frontend/shared/model-commands.ts +400 -0
- package/src/frontend/telegram/admin.ts +75 -52
- package/src/frontend/telegram/auth-panel.ts +205 -0
- package/src/frontend/telegram/callbacks/auth.ts +70 -0
- package/src/frontend/telegram/callbacks/index.ts +16 -0
- package/src/frontend/telegram/callbacks/whatsapp.ts +50 -0
- package/src/frontend/telegram/commands/auth.ts +58 -0
- package/src/frontend/telegram/commands/definitions.ts +4 -0
- package/src/frontend/telegram/commands/index.ts +10 -4
- package/src/frontend/telegram/commands/whatsapp-pairing.ts +121 -79
- package/src/frontend/whatsapp/actions/chat-info.ts +2 -36
- package/src/frontend/whatsapp/actions/history.ts +100 -0
- package/src/frontend/whatsapp/actions/index.ts +4 -1
- package/src/frontend/whatsapp/commands.ts +371 -0
- package/src/frontend/whatsapp/inbound.ts +24 -46
- package/src/frontend/whatsapp/index.ts +12 -1
- package/src/frontend/whatsapp/media-store.ts +20 -1
- package/src/frontend/whatsapp/message-store.ts +101 -24
- package/src/storage/history.ts +22 -1
- package/src/storage/repositories/history-repo.ts +14 -0
- package/src/storage/repositories/whatsapp-messages-repo.ts +101 -0
- package/src/storage/sql/history.sql +12 -0
- package/src/storage/sql/schema.sql +21 -0
- package/src/storage/sql/statements.generated.ts +50 -1
- package/src/storage/sql/whatsapp-messages.sql +29 -0
- package/src/storage/trigger-store.ts +8 -0
- package/src/storage/whatsapp-messages.ts +51 -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
|
}
|
|
@@ -107,6 +107,12 @@ export interface RemoteServerBindings<TClient extends RemoteAgentClient> {
|
|
|
107
107
|
stop(): void;
|
|
108
108
|
/** Lazily start (or reuse) the local server and return a strict client. */
|
|
109
109
|
ensureServer(): Promise<TClient>;
|
|
110
|
+
/**
|
|
111
|
+
* Register a chat turn's abort controller for the lifetime of the turn.
|
|
112
|
+
* `stop()` aborts every registered controller so no turn outlives its
|
|
113
|
+
* server. Returns the unregister function; call it when the turn ends.
|
|
114
|
+
*/
|
|
115
|
+
trackActiveTurn(controller: AbortController): () => void;
|
|
110
116
|
ensureChatMcpServer(oc: TClient, chatId: string): Promise<string>;
|
|
111
117
|
ensurePluginMcpServers(oc: TClient, chatId: string): Promise<string[]>;
|
|
112
118
|
/**
|
|
@@ -193,6 +199,12 @@ export function bindRemoteServer<TClient extends RemoteAgentClient>(
|
|
|
193
199
|
});
|
|
194
200
|
},
|
|
195
201
|
ensureServer,
|
|
202
|
+
trackActiveTurn(controller) {
|
|
203
|
+
state.activeTurns.add(controller);
|
|
204
|
+
return () => {
|
|
205
|
+
state.activeTurns.delete(controller);
|
|
206
|
+
};
|
|
207
|
+
},
|
|
196
208
|
ensureChatMcpServer,
|
|
197
209
|
ensurePluginMcpServers,
|
|
198
210
|
buildToolOverrides: (oc, chatServerName, pluginServerNames = []) =>
|
|
@@ -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
|
{
|
|
@@ -56,6 +56,13 @@ export interface RemoteServerState<TClient extends RemoteAgentClient> {
|
|
|
56
56
|
readonly registeredMcpTools: Map<string, readonly string[]>;
|
|
57
57
|
/** Plugin-name → registered server-name mapping for each chat context. */
|
|
58
58
|
readonly pluginMcpServersByChat: Map<string, Map<string, string>>;
|
|
59
|
+
/**
|
|
60
|
+
* One controller per in-flight chat turn. `stopRemoteServer` aborts them
|
|
61
|
+
* so a turn cannot outlive the server it is streaming from — without
|
|
62
|
+
* this, a mid-turn backend swap left the SSE await hanging until the
|
|
63
|
+
* 600s deadline while the question watchdog hammered the dead socket.
|
|
64
|
+
*/
|
|
65
|
+
readonly activeTurns: Set<AbortController>;
|
|
59
66
|
}
|
|
60
67
|
|
|
61
68
|
/** Inputs for {@link createRemoteServerState}. */
|
|
@@ -90,6 +97,7 @@ export function createRemoteServerState<TClient extends RemoteAgentClient>(
|
|
|
90
97
|
registeredMcpServers: new Set(),
|
|
91
98
|
registeredMcpTools: new Map(),
|
|
92
99
|
pluginMcpServersByChat: new Map(),
|
|
100
|
+
activeTurns: new Set(),
|
|
93
101
|
};
|
|
94
102
|
}
|
|
95
103
|
|