talon-agent 3.32.1 → 3.33.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 +1 -1
- package/src/backend/claude-sdk/constants.ts +1 -6
- package/src/backend/claude-sdk/index.ts +2 -11
- package/src/backend/claude-sdk/models/parsing.ts +2 -2
- package/src/backend/claude-sdk/plan-usage.ts +0 -5
- package/src/backend/claude-sdk/stream.ts +1 -1
- package/src/backend/codex/auth.ts +2 -2
- package/src/backend/codex/constants.ts +0 -21
- package/src/backend/codex/handler/index.ts +0 -1
- package/src/backend/codex/mcp-config.ts +2 -2
- package/src/backend/codex/plan-usage.ts +0 -5
- package/src/backend/codex/state.ts +1 -1
- package/src/backend/codex/token-usage.ts +1 -1
- package/src/backend/kilo/factory.ts +39 -119
- package/src/backend/kilo/handler/index.ts +1 -11
- package/src/backend/kilo/handler/message.ts +20 -317
- package/src/backend/kilo/index.ts +0 -6
- package/src/backend/kilo/server.ts +55 -292
- package/src/backend/kilo/sessions.ts +5 -86
- package/src/backend/openai-agents/handler/index.ts +0 -1
- package/src/backend/openai-agents/handler/state.ts +0 -5
- package/src/backend/openai-agents/init.ts +1 -18
- package/src/backend/opencode/factory.ts +41 -118
- package/src/backend/opencode/handler/index.ts +1 -11
- package/src/backend/opencode/handler/message.ts +20 -321
- package/src/backend/opencode/index.ts +0 -4
- package/src/backend/opencode/server.ts +56 -225
- package/src/backend/opencode/sessions.ts +5 -86
- package/src/backend/remote-server/chat-turn.ts +359 -0
- package/src/backend/remote-server/client.ts +1 -1
- package/src/backend/remote-server/events.ts +1 -1
- package/src/backend/remote-server/factory.ts +150 -0
- package/src/backend/remote-server/index.ts +15 -43
- package/src/backend/remote-server/mcp.ts +1 -4
- package/src/backend/remote-server/model-catalog/catalog.ts +1 -1
- package/src/backend/remote-server/model-catalog/index.ts +1 -15
- package/src/backend/remote-server/model-catalog/provider.ts +0 -1
- package/src/backend/remote-server/server-bindings.ts +232 -0
- package/src/backend/remote-server/sse-stream.ts +1 -3
- package/src/backend/remote-server/turn-timeout.ts +1 -1
- package/src/backend/{kilo/handler → remote-server}/turn.ts +103 -67
- package/src/backend/shared/index.ts +7 -43
- package/src/cli/logs.ts +1 -1
- package/src/cli/status.ts +1 -1
- package/src/core/agent-runtime/backend-registry.ts +1 -1
- package/src/core/agent-runtime/capabilities.ts +1 -1
- package/src/core/agent-runtime/events.ts +1 -1
- package/src/core/background/heartbeat/agent.ts +1 -1
- package/src/core/background/heartbeat/index.ts +1 -2
- package/src/core/background/isolated-agent.ts +1 -1
- package/src/core/background/triggers/exit.ts +1 -1
- package/src/core/background/triggers/index.ts +1 -1
- package/src/core/bus/events.ts +2 -2
- package/src/core/doctor.ts +1 -1
- package/src/core/engine/backend-controller/index.ts +1 -3
- package/src/core/engine/backend-controller/rebind.ts +2 -2
- package/src/core/engine/dispatcher.ts +1 -15
- package/src/core/engine/gateway-actions/index.ts +0 -3
- package/src/core/engine/gateway-actions/types.ts +1 -1
- package/src/core/frontend-runtime/capabilities.ts +0 -2
- package/src/core/frontend-runtime/index.ts +1 -6
- package/src/core/frontend-runtime/routing.ts +0 -1
- package/src/core/mcp-hub/children.ts +0 -5
- package/src/core/mesh/bridge-links.ts +319 -0
- package/src/core/mesh/common.ts +30 -0
- package/src/core/mesh/device-files.ts +710 -0
- package/src/core/mesh/index.ts +1 -18
- package/src/core/mesh/service.ts +72 -894
- package/src/core/models/active-model.ts +0 -19
- package/src/core/plugin/index.ts +0 -1
- package/src/core/plugin/registry.ts +1 -1
- package/src/core/prompt/templates.ts +0 -5
- package/src/core/scripting/lua-runner.ts +1 -1
- package/src/core/scripts/runner.ts +1 -1
- package/src/core/soul/critic.ts +1 -1
- package/src/core/soul/embedder.ts +2 -2
- package/src/core/soul/governance.ts +1 -1
- package/src/core/soul/hdc.ts +1 -1
- package/src/core/soul/reflex.ts +2 -2
- package/src/core/soul/retrieve.ts +1 -1
- package/src/core/soul/signals.ts +4 -4
- package/src/core/soul/types.ts +1 -1
- package/src/core/tasks/index.ts +0 -4
- package/src/core/tasks/types.ts +1 -1
- package/src/core/tools/index.ts +1 -6
- package/src/core/tools/mcp-env.ts +3 -33
- package/src/core/types.ts +1 -1
- package/src/core/update/self-update.ts +1 -1
- package/src/core/vfs/fusefs.ts +1 -1
- package/src/core/vfs/types.ts +1 -1
- package/src/core/weaver/index.ts +1 -1
- package/src/core/weaver/typing-loop.ts +1 -1
- package/src/frontend/discord/actions/types.ts +1 -1
- package/src/frontend/discord/commands/definitions.ts +1 -1
- package/src/frontend/discord/handlers/access.ts +7 -61
- package/src/frontend/discord/handlers/index.ts +2 -15
- package/src/frontend/discord/handlers/registry.ts +0 -6
- package/src/frontend/discord/handlers/state.ts +15 -8
- package/src/frontend/native/protocol.ts +1 -4
- package/src/frontend/native/server.ts +392 -324
- package/src/frontend/shared/access.ts +152 -0
- package/src/frontend/shared/status-context.ts +1 -1
- package/src/frontend/telegram/actions/types.ts +1 -1
- package/src/frontend/telegram/commands/index.ts +1 -1
- package/src/frontend/telegram/commands/state.ts +1 -1
- package/src/frontend/telegram/handlers/access.ts +8 -25
- package/src/frontend/telegram/handlers/index.ts +0 -1
- package/src/frontend/telegram/handlers/queue.ts +3 -35
- package/src/frontend/telegram/handlers/state.ts +15 -8
- package/src/frontend/telegram/helpers/menu.ts +1 -1
- package/src/frontend/terminal/commands.ts +1 -4
- package/src/frontend/terminal/renderer.ts +1 -1
- package/src/frontend/whatsapp/actions/chat-info.ts +0 -3
- package/src/frontend/whatsapp/actions/messaging.ts +1 -7
- package/src/frontend/whatsapp/actions/shared.ts +1 -1
- package/src/frontend/whatsapp/actions/types.ts +1 -1
- package/src/frontend/whatsapp/formatting.ts +1 -1
- package/src/frontend/whatsapp/media-store.ts +1 -1
- package/src/frontend/whatsapp/pins.ts +0 -5
- package/src/native/fusefs.ts +3 -3
- package/src/native/scheduler-core.ts +1 -1
- package/src/native/warden.ts +2 -2
- package/src/plugins/mempalace/provision.ts +1 -5
- package/src/plugins/playwright/provision.ts +1 -1
- package/src/storage/db.ts +1 -1
- package/src/storage/goal-store.ts +1 -5
- package/src/storage/metrics.ts +0 -5
- package/src/storage/repositories/scripts-repo.ts +0 -5
- package/src/storage/scheduled-store.ts +1 -5
- package/src/storage/script-store.ts +5 -13
- package/src/storage/sessions.ts +0 -3
- package/src/storage/skill-store.ts +3 -3
- package/src/util/exec-output.ts +1 -1
- package/src/util/mcp-launcher.ts +3 -0
- package/src/backend/kilo/events.ts +0 -60
- package/src/backend/kilo/handler/state.ts +0 -9
- package/src/backend/opencode/handler/state.ts +0 -9
- package/src/backend/opencode/handler/turn.ts +0 -314
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The chat-turn orchestration for the remote-server family — one message
|
|
3
|
+
* in, one `QueryResult` out.
|
|
4
|
+
*
|
|
5
|
+
* Resolves the active model against the server's catalog, makes sure the
|
|
6
|
+
* server, session, and this chat's MCP servers exist, builds the prompt
|
|
7
|
+
* pair, drives the turn (`./turn.ts`), then does the post-turn accounting:
|
|
8
|
+
* usage fallback from the session summary, metrics, session id/name
|
|
9
|
+
* persistence, and the shared delivery decision.
|
|
10
|
+
*
|
|
11
|
+
* Kilo and OpenCode ran byte-for-byte copies of this (modulo the backend
|
|
12
|
+
* name in log lines). The copies are gone; `RemoteChatBindings` is the
|
|
13
|
+
* seam a backend supplies, and it is a subset of what `bindRemoteServer`
|
|
14
|
+
* already returns.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import {
|
|
18
|
+
getSession,
|
|
19
|
+
incrementTurns,
|
|
20
|
+
recordUsage,
|
|
21
|
+
setSessionId,
|
|
22
|
+
setSessionName,
|
|
23
|
+
} from "../../storage/sessions.js";
|
|
24
|
+
import { getChatSettings } from "../../storage/chat-settings.js";
|
|
25
|
+
import { log, logError } from "../../util/log.js";
|
|
26
|
+
import { traceMessage } from "../../util/trace.js";
|
|
27
|
+
import type { QueryParams, QueryResult } from "../shared/handler-types.js";
|
|
28
|
+
import { frontendsForChat, nonTerminalFrontends } from "../shared/frontends.js";
|
|
29
|
+
import {
|
|
30
|
+
createStreamState,
|
|
31
|
+
recordTokens,
|
|
32
|
+
finalizeResponseText,
|
|
33
|
+
formatUserPrompt,
|
|
34
|
+
prepareSystemPrompt,
|
|
35
|
+
extractSessionName,
|
|
36
|
+
summarizeUsage,
|
|
37
|
+
routeDelivery,
|
|
38
|
+
applyRetryDecision,
|
|
39
|
+
recordTurnMetrics,
|
|
40
|
+
recordFailedTurnAccounting,
|
|
41
|
+
registerTurnInterrupt,
|
|
42
|
+
} from "../shared/index.js";
|
|
43
|
+
import type { RemoteAgentClient } from "./client.js";
|
|
44
|
+
import type { RemoteServerBindings } from "./server-bindings.js";
|
|
45
|
+
import { getTurnSummary, type RemoteSessionClient } from "./session-helpers.js";
|
|
46
|
+
import { runRemoteTurn, type RemoteTurnClient } from "./turn.js";
|
|
47
|
+
|
|
48
|
+
/** What a backend hands the shared handler: its id/label plus server bindings. */
|
|
49
|
+
export type RemoteChatBindings<TClient extends RemoteAgentClient> = Pick<
|
|
50
|
+
RemoteServerBindings<TClient>,
|
|
51
|
+
| "getConfig"
|
|
52
|
+
| "ensureServer"
|
|
53
|
+
| "parseModelSelection"
|
|
54
|
+
| "resolveProviderID"
|
|
55
|
+
| "ensureSession"
|
|
56
|
+
| "ensureChatMcpServer"
|
|
57
|
+
| "ensurePluginMcpServers"
|
|
58
|
+
| "buildToolOverrides"
|
|
59
|
+
| "systemPromptSuffix"
|
|
60
|
+
> & {
|
|
61
|
+
/** Registry id — the `backend` column in turn metrics ("kilo"). */
|
|
62
|
+
id: string;
|
|
63
|
+
/** Display label for log lines and retry classification ("Kilo"). */
|
|
64
|
+
label: string;
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
export async function runRemoteChatTurn<TClient extends RemoteAgentClient>(
|
|
68
|
+
bindings: RemoteChatBindings<TClient>,
|
|
69
|
+
params: QueryParams,
|
|
70
|
+
retried = false,
|
|
71
|
+
): Promise<QueryResult> {
|
|
72
|
+
const { id, label } = bindings;
|
|
73
|
+
const config = bindings.getConfig();
|
|
74
|
+
if (!config) throw new Error(`${label} agent not initialized`);
|
|
75
|
+
|
|
76
|
+
const {
|
|
77
|
+
chatId,
|
|
78
|
+
text,
|
|
79
|
+
senderName,
|
|
80
|
+
senderHandle,
|
|
81
|
+
isGroup,
|
|
82
|
+
messageId,
|
|
83
|
+
onTextBlock,
|
|
84
|
+
onToolUse,
|
|
85
|
+
} = params;
|
|
86
|
+
const t0 = Date.now();
|
|
87
|
+
const session = getSession(chatId);
|
|
88
|
+
const previousTurns = session.turns;
|
|
89
|
+
|
|
90
|
+
// Resolve active model + provider lookup against the server's catalog.
|
|
91
|
+
const chatSettings = getChatSettings(chatId);
|
|
92
|
+
const activeModel = params.model ?? chatSettings.model ?? config.model;
|
|
93
|
+
const { providerID: selectedProviderID, modelID } =
|
|
94
|
+
bindings.parseModelSelection(activeModel);
|
|
95
|
+
|
|
96
|
+
const oc = await bindings.ensureServer();
|
|
97
|
+
const providerID =
|
|
98
|
+
selectedProviderID ?? (await bindings.resolveProviderID(oc, modelID));
|
|
99
|
+
log(
|
|
100
|
+
"agent",
|
|
101
|
+
`[${chatId}] ${label} model resolved: provider=${providerID} model=${modelID}` +
|
|
102
|
+
(selectedProviderID ? "" : " (provider via catalog lookup)"),
|
|
103
|
+
);
|
|
104
|
+
const sessionId = await bindings.ensureSession(oc, chatId);
|
|
105
|
+
const chatMcpServerName = await bindings.ensureChatMcpServer(oc, chatId);
|
|
106
|
+
const pluginMcpServerNames = await bindings.ensurePluginMcpServers(
|
|
107
|
+
oc,
|
|
108
|
+
chatId,
|
|
109
|
+
);
|
|
110
|
+
const toolOverrides = await bindings.buildToolOverrides(
|
|
111
|
+
oc,
|
|
112
|
+
chatMcpServerName,
|
|
113
|
+
pluginMcpServerNames,
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
// Build the prompt (time tag + sender + msg_id reference).
|
|
117
|
+
const prompt = formatUserPrompt({
|
|
118
|
+
text,
|
|
119
|
+
senderName: senderName ?? "user",
|
|
120
|
+
senderHandle,
|
|
121
|
+
isGroup,
|
|
122
|
+
messageId,
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
// Per-session frozen prompt + this backend's delivery suffix.
|
|
126
|
+
const { text: systemPrompt } = prepareSystemPrompt({
|
|
127
|
+
config,
|
|
128
|
+
previousTurns,
|
|
129
|
+
backendSuffix: bindings.systemPromptSuffix(
|
|
130
|
+
frontendsForChat(chatId, nonTerminalFrontends(config.frontend))[0] ??
|
|
131
|
+
"telegram",
|
|
132
|
+
),
|
|
133
|
+
chatId,
|
|
134
|
+
sessionEpoch: session.createdAt,
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
log("agent", `[${chatId}] <- (${text.length} chars)`);
|
|
138
|
+
traceMessage(chatId, "in", text, { senderName, isGroup });
|
|
139
|
+
|
|
140
|
+
// Bind the stream state to the chat so token mutators mirror counts
|
|
141
|
+
// into the live-turn overlay — /status updates while the turn runs.
|
|
142
|
+
const state = createStreamState(chatId);
|
|
143
|
+
state.newSessionId = sessionId;
|
|
144
|
+
const turnClient = oc as unknown as RemoteTurnClient;
|
|
145
|
+
// A user interrupt is a synthetic turn terminator: marking the flag
|
|
146
|
+
// before aborting the session routes the close through the same clean
|
|
147
|
+
// path a model-fired end_turn takes (MessageAborted swallowed as the
|
|
148
|
+
// expected close, no retry), settling with whatever the turn produced.
|
|
149
|
+
const unregisterInterrupt = registerTurnInterrupt(chatId, async () => {
|
|
150
|
+
state.turnTerminated = true;
|
|
151
|
+
await turnClient.session.abort({ sessionID: sessionId });
|
|
152
|
+
});
|
|
153
|
+
const promptStartedAt = Date.now();
|
|
154
|
+
const seenQuestionIds = new Set<string>();
|
|
155
|
+
const seenPermissionIds = new Set<string>();
|
|
156
|
+
const seenToolCallIds = new Set<string>();
|
|
157
|
+
|
|
158
|
+
const setupMs = Date.now() - t0;
|
|
159
|
+
let promptMs = 0;
|
|
160
|
+
|
|
161
|
+
try {
|
|
162
|
+
// Drive the turn: subscribe to SSE events in parallel with promptAsync,
|
|
163
|
+
// surface tool calls + terminator into shared state, and exit when the
|
|
164
|
+
// turn closes / goes idle. `onStreamDelta` is intentionally NOT
|
|
165
|
+
// forwarded — the frontend contract is "send the final reply once", not
|
|
166
|
+
// live edit_message updates. Final delivery happens through
|
|
167
|
+
// `end_turn` / `send`.
|
|
168
|
+
const turnStart = Date.now();
|
|
169
|
+
await runRemoteTurn({
|
|
170
|
+
label,
|
|
171
|
+
oc: turnClient,
|
|
172
|
+
sessionId,
|
|
173
|
+
prompt,
|
|
174
|
+
systemPrompt,
|
|
175
|
+
providerID,
|
|
176
|
+
modelID,
|
|
177
|
+
state,
|
|
178
|
+
chatId,
|
|
179
|
+
seenQuestionIds,
|
|
180
|
+
seenPermissionIds,
|
|
181
|
+
seenToolCallIds,
|
|
182
|
+
toolOverrides,
|
|
183
|
+
onStreamDelta: undefined,
|
|
184
|
+
onTextBlock,
|
|
185
|
+
onToolUse,
|
|
186
|
+
});
|
|
187
|
+
promptMs = Date.now() - turnStart;
|
|
188
|
+
} catch (err) {
|
|
189
|
+
const outcome = await applyRetryDecision({
|
|
190
|
+
err,
|
|
191
|
+
chatId,
|
|
192
|
+
activeModel,
|
|
193
|
+
retried,
|
|
194
|
+
params,
|
|
195
|
+
recurseWithRetried: (p) => runRemoteChatTurn(bindings, p, true),
|
|
196
|
+
backendLabel: label,
|
|
197
|
+
});
|
|
198
|
+
if (outcome.retry) return outcome.retry;
|
|
199
|
+
|
|
200
|
+
// Terminal failure — account for whatever the turn consumed before
|
|
201
|
+
// dying and drop the live overlay (the retry path above did its own
|
|
202
|
+
// accounting inside the recursive attempt).
|
|
203
|
+
recordFailedTurnAccounting({
|
|
204
|
+
backend: id,
|
|
205
|
+
chatId,
|
|
206
|
+
durationMs: Date.now() - t0,
|
|
207
|
+
toolCalls: state.toolCalls,
|
|
208
|
+
apiCalls: state.numApiCalls,
|
|
209
|
+
model: activeModel,
|
|
210
|
+
usage: {
|
|
211
|
+
inputTokens: state.sdkInputTokens,
|
|
212
|
+
outputTokens: state.sdkOutputTokens,
|
|
213
|
+
cacheRead: state.sdkCacheRead,
|
|
214
|
+
cacheWrite: state.sdkCacheWrite,
|
|
215
|
+
},
|
|
216
|
+
contextTokens: state.contextTokens,
|
|
217
|
+
contextWindow: state.contextWindow,
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
logError(
|
|
221
|
+
"agent",
|
|
222
|
+
`[${chatId}] ${label} error: ${outcome.classified.message}`,
|
|
223
|
+
);
|
|
224
|
+
throw outcome.classified;
|
|
225
|
+
} finally {
|
|
226
|
+
unregisterInterrupt();
|
|
227
|
+
// Note: we deliberately do NOT disconnect the chat MCP server here.
|
|
228
|
+
// The server is named per-chat so it's safe to keep across turns;
|
|
229
|
+
// re-spawning the subprocess each turn cost ~800ms per message.
|
|
230
|
+
// Per-prompt overrides isolate visibility across concurrent chats.
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// ── Post-loop accounting ──────────────────────────────────────────────────
|
|
234
|
+
|
|
235
|
+
// If the SSE loop missed any usage info, fall back to the session
|
|
236
|
+
// summary endpoint (which always reflects the final server state).
|
|
237
|
+
if (
|
|
238
|
+
state.sdkInputTokens === 0 &&
|
|
239
|
+
state.sdkOutputTokens === 0 &&
|
|
240
|
+
state.sdkCacheRead === 0
|
|
241
|
+
) {
|
|
242
|
+
try {
|
|
243
|
+
const summary = await getTurnSummary(
|
|
244
|
+
oc as unknown as RemoteSessionClient,
|
|
245
|
+
sessionId,
|
|
246
|
+
promptStartedAt,
|
|
247
|
+
);
|
|
248
|
+
if (summary.usage.assistantMessages > 0) {
|
|
249
|
+
recordTokens(state, {
|
|
250
|
+
inputTokens: summary.usage.inputTokens,
|
|
251
|
+
outputTokens: summary.usage.outputTokens,
|
|
252
|
+
cacheRead: summary.usage.cacheRead,
|
|
253
|
+
cacheWrite: summary.usage.cacheWrite,
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
} catch {
|
|
257
|
+
// best-effort — session summaries can race on cancellation
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const responseText = finalizeResponseText(state);
|
|
262
|
+
const durationMs = Date.now() - t0;
|
|
263
|
+
recordTurnMetrics({
|
|
264
|
+
chatId,
|
|
265
|
+
backend: id,
|
|
266
|
+
durationMs,
|
|
267
|
+
toolCalls: state.toolCalls,
|
|
268
|
+
apiCalls: state.numApiCalls,
|
|
269
|
+
usage: {
|
|
270
|
+
inputTokens: state.sdkInputTokens,
|
|
271
|
+
outputTokens: state.sdkOutputTokens,
|
|
272
|
+
cacheRead: state.sdkCacheRead,
|
|
273
|
+
cacheWrite: state.sdkCacheWrite,
|
|
274
|
+
},
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
if (state.newSessionId) {
|
|
278
|
+
// setSessionId tolerates "same id" calls — keep it simple.
|
|
279
|
+
const stored = getSession(chatId).sessionId;
|
|
280
|
+
if (stored !== state.newSessionId) {
|
|
281
|
+
setSessionId(chatId, state.newSessionId);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
incrementTurns(chatId);
|
|
286
|
+
recordUsage(chatId, {
|
|
287
|
+
inputTokens: state.sdkInputTokens,
|
|
288
|
+
outputTokens: state.sdkOutputTokens,
|
|
289
|
+
cacheRead: state.sdkCacheRead,
|
|
290
|
+
cacheWrite: state.sdkCacheWrite,
|
|
291
|
+
durationMs,
|
|
292
|
+
model: activeModel,
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
// Set a descriptive session name from the user's first message.
|
|
296
|
+
if (previousTurns === 0) {
|
|
297
|
+
const name = extractSessionName(text);
|
|
298
|
+
if (name) setSessionName(chatId, name);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// ── Delivery — the decision tree shared by every backend ──────────────────
|
|
302
|
+
const delivery = await routeDelivery({
|
|
303
|
+
backendLabel: label,
|
|
304
|
+
chatId,
|
|
305
|
+
state,
|
|
306
|
+
responseText,
|
|
307
|
+
onTextBlock,
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
log(
|
|
311
|
+
"agent",
|
|
312
|
+
`[${chatId}] delivery: ${delivery.route} (${delivery.chars} chars)`,
|
|
313
|
+
);
|
|
314
|
+
|
|
315
|
+
log(
|
|
316
|
+
"agent",
|
|
317
|
+
`[${chatId}] -> (${summarizeUsage(
|
|
318
|
+
{
|
|
319
|
+
inputTokens: state.sdkInputTokens,
|
|
320
|
+
outputTokens: state.sdkOutputTokens,
|
|
321
|
+
cacheRead: state.sdkCacheRead,
|
|
322
|
+
cacheWrite: state.sdkCacheWrite,
|
|
323
|
+
},
|
|
324
|
+
{ durationMs, toolCalls: state.toolCalls },
|
|
325
|
+
)} terminator=${state.turnTerminated ? "yes" : "no"} ` +
|
|
326
|
+
`delivered=${state.deliveredTextNorms.length} ` +
|
|
327
|
+
`respLen=${responseText.length} ` +
|
|
328
|
+
`setup=${setupMs}ms turn=${promptMs}ms ` +
|
|
329
|
+
`events=${formatEventCounts(state.eventCounts)})`,
|
|
330
|
+
);
|
|
331
|
+
traceMessage(chatId, "out", responseText, {
|
|
332
|
+
durationMs,
|
|
333
|
+
toolCalls: state.toolCalls,
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
return {
|
|
337
|
+
text: responseText,
|
|
338
|
+
durationMs,
|
|
339
|
+
inputTokens: state.sdkInputTokens,
|
|
340
|
+
outputTokens: state.sdkOutputTokens,
|
|
341
|
+
cacheRead: state.sdkCacheRead,
|
|
342
|
+
cacheWrite: state.sdkCacheWrite,
|
|
343
|
+
};
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* Compact summary of which SSE event types fired this turn. `none` for a
|
|
348
|
+
* silent turn (itself a useful diagnostic — the SSE socket dropped or never
|
|
349
|
+
* matched our session id). Otherwise renders as `delta×42,part.updated×1`.
|
|
350
|
+
*/
|
|
351
|
+
function formatEventCounts(counts: Record<string, number>): string {
|
|
352
|
+
const entries = Object.entries(counts);
|
|
353
|
+
if (entries.length === 0) return "none";
|
|
354
|
+
// Trim the noisy `message.` / `session.` prefixes so the line stays
|
|
355
|
+
// readable in the live log tail.
|
|
356
|
+
return entries
|
|
357
|
+
.map(([type, n]) => `${type.replace(/^(message|session)\./, "")}×${n}`)
|
|
358
|
+
.join(",");
|
|
359
|
+
}
|
|
@@ -365,7 +365,7 @@ export function maybeFireStreamDelta(
|
|
|
365
365
|
// ── Sync-response parts backfill ───────────────────────────────────────────
|
|
366
366
|
|
|
367
367
|
/** Shape returned by a backend's parts-summary extractor. */
|
|
368
|
-
|
|
368
|
+
interface PartsSummary {
|
|
369
369
|
text: string;
|
|
370
370
|
toolCalls: number;
|
|
371
371
|
syntheticErrorText?: string;
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compose a registry `BackendFactory` for a remote-server backend.
|
|
3
|
+
*
|
|
4
|
+
* The capability wiring is identical for every member of the family:
|
|
5
|
+
* chat through the shared handler, one-shot through the shared runner, the
|
|
6
|
+
* catalog-driven model slots, session snapshots off the server, plugin-MCP
|
|
7
|
+
* refresh, warm-after-reset, and the system-prompt control. A backend
|
|
8
|
+
* supplies its id/label/SDK name and the bound functions; this returns the
|
|
9
|
+
* factory for `registerBackend`.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import type {
|
|
13
|
+
BackendFactory,
|
|
14
|
+
FrontendName,
|
|
15
|
+
} from "../../core/agent-runtime/backend-registry.js";
|
|
16
|
+
import type { BackendId } from "../../core/agent-runtime/model-ref.js";
|
|
17
|
+
import {
|
|
18
|
+
composeBackend,
|
|
19
|
+
type BackgroundRunner,
|
|
20
|
+
type ChatBackend,
|
|
21
|
+
type ModelCatalog,
|
|
22
|
+
type SessionBackend,
|
|
23
|
+
type SystemControl,
|
|
24
|
+
type ToolRuntime,
|
|
25
|
+
type UsageTelemetry,
|
|
26
|
+
} from "../../core/agent-runtime/capabilities.js";
|
|
27
|
+
import type { OneShotAgentParams, OneShotUsage } from "../../core/types.js";
|
|
28
|
+
import type { TalonConfig } from "../../util/config.js";
|
|
29
|
+
import { log } from "../../util/log.js";
|
|
30
|
+
import { handlerToEvents } from "../shared/handler-to-events.js";
|
|
31
|
+
import type { QueryParams, QueryResult } from "../shared/handler-types.js";
|
|
32
|
+
import { interruptChatTurn } from "../shared/turn-interrupt.js";
|
|
33
|
+
import type { RemoteModelProvider } from "./model-catalog/provider.js";
|
|
34
|
+
import type { RemoteSessionSnapshot } from "./session-helpers.js";
|
|
35
|
+
|
|
36
|
+
export interface RemoteBackendFactoryInputs {
|
|
37
|
+
/** Registry id — matches `config.backend` ("kilo"). */
|
|
38
|
+
id: BackendId;
|
|
39
|
+
/** Display label ("Kilo"). */
|
|
40
|
+
label: string;
|
|
41
|
+
/** npm package of the SDK, for the startup log line. */
|
|
42
|
+
sdkPackage: string;
|
|
43
|
+
init(
|
|
44
|
+
config: TalonConfig,
|
|
45
|
+
getGatewayPort?: () => number,
|
|
46
|
+
frontend?: FrontendName,
|
|
47
|
+
): void;
|
|
48
|
+
stop(): void;
|
|
49
|
+
handleMessage(params: QueryParams): Promise<QueryResult>;
|
|
50
|
+
runOneShotAgent(params: OneShotAgentParams): Promise<OneShotUsage | void>;
|
|
51
|
+
getSessionSnapshot(
|
|
52
|
+
sessionId: string | undefined,
|
|
53
|
+
): Promise<RemoteSessionSnapshot | undefined>;
|
|
54
|
+
models: RemoteModelProvider;
|
|
55
|
+
refreshPluginMcpServers: ToolRuntime["refreshTools"];
|
|
56
|
+
warmSession(chatId: string): Promise<void>;
|
|
57
|
+
updateSystemPrompt(prompt: string): void;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function createRemoteBackendFactory(
|
|
61
|
+
inputs: RemoteBackendFactoryInputs,
|
|
62
|
+
): BackendFactory {
|
|
63
|
+
const { id, label } = inputs;
|
|
64
|
+
return {
|
|
65
|
+
id,
|
|
66
|
+
label,
|
|
67
|
+
async init(config, ctx) {
|
|
68
|
+
inputs.init(config, ctx.getBridgePort, ctx.frontendName);
|
|
69
|
+
log("bot", `Backend: ${label} (${inputs.sdkPackage})`);
|
|
70
|
+
|
|
71
|
+
const chat: ChatBackend = {
|
|
72
|
+
runChatTurn: (params) =>
|
|
73
|
+
handlerToEvents((p) => inputs.handleMessage(p), params),
|
|
74
|
+
interruptChatTurn: (chatId) => interruptChatTurn(chatId),
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const background: BackgroundRunner = {
|
|
78
|
+
runOneShotAgent: (p) => inputs.runOneShotAgent(p),
|
|
79
|
+
// A long-lived HTTP server — no per-query subprocesses, so
|
|
80
|
+
// `evictOrphanSubprocesses` is intentionally not implemented.
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const m = inputs.models;
|
|
84
|
+
const models: ModelCatalog = {
|
|
85
|
+
resolveModelInfo: (q) => m.resolveModel(q),
|
|
86
|
+
// Catalog-driven backend with no canonical default — fall through
|
|
87
|
+
// to `config.backendDefaults.<id>`.
|
|
88
|
+
getDefaultModelId: () => undefined,
|
|
89
|
+
getRawModelInfo: (modelId) => m.getModelInfo(modelId),
|
|
90
|
+
getSettingsPresentation: (activeModel, options) =>
|
|
91
|
+
m.getSettingsPresentation(activeModel, options),
|
|
92
|
+
getProviders: () => m.getProviders(),
|
|
93
|
+
getProviderModels: (p, pg, ps) => m.getProviderModels(p, pg, ps),
|
|
94
|
+
formatModelError: (q, r) => m.formatModelError(q, r),
|
|
95
|
+
listModels: (f) => m.listModels(f),
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
const usage: UsageTelemetry = {
|
|
99
|
+
getSessionSnapshot: async (sessionId) => {
|
|
100
|
+
const snap = await inputs.getSessionSnapshot(sessionId);
|
|
101
|
+
if (!snap) return undefined;
|
|
102
|
+
return {
|
|
103
|
+
inputTokens: snap.usage?.totalInputTokens,
|
|
104
|
+
outputTokens: snap.usage?.totalOutputTokens,
|
|
105
|
+
cacheRead: snap.usage?.totalCacheRead,
|
|
106
|
+
cacheWrite: snap.usage?.totalCacheWrite,
|
|
107
|
+
contextModelId: snap.assistant?.modelID,
|
|
108
|
+
};
|
|
109
|
+
},
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
const tools: ToolRuntime = {
|
|
113
|
+
refreshTools: (chatId) => inputs.refreshPluginMcpServers(chatId),
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
// Session state lives on the server, and `/reset` already clears
|
|
117
|
+
// Talon's stored id centrally (storage/sessions.ts), so the next turn
|
|
118
|
+
// creates a fresh one — no `resetChat` needed. `warmSession`
|
|
119
|
+
// front-loads that creation plus the plugin-MCP sweep, matching what
|
|
120
|
+
// the Claude backend does after a reset.
|
|
121
|
+
const sessions: SessionBackend = {
|
|
122
|
+
warmSession: (chatId) => inputs.warmSession(chatId),
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
const control: SystemControl = {
|
|
126
|
+
updateSystemPrompt: (prompt) => inputs.updateSystemPrompt(prompt),
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
const backend = composeBackend({
|
|
130
|
+
id,
|
|
131
|
+
label,
|
|
132
|
+
cacheMetrics: "readwrite",
|
|
133
|
+
chat,
|
|
134
|
+
background,
|
|
135
|
+
models,
|
|
136
|
+
sessions,
|
|
137
|
+
tools,
|
|
138
|
+
usage,
|
|
139
|
+
control,
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
return {
|
|
143
|
+
backend,
|
|
144
|
+
cleanup: () => {
|
|
145
|
+
inputs.stop();
|
|
146
|
+
},
|
|
147
|
+
};
|
|
148
|
+
},
|
|
149
|
+
};
|
|
150
|
+
}
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* for MCP registration, session lifecycle, tool listing, and provider
|
|
7
7
|
* resolution).
|
|
8
8
|
*
|
|
9
|
-
* Architecture in
|
|
9
|
+
* Architecture in four layers:
|
|
10
10
|
*
|
|
11
11
|
* - {@link RemoteServerState} (state.ts) — per-backend mutable container.
|
|
12
12
|
* Each concrete backend owns one instance, holding the cached client,
|
|
@@ -16,65 +16,37 @@
|
|
|
16
16
|
* - Lifecycle / MCP / sessions / providers — pure helpers that take a
|
|
17
17
|
* `RemoteAgentClient` + `RemoteServerState` and act on them.
|
|
18
18
|
*
|
|
19
|
-
* -
|
|
20
|
-
* the
|
|
21
|
-
*
|
|
19
|
+
* - Bindings (server-bindings.ts, chat-turn.ts, turn.ts, factory.ts) —
|
|
20
|
+
* the helpers closed over one backend's state, the SSE-driven turn,
|
|
21
|
+
* the chat-turn orchestration, and the registry factory composition.
|
|
22
|
+
* This is where the code that used to be copied per backend lives.
|
|
23
|
+
*
|
|
24
|
+
* - Concrete backends (`backend/opencode`, `backend/kilo`) — a
|
|
25
|
+
* `RemoteBackendProfile` (SDK constructors, port, delivery contract,
|
|
26
|
+
* model-selection parser) plus re-exports under historical names.
|
|
22
27
|
*
|
|
23
28
|
* What's NOT here (intentionally):
|
|
24
29
|
*
|
|
25
|
-
* - SDK-specific event types (Kilo's SSE events, OpenCode's sync
|
|
26
|
-
* `prompt` parts list) — those stay in each backend.
|
|
27
|
-
* - Per-backend model catalog logic (`models.ts`) — Kilo and OpenCode
|
|
28
|
-
* have different bucket priorities, fuzzy-match weights, etc.
|
|
29
30
|
* - Tool definitions, frontend prompt format — those are backend-
|
|
30
31
|
* agnostic and live in `core/` and `backend/shared/`.
|
|
32
|
+
*
|
|
33
|
+
* This barrel exposes the helper layer for tests and the conformance
|
|
34
|
+
* suite; the bindings modules import from the concrete files directly.
|
|
31
35
|
*/
|
|
32
36
|
|
|
33
|
-
export type {
|
|
34
|
-
RemoteAgentClient,
|
|
35
|
-
RemoteMcpServerConfig,
|
|
36
|
-
RemotePermissionRule,
|
|
37
|
-
} from "./client.js";
|
|
37
|
+
export type { RemoteAgentClient } from "./client.js";
|
|
38
38
|
|
|
39
|
-
export {
|
|
40
|
-
type RemoteServerState,
|
|
41
|
-
type RemoteServerStateInputs,
|
|
42
|
-
createRemoteServerState,
|
|
43
|
-
errMsg,
|
|
44
|
-
} from "./state.js";
|
|
39
|
+
export { type RemoteServerState, createRemoteServerState } from "./state.js";
|
|
45
40
|
|
|
46
|
-
export {
|
|
47
|
-
type EnsureRemoteServerInputs,
|
|
48
|
-
ensureRemoteServer,
|
|
49
|
-
stopRemoteServer,
|
|
50
|
-
} from "./lifecycle.js";
|
|
41
|
+
export { stopRemoteServer } from "./lifecycle.js";
|
|
51
42
|
|
|
52
43
|
export {
|
|
53
44
|
TALON_MCP_SERVER_NAME,
|
|
54
|
-
TALON_PLUGIN_MCP_SERVER_NAME,
|
|
55
45
|
getChatMcpServerName,
|
|
56
|
-
getPluginMcpServerName,
|
|
57
|
-
safeMcpNamePart,
|
|
58
46
|
isTalonToolID,
|
|
59
47
|
ensureChatMcpServer,
|
|
60
48
|
ensurePluginMcpServers,
|
|
61
49
|
buildToolOverrides,
|
|
62
50
|
disconnectChatMcpServer,
|
|
63
|
-
refreshPluginMcpServers,
|
|
64
51
|
getRegisteredMcpServerNames,
|
|
65
52
|
} from "./mcp.js";
|
|
66
|
-
|
|
67
|
-
export {
|
|
68
|
-
buildPermissionRuleset,
|
|
69
|
-
ensureRemoteSession,
|
|
70
|
-
warmRemoteSession,
|
|
71
|
-
type RemoteWarmDeps,
|
|
72
|
-
} from "./sessions.js";
|
|
73
|
-
|
|
74
|
-
export { type ProviderResolverHooks, resolveProviderID } from "./providers.js";
|
|
75
|
-
|
|
76
|
-
export {
|
|
77
|
-
RemoteTurnTimeoutError,
|
|
78
|
-
remoteTurnTimeoutMs,
|
|
79
|
-
awaitRemoteTurn,
|
|
80
|
-
} from "./turn-timeout.js";
|
|
@@ -92,10 +92,7 @@ export function safeMcpNamePart(value: string, fallback: string): string {
|
|
|
92
92
|
}
|
|
93
93
|
|
|
94
94
|
/** Per-chat registration name for a plugin-provided MCP server. */
|
|
95
|
-
|
|
96
|
-
pluginName: string,
|
|
97
|
-
chatId: string,
|
|
98
|
-
): string {
|
|
95
|
+
function getPluginMcpServerName(pluginName: string, chatId: string): string {
|
|
99
96
|
return `${TALON_PLUGIN_MCP_SERVER_NAME}-${safeMcpNamePart(chatId, "chat")}-${safeMcpNamePart(pluginName, "plugin")}`;
|
|
100
97
|
}
|
|
101
98
|
|
|
@@ -164,7 +164,7 @@ function extractReasoningLevels(rawModel: RemoteRawModel) {
|
|
|
164
164
|
]);
|
|
165
165
|
}
|
|
166
166
|
|
|
167
|
-
|
|
167
|
+
function buildModelCatalog(
|
|
168
168
|
providersData: RemoteRawProvidersData,
|
|
169
169
|
authMap: Record<string, Array<RemoteAuthMethod>>,
|
|
170
170
|
): RemoteModelCatalog {
|
|
@@ -25,20 +25,13 @@ import type {
|
|
|
25
25
|
|
|
26
26
|
export type {
|
|
27
27
|
ModelButton,
|
|
28
|
-
RemoteAuthMethod,
|
|
29
28
|
RemoteModelCatalog,
|
|
30
29
|
RemoteModelCatalogEntry,
|
|
31
30
|
RemoteModelResolution,
|
|
32
|
-
RemoteProviderCatalogEntry,
|
|
33
|
-
RemoteProviderClient,
|
|
34
|
-
RemoteRawModel,
|
|
35
|
-
RemoteRawProvider,
|
|
36
|
-
RemoteRawProvidersData,
|
|
37
31
|
} from "./types.js";
|
|
38
|
-
export {
|
|
32
|
+
export { sortCatalogModels } from "./catalog.js";
|
|
39
33
|
export {
|
|
40
34
|
getBucketPriority,
|
|
41
|
-
getRemoteModelInfo,
|
|
42
35
|
getRemoteModelSelectionValue,
|
|
43
36
|
guessProviderID,
|
|
44
37
|
normalizeModelLookup,
|
|
@@ -46,14 +39,7 @@ export {
|
|
|
46
39
|
resolveRemoteModelInput,
|
|
47
40
|
} from "./resolve.js";
|
|
48
41
|
export { formatRemoteUnavailableModel } from "./presentation.js";
|
|
49
|
-
export type { RemoteModelPresentation } from "./presentation.js";
|
|
50
42
|
export { createRemoteModelProvider } from "./provider.js";
|
|
51
|
-
export type {
|
|
52
|
-
RemoteModelProvider,
|
|
53
|
-
RemoteModelProviderDeps,
|
|
54
|
-
} from "./provider.js";
|
|
55
|
-
export type { RemoteModelCatalogStore } from "./catalog.js";
|
|
56
|
-
|
|
57
43
|
export interface RemoteModelCatalogModuleOptions {
|
|
58
44
|
/** Human label — "OpenCode" / "Kilo" — used in headers and errors. */
|
|
59
45
|
label: string;
|