talon-agent 5.0.1 → 5.2.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 +3 -1
- package/bin/talon.js +35 -0
- package/package.json +3 -3
- package/prompts/identity.md +10 -2
- package/prompts/system/agent-brief.md +43 -0
- package/src/app.ts +19 -26
- package/src/backend/builtins.ts +26 -7
- package/src/backend/claude-sdk/handler.ts +191 -69
- package/src/backend/claude-sdk/one-shot.ts +30 -7
- package/src/backend/claude-sdk/stream.ts +9 -0
- package/src/backend/codex/one-shot.ts +18 -4
- package/src/backend/remote-server/index.ts +6 -4
- package/src/backend/remote-server/model-catalog/index.ts +4 -10
- package/src/backend/remote-server/model-catalog/provider.ts +3 -3
- package/src/backend/remote-server/one-shot.ts +16 -3
- package/src/backend/remote-server/profiles/bind.ts +225 -0
- package/src/backend/remote-server/profiles/index.ts +10 -0
- package/src/backend/remote-server/profiles/kilo.ts +82 -0
- package/src/backend/remote-server/profiles/opencode.ts +61 -0
- package/src/backend/remote-server/server-bindings.ts +3 -4
- package/src/backend/runtime/one-shot-hooks.ts +45 -0
- package/src/bootstrap.ts +15 -1
- package/src/cli/chat.ts +5 -0
- package/src/cli/events.ts +9 -0
- package/src/core/agent-runtime/agent-host.ts +7 -6
- package/src/core/agent-runtime/capabilities.ts +3 -0
- package/src/core/agents/context.ts +48 -0
- package/src/core/agents/delivery.ts +167 -0
- package/src/core/agents/index.ts +37 -0
- package/src/core/agents/prompt.ts +116 -0
- package/src/core/agents/registry.ts +426 -0
- package/src/core/agents/runner.ts +448 -0
- package/src/core/agents/types.ts +124 -0
- package/src/core/background/cron/job-oneshot.ts +7 -12
- package/src/core/background/cron/job-prompt.ts +1 -1
- package/src/core/background/{cron/isolated-agent.ts → isolated-agent.ts} +45 -24
- package/src/core/background/run-log.ts +33 -0
- package/src/core/bus/events.ts +49 -2
- package/src/core/config/index.ts +25 -0
- package/src/core/engine/gateway-actions/agents/control.ts +299 -0
- package/src/core/engine/gateway-actions/agents/index.ts +31 -0
- package/src/core/engine/gateway-actions/agents/report.ts +107 -0
- package/src/core/engine/gateway-actions/index.ts +30 -0
- package/src/core/engine/gateway-actions/native/exec-remote.ts +1 -1
- package/src/core/engine/gateway-actions/native/exec.ts +1 -1
- package/src/core/engine/gateway-actions/native/read.ts +1 -1
- package/src/core/engine/gateway-actions/native/search.ts +1 -1
- package/src/core/engine/gateway-actions/native/teleport.ts +1 -1
- package/src/core/engine/gateway-actions/native/write.ts +1 -1
- package/src/core/engine/gateway-routes.ts +12 -0
- package/src/core/engine/gateway.ts +96 -25
- package/src/core/frontend-runtime/capabilities.ts +18 -0
- package/src/core/frontend-runtime/index.ts +4 -0
- package/src/core/frontend-runtime/lifecycle.ts +33 -0
- package/src/core/frontend-runtime/registry.ts +3 -3
- package/src/core/frontend-runtime/run-loop.ts +59 -0
- package/src/core/mcp-hub/children.ts +21 -5
- package/src/core/mesh/{registry.ts → devices/registry.ts} +4 -4
- package/src/core/mesh/{service.ts → devices/service.ts} +13 -10
- package/src/core/mesh/{teleport.ts → devices/teleport.ts} +2 -2
- package/src/core/mesh/index.ts +6 -2
- package/src/core/mesh/{bridge-links.ts → links/bridge-links.ts} +1 -1
- package/src/core/mesh/{companion-pairing.ts → links/companion-pairing.ts} +1 -1
- package/src/core/mesh/{node-binaries.ts → links/node-binaries.ts} +5 -5
- package/src/core/mesh/{node-provision.ts → links/node-provision.ts} +1 -1
- package/src/core/mesh/{common.ts → tool-surface.ts} +7 -2
- package/src/core/mesh/{device-files.ts → transfers/device-files.ts} +5 -5
- package/src/core/prompt/embedded-prompts.ts +38 -36
- package/src/core/tasks/types.ts +2 -2
- package/src/core/tools/index.ts +5 -0
- package/src/core/tools/ops/agents.ts +195 -0
- package/src/core/tools/ops/bridge.ts +4 -0
- package/src/core/tools/types.ts +1 -0
- package/src/core/types.ts +12 -1
- package/src/frontend/discord/commands/info.ts +1 -1
- package/src/frontend/discord/render.ts +1 -1
- package/src/frontend/native/bridge/routes/mesh.ts +1 -1
- package/src/frontend/native/index.ts +2 -0
- package/src/frontend/presentation/reports.ts +1 -1
- package/src/frontend/teams/index.ts +4 -3
- package/src/frontend/telegram/commands/info.ts +61 -20
- package/src/frontend/telegram/index.ts +27 -4
- package/src/frontend/telegram/render/reports.ts +1 -1
- package/src/frontend/terminal/index.ts +6 -2
- package/src/frontend/whatsapp/connection/connection.ts +62 -11
- package/src/frontend/whatsapp/index.ts +25 -1
- package/src/frontend/whatsapp/runtime.ts +7 -0
- package/src/util/log.ts +1 -0
- package/src/backend/kilo/factory.ts +0 -53
- package/src/backend/kilo/handler/index.ts +0 -2
- package/src/backend/kilo/handler/message.ts +0 -44
- package/src/backend/kilo/index.ts +0 -61
- package/src/backend/kilo/model-provider.ts +0 -36
- package/src/backend/kilo/models/index.ts +0 -55
- package/src/backend/kilo/one-shot.ts +0 -42
- package/src/backend/kilo/server.ts +0 -98
- package/src/backend/kilo/sessions.ts +0 -37
- package/src/backend/opencode/factory.ts +0 -53
- package/src/backend/opencode/handler/index.ts +0 -2
- package/src/backend/opencode/handler/message.ts +0 -44
- package/src/backend/opencode/index.ts +0 -42
- package/src/backend/opencode/model-provider.ts +0 -36
- package/src/backend/opencode/models/index.ts +0 -54
- package/src/backend/opencode/one-shot.ts +0 -42
- package/src/backend/opencode/server.ts +0 -80
- package/src/backend/opencode/sessions.ts +0 -35
- /package/src/core/mesh/{transfers.ts → transfers/transfers.ts} +0 -0
|
@@ -19,7 +19,9 @@ import { log, logWarn } from "../../util/log.js";
|
|
|
19
19
|
import { ALLOWED_TOOLS_BACKGROUND } from "../../core/constants.js";
|
|
20
20
|
import { EFFORT_MAP } from "./constants.js";
|
|
21
21
|
import { buildMcpServers, buildPluginMcpServers } from "./options.js";
|
|
22
|
+
import { isBackgroundToolContext } from "../../core/agents/context.js";
|
|
22
23
|
import { warnIfBelowCacheMinimum } from "../runtime/cache/cache-telemetry.js";
|
|
24
|
+
import { emitAssistantText } from "../runtime/one-shot-hooks.js";
|
|
23
25
|
|
|
24
26
|
const DEFAULT_SUBPROCESS_KILL_GRACE_MS = 5 * 1000;
|
|
25
27
|
|
|
@@ -65,6 +67,7 @@ export async function runOneShotAgent(
|
|
|
65
67
|
contextLabel,
|
|
66
68
|
abortController,
|
|
67
69
|
appendLog,
|
|
70
|
+
onAssistantText,
|
|
68
71
|
} = params;
|
|
69
72
|
|
|
70
73
|
// Reasoning effort is opt-in for background runs (config `heartbeatEffort`
|
|
@@ -126,7 +129,7 @@ export async function runOneShotAgent(
|
|
|
126
129
|
// settlement figure the task table records.
|
|
127
130
|
let usage: OneShotUsage | undefined;
|
|
128
131
|
for await (const msg of qi) {
|
|
129
|
-
await formatAndAppendMessage(appendLog, msg);
|
|
132
|
+
await formatAndAppendMessage(appendLog, msg, onAssistantText);
|
|
130
133
|
if (msg.type === "result") {
|
|
131
134
|
const u = msg.usage;
|
|
132
135
|
usage = {
|
|
@@ -142,8 +145,12 @@ export async function runOneShotAgent(
|
|
|
142
145
|
|
|
143
146
|
/**
|
|
144
147
|
* Per-context MCP server selection.
|
|
145
|
-
* -
|
|
146
|
-
*
|
|
148
|
+
* - background tool contexts (`heartbeat`, and every `agent:<id>` sub-agent
|
|
149
|
+
* run — see `core/agents/context.ts`): frontend tools + all loaded plugins,
|
|
150
|
+
* the full surface these runs need to post messages, react, read history
|
|
151
|
+
* and reach their own agent tools. The servers are keyed by the context
|
|
152
|
+
* label itself, so each sub-agent gets its own hub session and its tool
|
|
153
|
+
* calls arrive at the gateway identified as that agent.
|
|
147
154
|
* - "dream": only mempalace (when configured) — dream is a memory
|
|
148
155
|
* consolidation pass and shouldn't be doing outbound messaging.
|
|
149
156
|
* - anything else: empty (treat unknown contexts as plugin-free).
|
|
@@ -154,16 +161,19 @@ export async function runOneShotAgent(
|
|
|
154
161
|
* servers still load and the agent runs normally.
|
|
155
162
|
*/
|
|
156
163
|
function assembleMcpServers(contextLabel: string): Record<string, unknown> {
|
|
157
|
-
if (contextLabel
|
|
164
|
+
if (isBackgroundToolContext(contextLabel)) {
|
|
158
165
|
let frontendServers: Record<string, unknown> = {};
|
|
159
166
|
try {
|
|
160
|
-
frontendServers = buildMcpServers(
|
|
167
|
+
frontendServers = buildMcpServers(contextLabel) as Record<
|
|
168
|
+
string,
|
|
169
|
+
unknown
|
|
170
|
+
>;
|
|
161
171
|
} catch {
|
|
162
172
|
frontendServers = {};
|
|
163
173
|
}
|
|
164
174
|
let pluginServers: Record<string, unknown> = {};
|
|
165
175
|
try {
|
|
166
|
-
pluginServers = buildPluginMcpServers(
|
|
176
|
+
pluginServers = buildPluginMcpServers(contextLabel);
|
|
167
177
|
} catch {
|
|
168
178
|
pluginServers = {};
|
|
169
179
|
}
|
|
@@ -183,6 +193,7 @@ function assembleMcpServers(contextLabel: string): Record<string, unknown> {
|
|
|
183
193
|
async function formatAndAppendMessage(
|
|
184
194
|
appendLog: (text: string) => Promise<void>,
|
|
185
195
|
msg: SDKMessage,
|
|
196
|
+
onAssistantText?: OneShotAgentParams["onAssistantText"],
|
|
186
197
|
): Promise<void> {
|
|
187
198
|
try {
|
|
188
199
|
const ts = new Date().toISOString().slice(11, 19);
|
|
@@ -200,7 +211,11 @@ async function formatAndAppendMessage(
|
|
|
200
211
|
});
|
|
201
212
|
|
|
202
213
|
if (textBlocks.length > 0) {
|
|
203
|
-
|
|
214
|
+
const assistantText = textBlocks.join("\n");
|
|
215
|
+
// Report before the log write: an append failure (full disk, closed
|
|
216
|
+
// handle) must not also swallow the run's result for a hook caller.
|
|
217
|
+
emitAssistantText(onAssistantText, assistantText);
|
|
218
|
+
await appendLog(`\n## [${ts}] Assistant\n${assistantText}\n`);
|
|
204
219
|
}
|
|
205
220
|
if (toolUseBlocks.length > 0) {
|
|
206
221
|
await appendLog(`\n${toolUseBlocks.join("\n\n")}\n`);
|
|
@@ -208,6 +223,14 @@ async function formatAndAppendMessage(
|
|
|
208
223
|
break;
|
|
209
224
|
}
|
|
210
225
|
case "result": {
|
|
226
|
+
// Deliberately NOT reported through `onAssistantText`. The SDK's
|
|
227
|
+
// terminal `result` message restates the last assistant turn's text
|
|
228
|
+
// (`subtype: "success"`) or carries an error string (the
|
|
229
|
+
// `error_*` subtypes) — never anything the `assistant` case above
|
|
230
|
+
// has not already emitted. Reporting it too would hand every hook
|
|
231
|
+
// caller a duplicate final segment, and the truncated copy at that
|
|
232
|
+
// (2000 chars, below). The log keeps it because a run log wants the
|
|
233
|
+
// settlement line; a sub-agent result does not.
|
|
211
234
|
const result =
|
|
212
235
|
"result" in msg
|
|
213
236
|
? (msg as { result: string }).result
|
|
@@ -422,6 +422,15 @@ function readResultError(msg: SDKResultMessage, state: StreamState): void {
|
|
|
422
422
|
.filter((e) => typeof e === "string" && !e.startsWith("[ede_diagnostic]"))
|
|
423
423
|
.join("; ")
|
|
424
424
|
.slice(0, 500);
|
|
425
|
+
// A known startup failure (SDK ≥ 0.3.274) names its cause — the CLI
|
|
426
|
+
// never ran a turn, so nothing else in the result says why. Lead with
|
|
427
|
+
// it; the diagnostics text is the same line stderr carried.
|
|
428
|
+
if (msg.startup_failure_reason) {
|
|
429
|
+
state.resultErrorText =
|
|
430
|
+
`Claude Code failed to start (${msg.startup_failure_reason})` +
|
|
431
|
+
(diagnostics ? `: ${diagnostics}` : "");
|
|
432
|
+
return;
|
|
433
|
+
}
|
|
425
434
|
state.resultErrorText =
|
|
426
435
|
state.lastTrailingText.trim() ||
|
|
427
436
|
diagnostics ||
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
import type { OneShotAgentParams, OneShotUsage } from "../../core/types.js";
|
|
21
21
|
import { log, logWarn } from "../../util/log.js";
|
|
22
22
|
import { appendBackendSuffix } from "../runtime/index.js";
|
|
23
|
+
import { emitAssistantText } from "../runtime/one-shot-hooks.js";
|
|
23
24
|
import { ensureCodex, getCodexAuthInfo } from "./init.js";
|
|
24
25
|
import {
|
|
25
26
|
CODEX_SYSTEM_PROMPT_SUFFIX,
|
|
@@ -79,6 +80,7 @@ export async function runOneShotAgent(
|
|
|
79
80
|
contextLabel,
|
|
80
81
|
abortController,
|
|
81
82
|
appendLog,
|
|
83
|
+
onAssistantText,
|
|
82
84
|
} = params;
|
|
83
85
|
|
|
84
86
|
const codex = ensureCodex(contextLabel);
|
|
@@ -143,7 +145,7 @@ export async function runOneShotAgent(
|
|
|
143
145
|
let usage: OneShotUsage | undefined;
|
|
144
146
|
for await (const event of events) {
|
|
145
147
|
if (abortController.signal.aborted) break;
|
|
146
|
-
await appendCodexEvent(appendLog, event);
|
|
148
|
+
await appendCodexEvent(appendLog, event, onAssistantText);
|
|
147
149
|
if (event.type === "turn.completed") {
|
|
148
150
|
const u = (event as { usage?: Record<string, number> }).usage;
|
|
149
151
|
if (u) {
|
|
@@ -217,6 +219,7 @@ export async function runOneShotAgent(
|
|
|
217
219
|
async function appendCodexEvent(
|
|
218
220
|
appendLog: (text: string) => Promise<void>,
|
|
219
221
|
event: { type: string } & Record<string, unknown>,
|
|
222
|
+
onAssistantText?: OneShotAgentParams["onAssistantText"],
|
|
220
223
|
): Promise<void> {
|
|
221
224
|
const ts = new Date().toISOString().slice(11, 19);
|
|
222
225
|
|
|
@@ -260,7 +263,7 @@ async function appendCodexEvent(
|
|
|
260
263
|
case "item.completed": {
|
|
261
264
|
const item = (event as unknown as { item?: Record<string, unknown> })
|
|
262
265
|
.item;
|
|
263
|
-
if (item) await appendCodexItem(appendLog, item, ts);
|
|
266
|
+
if (item) await appendCodexItem(appendLog, item, ts, onAssistantText);
|
|
264
267
|
return;
|
|
265
268
|
}
|
|
266
269
|
default:
|
|
@@ -268,17 +271,28 @@ async function appendCodexEvent(
|
|
|
268
271
|
}
|
|
269
272
|
}
|
|
270
273
|
|
|
271
|
-
/**
|
|
274
|
+
/**
|
|
275
|
+
* Append one `ThreadItem` to the run log, and report the model's final
|
|
276
|
+
* answers to the run's optional `onAssistantText` consumer.
|
|
277
|
+
*
|
|
278
|
+
* Only `agent_message` items are reported: `reasoning` items are the model's
|
|
279
|
+
* thinking and the rest are tool/command/diff payloads, none of which is the
|
|
280
|
+
* run's answer.
|
|
281
|
+
*/
|
|
272
282
|
async function appendCodexItem(
|
|
273
283
|
appendLog: (text: string) => Promise<void>,
|
|
274
284
|
item: Record<string, unknown>,
|
|
275
285
|
ts: string,
|
|
286
|
+
onAssistantText?: OneShotAgentParams["onAssistantText"],
|
|
276
287
|
): Promise<void> {
|
|
277
288
|
const type = typeof item.type === "string" ? item.type : "unknown";
|
|
278
289
|
|
|
279
290
|
if (type === "agent_message") {
|
|
280
291
|
const text = typeof item.text === "string" ? item.text : "";
|
|
281
|
-
if (text)
|
|
292
|
+
if (text) {
|
|
293
|
+
emitAssistantText(onAssistantText, text);
|
|
294
|
+
await appendLog(`\n## [${ts}] Assistant\n${text}\n`);
|
|
295
|
+
}
|
|
282
296
|
return;
|
|
283
297
|
}
|
|
284
298
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Shared remote-server backend framework — barrel re-export.
|
|
3
3
|
*
|
|
4
|
-
* Helpers used by
|
|
4
|
+
* Helpers used by the OpenCode and Kilo drivers (both wrap a
|
|
5
5
|
* long-running upstream agent server that exposes a common HTTP API
|
|
6
6
|
* for MCP registration, session lifecycle, tool listing, and provider
|
|
7
7
|
* resolution).
|
|
@@ -21,9 +21,11 @@
|
|
|
21
21
|
* the chat-turn orchestration, and the registry factory composition.
|
|
22
22
|
* This is where the code that used to be copied per backend lives.
|
|
23
23
|
*
|
|
24
|
-
* -
|
|
25
|
-
* `
|
|
26
|
-
*
|
|
24
|
+
* - Profiles (`profiles/bind.ts` + `profiles/{kilo,opencode}.ts`) —
|
|
25
|
+
* `bindRemoteProfile` closes all of the above over one driver's
|
|
26
|
+
* state, and each driver is one file of constants: SDK
|
|
27
|
+
* constructors, port, delivery contract, model-selection parser,
|
|
28
|
+
* model-picker budget.
|
|
27
29
|
*
|
|
28
30
|
* What's NOT here (intentionally):
|
|
29
31
|
*
|
|
@@ -3,9 +3,9 @@
|
|
|
3
3
|
* family (OpenCode + its Kilo fork). Both servers expose identical
|
|
4
4
|
* `/provider/list` + `/provider/auth` wire formats, so the catalog cache,
|
|
5
5
|
* query resolution, presentation, and the `Backend.models` adapter live here
|
|
6
|
-
* once.
|
|
7
|
-
* client + branding/
|
|
8
|
-
*
|
|
6
|
+
* once. `profiles/bind.ts` calls `createRemoteModelCatalogModule` with each
|
|
7
|
+
* driver's SDK client + branding/picker knobs and hangs the result off that
|
|
8
|
+
* profile.
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
import {
|
|
@@ -23,13 +23,7 @@ import type {
|
|
|
23
23
|
RemoteProviderClient,
|
|
24
24
|
} from "./types.js";
|
|
25
25
|
|
|
26
|
-
export type {
|
|
27
|
-
ModelButton,
|
|
28
|
-
RemoteModelCatalog,
|
|
29
|
-
RemoteModelCatalogEntry,
|
|
30
|
-
RemoteModelResolution,
|
|
31
|
-
} from "./types.js";
|
|
32
|
-
export { sortCatalogModels } from "./catalog.js";
|
|
26
|
+
export type { RemoteProviderClient } from "./types.js";
|
|
33
27
|
export {
|
|
34
28
|
getBucketPriority,
|
|
35
29
|
getRemoteModelSelectionValue,
|
|
@@ -3,9 +3,9 @@
|
|
|
3
3
|
* interface (resolveModel, getModelInfo, getProviders, …).
|
|
4
4
|
*
|
|
5
5
|
* One implementation for the whole OpenCode family. The catalog surface is
|
|
6
|
-
* injected (rather than captured from the module factory) so
|
|
7
|
-
* `
|
|
8
|
-
*
|
|
6
|
+
* injected (rather than captured from the module factory) so
|
|
7
|
+
* `profiles/bind.ts` can point it at that driver's own cached catalog —
|
|
8
|
+
* and so a test can point it at a static fixture without a module mock.
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
import type {
|
|
@@ -42,6 +42,7 @@ import {
|
|
|
42
42
|
type RemoteAssistantInfo,
|
|
43
43
|
} from "./session-helpers.js";
|
|
44
44
|
import { appendBackendSuffix, sleep } from "../runtime/index.js";
|
|
45
|
+
import { emitAssistantText } from "../runtime/one-shot-hooks.js";
|
|
45
46
|
import { buildPermissionRuleset } from "./sessions.js";
|
|
46
47
|
|
|
47
48
|
// ── Client surface ──────────────────────────────────────────────────────────
|
|
@@ -108,6 +109,7 @@ export async function runRemoteOneShotAgent<
|
|
|
108
109
|
contextLabel,
|
|
109
110
|
abortController,
|
|
110
111
|
appendLog,
|
|
112
|
+
onAssistantText,
|
|
111
113
|
} = params;
|
|
112
114
|
const { label, errMsg } = bindings;
|
|
113
115
|
|
|
@@ -210,7 +212,7 @@ export async function runRemoteOneShotAgent<
|
|
|
210
212
|
: [];
|
|
211
213
|
|
|
212
214
|
for (const part of parts) {
|
|
213
|
-
await appendResponsePart(appendLog, part);
|
|
215
|
+
await appendResponsePart(appendLog, part, onAssistantText);
|
|
214
216
|
}
|
|
215
217
|
|
|
216
218
|
// The prompt response's assistant info carries the run's token usage —
|
|
@@ -250,17 +252,28 @@ export async function runRemoteOneShotAgent<
|
|
|
250
252
|
|
|
251
253
|
// ── Run-log rendering ───────────────────────────────────────────────────────
|
|
252
254
|
|
|
253
|
-
/**
|
|
255
|
+
/**
|
|
256
|
+
* Render one response part into the Markdown run log, and report the
|
|
257
|
+
* assistant's final text to the run's optional `onAssistantText` consumer.
|
|
258
|
+
*
|
|
259
|
+
* Only the `text` parts are reported: `reasoning` parts are the model's
|
|
260
|
+
* thinking and tool parts are call payloads, neither of which is the run's
|
|
261
|
+
* answer.
|
|
262
|
+
*/
|
|
254
263
|
async function appendResponsePart(
|
|
255
264
|
appendLog: (text: string) => Promise<void>,
|
|
256
265
|
part: Record<string, unknown>,
|
|
266
|
+
onAssistantText?: OneShotAgentParams["onAssistantText"],
|
|
257
267
|
): Promise<void> {
|
|
258
268
|
const ts = new Date().toISOString().slice(11, 19);
|
|
259
269
|
const type = typeof part.type === "string" ? part.type : "unknown";
|
|
260
270
|
|
|
261
271
|
if (type === "text") {
|
|
262
272
|
const text = typeof part.text === "string" ? part.text : "";
|
|
263
|
-
if (text)
|
|
273
|
+
if (text) {
|
|
274
|
+
emitAssistantText(onAssistantText, text);
|
|
275
|
+
await appendLog(`\n## [${ts}] Assistant\n${text}\n`);
|
|
276
|
+
}
|
|
264
277
|
return;
|
|
265
278
|
}
|
|
266
279
|
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bind one remote-server driver from a profile.
|
|
3
|
+
*
|
|
4
|
+
* Everything a member of this family does — spawn or reuse the local
|
|
5
|
+
* server, register the chat and plugin MCP servers, create sessions,
|
|
6
|
+
* fetch and render the model catalog, run a chat turn, run a one-shot
|
|
7
|
+
* turn, read back a session snapshot — is shared code in
|
|
8
|
+
* `backend/remote-server/`. What differs between OpenCode and its Kilo
|
|
9
|
+
* fork is a short list of constants and two SDK constructors.
|
|
10
|
+
*
|
|
11
|
+
* `bindRemoteProfile` is where that list becomes a driver. It closes the
|
|
12
|
+
* shared helpers over one profile's state and returns a single object
|
|
13
|
+
* that is simultaneously:
|
|
14
|
+
*
|
|
15
|
+
* - the {@link RemoteServerBindings} the shared turn paths need,
|
|
16
|
+
* - the bound model catalog + `Backend.models` adapter, and
|
|
17
|
+
* - a `RemoteBackendFactoryInputs`, ready for
|
|
18
|
+
* `createRemoteBackendFactory` in `backend/builtins.ts`.
|
|
19
|
+
*
|
|
20
|
+
* Before this module each backend carried a `server.ts`, `sessions.ts`,
|
|
21
|
+
* `models/index.ts`, `model-provider.ts`, `handler/message.ts`,
|
|
22
|
+
* `one-shot.ts`, `index.ts` and `factory.ts` whose entire content was
|
|
23
|
+
* re-exporting these bindings under backend-prefixed names. Those 16
|
|
24
|
+
* files are the profile objects in `./kilo.ts` and `./opencode.ts` now.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import type { BackendId } from "../../../core/agent-runtime/model-ref.js";
|
|
28
|
+
import type { OneShotAgentParams, OneShotUsage } from "../../../core/types.js";
|
|
29
|
+
import type {
|
|
30
|
+
QueryParams,
|
|
31
|
+
QueryResult,
|
|
32
|
+
} from "../../runtime/turn/handler-types.js";
|
|
33
|
+
import type { DeliveryMode } from "../../runtime/prompt/delivery-contract.js";
|
|
34
|
+
import { runRemoteChatTurn } from "../chat-turn.js";
|
|
35
|
+
import type { RemoteAgentClient } from "../client.js";
|
|
36
|
+
import type { RemoteBackendFactoryInputs } from "../factory.js";
|
|
37
|
+
import {
|
|
38
|
+
createRemoteModelCatalogModule,
|
|
39
|
+
createRemoteModelProvider,
|
|
40
|
+
formatRemoteUnavailableModel,
|
|
41
|
+
getRemoteModelSelectionValue,
|
|
42
|
+
resolveRemoteModelInput,
|
|
43
|
+
type RemoteModelCatalogModule,
|
|
44
|
+
type RemoteProviderClient,
|
|
45
|
+
} from "../model-catalog/index.js";
|
|
46
|
+
import {
|
|
47
|
+
runRemoteOneShotAgent,
|
|
48
|
+
type RemoteOneShotClient,
|
|
49
|
+
} from "../one-shot.js";
|
|
50
|
+
import {
|
|
51
|
+
bindRemoteServer,
|
|
52
|
+
type RemoteModelSelection,
|
|
53
|
+
type RemoteServerBindings,
|
|
54
|
+
} from "../server-bindings.js";
|
|
55
|
+
import {
|
|
56
|
+
getSessionSnapshot,
|
|
57
|
+
type RemoteSessionClient,
|
|
58
|
+
} from "../session-helpers.js";
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* The client shape a profile's SDK must satisfy: the shared helper
|
|
62
|
+
* surface, the provider catalog, and the session lifecycle the one-shot
|
|
63
|
+
* runner drives directly. Both `OpencodeClient` and `KiloClient` match
|
|
64
|
+
* structurally.
|
|
65
|
+
*/
|
|
66
|
+
export type RemoteProfileClient = RemoteAgentClient &
|
|
67
|
+
RemoteOneShotClient &
|
|
68
|
+
RemoteProviderClient;
|
|
69
|
+
|
|
70
|
+
/** Everything that differs between two members of this family. */
|
|
71
|
+
export interface RemoteProfileDefinition<TClient extends RemoteProfileClient> {
|
|
72
|
+
/** Registry id — matches `config.backend` ("kilo"). */
|
|
73
|
+
id: BackendId;
|
|
74
|
+
/** Display label for log lines, headers and error text ("Kilo"). */
|
|
75
|
+
label: string;
|
|
76
|
+
/** npm package of the SDK, for the startup log line. */
|
|
77
|
+
sdkPackage: string;
|
|
78
|
+
/** Loopback port the local server listens on by default. */
|
|
79
|
+
defaultPort: number;
|
|
80
|
+
/**
|
|
81
|
+
* Env var that overrides the port, so integration tests can spawn an
|
|
82
|
+
* isolated server alongside a running production Talon that holds the
|
|
83
|
+
* default.
|
|
84
|
+
*/
|
|
85
|
+
portEnv: string;
|
|
86
|
+
/** Delivery contract the system-prompt suffix carries. */
|
|
87
|
+
deliveryContract: DeliveryMode;
|
|
88
|
+
/** Strict SDK client over an already-running server URL. */
|
|
89
|
+
createClient(baseUrl: string): TClient;
|
|
90
|
+
/** Spawn a fresh local server; `close()` runs from `stop()`. */
|
|
91
|
+
createServer(args: {
|
|
92
|
+
hostname: string;
|
|
93
|
+
port: number;
|
|
94
|
+
timeout: number;
|
|
95
|
+
}): Promise<{ url: string; close(): void }>;
|
|
96
|
+
/**
|
|
97
|
+
* Split a stored model-selection string into provider/model ids. The
|
|
98
|
+
* one genuinely behavioural knob: the two upstream routers disagree
|
|
99
|
+
* about what a `provider/model` prefix means.
|
|
100
|
+
*/
|
|
101
|
+
parseModelSelection(value: string): RemoteModelSelection;
|
|
102
|
+
/**
|
|
103
|
+
* Model-picker budget, set by the surface the picker renders through.
|
|
104
|
+
* Discord StringSelectMenu values hold 100 chars of anything; Telegram
|
|
105
|
+
* `callback_data` holds 64 bytes and the keyboard is tight.
|
|
106
|
+
*/
|
|
107
|
+
maxCallbackIdLength: number;
|
|
108
|
+
allowCallbackSeparators: boolean;
|
|
109
|
+
quickPickLimit: number;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* A bound driver: the server bindings, the catalog module, and the
|
|
114
|
+
* registry factory inputs, in one object.
|
|
115
|
+
*/
|
|
116
|
+
export type RemoteProfile<TClient extends RemoteProfileClient> =
|
|
117
|
+
RemoteServerBindings<TClient> &
|
|
118
|
+
RemoteBackendFactoryInputs & {
|
|
119
|
+
/** The bound catalog — cache, resolution, and picker rendering. */
|
|
120
|
+
catalog: RemoteModelCatalogModule;
|
|
121
|
+
/**
|
|
122
|
+
* The knobs this driver was built from. Kept on the result so a
|
|
123
|
+
* live-backend test can rebind the catalog to its own throwaway
|
|
124
|
+
* server without restating the profile's picker budget.
|
|
125
|
+
*/
|
|
126
|
+
definition: RemoteProfileDefinition<TClient>;
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
export function bindRemoteProfile<TClient extends RemoteProfileClient>(
|
|
130
|
+
definition: RemoteProfileDefinition<TClient>,
|
|
131
|
+
): RemoteProfile<TClient> {
|
|
132
|
+
const { id, label, sdkPackage } = definition;
|
|
133
|
+
|
|
134
|
+
const server = bindRemoteServer<TClient>({
|
|
135
|
+
label,
|
|
136
|
+
defaultPort: definition.defaultPort,
|
|
137
|
+
portEnv: definition.portEnv,
|
|
138
|
+
deliveryContract: definition.deliveryContract,
|
|
139
|
+
createClient: definition.createClient,
|
|
140
|
+
createServer: definition.createServer,
|
|
141
|
+
parseModelSelection: definition.parseModelSelection,
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
const catalog = createRemoteModelCatalogModule({
|
|
145
|
+
label,
|
|
146
|
+
getClient: () => server.ensureServer(),
|
|
147
|
+
maxCallbackIdLength: definition.maxCallbackIdLength,
|
|
148
|
+
allowCallbackSeparators: definition.allowCallbackSeparators,
|
|
149
|
+
quickPickLimit: definition.quickPickLimit,
|
|
150
|
+
});
|
|
151
|
+
// A stopped server invalidates the catalog it served.
|
|
152
|
+
server.onServerStop(catalog.clearCache);
|
|
153
|
+
|
|
154
|
+
const models = createRemoteModelProvider({
|
|
155
|
+
label,
|
|
156
|
+
getCatalog: (forceRefresh) => catalog.getCatalog(forceRefresh),
|
|
157
|
+
getModelInfo: (modelId) => catalog.getModelInfo(modelId),
|
|
158
|
+
resolveModelInput: (query, cat) => resolveRemoteModelInput(query, cat),
|
|
159
|
+
getSelectionValue: (model, cat) => getRemoteModelSelectionValue(model, cat),
|
|
160
|
+
formatUnavailableModel: (model) => formatRemoteUnavailableModel(model),
|
|
161
|
+
getSettingsPresentation: (activeModel, pickerOptions) =>
|
|
162
|
+
catalog.getSettingsPresentation(activeModel, pickerOptions),
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
const handleMessage = (params: QueryParams): Promise<QueryResult> =>
|
|
166
|
+
runRemoteChatTurn(
|
|
167
|
+
{
|
|
168
|
+
id,
|
|
169
|
+
label,
|
|
170
|
+
getConfig: server.getConfig,
|
|
171
|
+
ensureServer: server.ensureServer,
|
|
172
|
+
trackActiveTurn: server.trackActiveTurn,
|
|
173
|
+
parseModelSelection: server.parseModelSelection,
|
|
174
|
+
resolveProviderID: server.resolveProviderID,
|
|
175
|
+
ensureSession: server.ensureSession,
|
|
176
|
+
ensureChatMcpServer: server.ensureChatMcpServer,
|
|
177
|
+
ensurePluginMcpServers: server.ensurePluginMcpServers,
|
|
178
|
+
buildToolOverrides: server.buildToolOverrides,
|
|
179
|
+
systemPromptSuffix: server.systemPromptSuffix,
|
|
180
|
+
},
|
|
181
|
+
params,
|
|
182
|
+
);
|
|
183
|
+
|
|
184
|
+
const runOneShotAgent = (
|
|
185
|
+
params: OneShotAgentParams,
|
|
186
|
+
): Promise<OneShotUsage | void> =>
|
|
187
|
+
runRemoteOneShotAgent(
|
|
188
|
+
{
|
|
189
|
+
label,
|
|
190
|
+
// The one-shot runner has no frontend in hand (heartbeat and
|
|
191
|
+
// dream are cross-surface), so it carries the telegram-shaped
|
|
192
|
+
// suffix — the same one the per-backend runners passed.
|
|
193
|
+
systemPromptSuffix: server.defaultSystemPromptSuffix,
|
|
194
|
+
ensureServer: server.ensureServer,
|
|
195
|
+
parseModelSelection: server.parseModelSelection,
|
|
196
|
+
resolveProviderID: server.resolveProviderID,
|
|
197
|
+
ensureChatMcpServer: server.ensureChatMcpServer,
|
|
198
|
+
ensurePluginMcpServers: server.ensurePluginMcpServers,
|
|
199
|
+
buildToolOverrides: server.buildToolOverrides,
|
|
200
|
+
disconnectChatMcpServer: server.disconnectChatMcpServer,
|
|
201
|
+
errMsg: server.errMsg,
|
|
202
|
+
},
|
|
203
|
+
params,
|
|
204
|
+
);
|
|
205
|
+
|
|
206
|
+
return {
|
|
207
|
+
...server,
|
|
208
|
+
id,
|
|
209
|
+
label,
|
|
210
|
+
sdkPackage,
|
|
211
|
+
definition,
|
|
212
|
+
catalog,
|
|
213
|
+
models,
|
|
214
|
+
handleMessage,
|
|
215
|
+
runOneShotAgent,
|
|
216
|
+
async getSessionSnapshot(sessionId) {
|
|
217
|
+
if (!sessionId) return undefined;
|
|
218
|
+
const oc = await server.ensureServer();
|
|
219
|
+
return getSessionSnapshot(
|
|
220
|
+
oc as unknown as RemoteSessionClient,
|
|
221
|
+
sessionId,
|
|
222
|
+
);
|
|
223
|
+
},
|
|
224
|
+
};
|
|
225
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Remote-server profiles — the drivers built from `./bind.ts`.
|
|
3
|
+
*
|
|
4
|
+
* One import for `backend/builtins.ts`, which is the only place that
|
|
5
|
+
* lists them (structure rule 4). Adding a member of this family is
|
|
6
|
+
* adding a profile module here and a line there.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export { kiloProfile } from "./kilo.js";
|
|
10
|
+
export { opencodeProfile } from "./opencode.js";
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Kilo — a remote-server profile.
|
|
3
|
+
*
|
|
4
|
+
* Kilo is a fork of OpenCode and exposes the same HTTP API, so the whole
|
|
5
|
+
* driver is `bindRemoteProfile` over the constants below: the
|
|
6
|
+
* `@kilocode/sdk` constructors, port 4097, the text-or-tools delivery
|
|
7
|
+
* contract, the `kilo/`-prefix model parser, and the Discord-sized model
|
|
8
|
+
* picker budget.
|
|
9
|
+
*
|
|
10
|
+
* Kilo delivery model: the reply reaches the user either as a `text`
|
|
11
|
+
* part (what most Kilo-routed models emit by default — DeepSeek, GLM,
|
|
12
|
+
* openrouter routes) or through a delivery tool (`end_turn` / `send` /
|
|
13
|
+
* `react`) when reply-to targeting, buttons, photos, or polls are
|
|
14
|
+
* needed. Both routes work; the shared text-or-tools contract documents
|
|
15
|
+
* the choice.
|
|
16
|
+
*
|
|
17
|
+
* Note: the model catalog's internal type names are `Remote*` and its
|
|
18
|
+
* wire shape is OpenCode's — Kilo's provider-bucket API is forked from
|
|
19
|
+
* it, so the names match what the upstream actually emits.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import {
|
|
23
|
+
createKiloClient,
|
|
24
|
+
createKiloServer,
|
|
25
|
+
type KiloClient,
|
|
26
|
+
} from "@kilocode/sdk/v2";
|
|
27
|
+
import type { RemoteModelSelection } from "../server-bindings.js";
|
|
28
|
+
import { bindRemoteProfile, type RemoteProfile } from "./bind.js";
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Parse the stored model-selection string into a `{providerID?, modelID}`
|
|
32
|
+
* pair.
|
|
33
|
+
*
|
|
34
|
+
* Kilo model ids frequently contain `/` and `:` inside the model.id itself
|
|
35
|
+
* (e.g. `inclusionai/ling-2.6-1t:free`, `deepseek/deepseek-v4-flash:free`).
|
|
36
|
+
* A naive `provider/model` splitter mis-treats those vendor prefixes as
|
|
37
|
+
* the provider, so we generally return the whole string as the model id
|
|
38
|
+
* and let `resolveProviderID` look up the real provider from the live
|
|
39
|
+
* catalog.
|
|
40
|
+
*
|
|
41
|
+
* Exception: if the value starts with the literal `kilo/` prefix
|
|
42
|
+
* (Talon's old hint that "this is a kilo-routed model"), strip it AND
|
|
43
|
+
* pin providerID to `"kilo"`. Otherwise the upstream Kilo router gets
|
|
44
|
+
* `kilo/deepseek/deepseek-v4-flash:free` as the model id and concats
|
|
45
|
+
* its own provider in front, producing
|
|
46
|
+
* `Model not found: opencode/kilo/deepseek/deepseek-v4-flash:free`.
|
|
47
|
+
*/
|
|
48
|
+
function parseStoredKiloModelSelection(value: string): RemoteModelSelection {
|
|
49
|
+
const trimmed = value.trim();
|
|
50
|
+
if (trimmed.startsWith("kilo/")) {
|
|
51
|
+
return {
|
|
52
|
+
providerID: "kilo",
|
|
53
|
+
modelID: trimmed.slice("kilo/".length),
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
return {
|
|
57
|
+
providerID: undefined,
|
|
58
|
+
modelID: trimmed,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export const kiloProfile: RemoteProfile<KiloClient> =
|
|
63
|
+
bindRemoteProfile<KiloClient>({
|
|
64
|
+
id: "kilo",
|
|
65
|
+
label: "Kilo",
|
|
66
|
+
sdkPackage: "@kilocode/sdk",
|
|
67
|
+
defaultPort: 4097,
|
|
68
|
+
portEnv: "KILO_PORT",
|
|
69
|
+
deliveryContract: "text-or-tools",
|
|
70
|
+
createClient: (baseUrl) =>
|
|
71
|
+
createKiloClient({ baseUrl, throwOnError: true }),
|
|
72
|
+
createServer: ({ hostname, port, timeout }) =>
|
|
73
|
+
createKiloServer({ hostname, port, timeout }),
|
|
74
|
+
parseModelSelection: parseStoredKiloModelSelection,
|
|
75
|
+
// Kilo's model picker renders through Discord StringSelectMenus,
|
|
76
|
+
// which allow 25 options and values up to 100 chars with any
|
|
77
|
+
// characters — Kilo ids routinely contain "/" and ":" (e.g.
|
|
78
|
+
// "inclusionai/ling-2.6-1t:free").
|
|
79
|
+
maxCallbackIdLength: 90,
|
|
80
|
+
allowCallbackSeparators: true,
|
|
81
|
+
quickPickLimit: 24,
|
|
82
|
+
});
|