talon-agent 1.3.0 → 1.5.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/package.json +4 -2
- package/prompts/heartbeat.md +18 -6
- package/src/__tests__/compose-tools.test.ts +216 -0
- package/src/__tests__/fuzz.test.ts +0 -2
- package/src/__tests__/gateway-actions.test.ts +1 -423
- package/src/__tests__/heartbeat.test.ts +21 -0
- package/src/__tests__/reload-plugins.test.ts +199 -0
- package/src/__tests__/sessions.test.ts +155 -121
- package/src/backend/claude-sdk/index.ts +230 -109
- package/src/backend/opencode/index.ts +5 -20
- package/src/bootstrap.ts +8 -44
- package/src/core/gateway-actions.ts +42 -88
- package/src/core/heartbeat.ts +8 -5
- package/src/core/plugin.ts +147 -0
- package/src/core/tools/admin.ts +22 -0
- package/src/core/tools/bridge.ts +40 -0
- package/src/core/tools/chat.ts +52 -0
- package/src/core/tools/history.ts +80 -0
- package/src/core/tools/index.ts +84 -0
- package/src/core/tools/mcp-server.ts +64 -0
- package/src/core/tools/media.ts +23 -0
- package/src/core/tools/members.ts +46 -0
- package/src/core/tools/messaging.ts +300 -0
- package/src/core/tools/scheduling.ts +89 -0
- package/src/core/tools/stickers.ts +143 -0
- package/src/core/tools/types.ts +61 -0
- package/src/core/tools/web.ts +26 -0
- package/src/frontend/teams/index.ts +9 -10
- package/src/frontend/telegram/actions.ts +10 -1
- package/src/frontend/telegram/commands.ts +11 -10
- package/src/plugins/github/index.ts +106 -0
- package/src/plugins/playwright/index.ts +82 -0
- package/src/storage/sessions.ts +34 -50
- package/src/util/config.ts +20 -1
- package/src/util/log.ts +3 -1
- package/src/backend/claude-sdk/tools.ts +0 -651
- package/src/frontend/teams/tools.ts +0 -175
|
@@ -9,7 +9,6 @@ import {
|
|
|
9
9
|
setSessionName,
|
|
10
10
|
} from "../../storage/sessions.js";
|
|
11
11
|
import { getChatSettings, setChatModel } from "../../storage/chat-settings.js";
|
|
12
|
-
import { getRecentHistory } from "../../storage/history.js";
|
|
13
12
|
import { resolve } from "node:path";
|
|
14
13
|
import { classify } from "../../core/errors.js";
|
|
15
14
|
import {
|
|
@@ -19,7 +18,7 @@ import {
|
|
|
19
18
|
import { rebuildSystemPrompt } from "../../util/config.js";
|
|
20
19
|
import { log, logError, logWarn } from "../../util/log.js";
|
|
21
20
|
import { traceMessage } from "../../util/trace.js";
|
|
22
|
-
import {
|
|
21
|
+
import { formatFullDatetime } from "../../util/time.js";
|
|
23
22
|
|
|
24
23
|
import type { QueryParams, QueryResult } from "../../core/types.js";
|
|
25
24
|
|
|
@@ -42,34 +41,15 @@ export function initAgent(
|
|
|
42
41
|
delete process.env.CLAUDECODE;
|
|
43
42
|
}
|
|
44
43
|
|
|
45
|
-
|
|
44
|
+
/** Update the system prompt on the live config. Used by plugin hot-reload
|
|
45
|
+
* so the next message picks up new plugin tool descriptions. */
|
|
46
|
+
export function updateSystemPrompt(prompt: string): void {
|
|
47
|
+
if (config) config.systemPrompt = prompt;
|
|
48
|
+
}
|
|
46
49
|
|
|
47
|
-
|
|
48
|
-
params: QueryParams,
|
|
49
|
-
_retried = false,
|
|
50
|
-
): Promise<QueryResult> {
|
|
51
|
-
if (!config)
|
|
52
|
-
throw new Error("Agent not initialized. Call initAgent() first.");
|
|
50
|
+
// ── Shared options builder ───────────────────────────────────────────────────
|
|
53
51
|
|
|
54
|
-
|
|
55
|
-
chatId,
|
|
56
|
-
text,
|
|
57
|
-
senderName,
|
|
58
|
-
isGroup,
|
|
59
|
-
onTextBlock,
|
|
60
|
-
onStreamDelta,
|
|
61
|
-
onToolUse,
|
|
62
|
-
} = params;
|
|
63
|
-
const session = getSession(chatId);
|
|
64
|
-
const t0 = Date.now();
|
|
65
|
-
|
|
66
|
-
// Rebuild system prompt on first turn of a new/reset session so identity,
|
|
67
|
-
// memory, and workspace listing are fresh
|
|
68
|
-
if (session.turns === 0) {
|
|
69
|
-
rebuildSystemPrompt(config, getPluginPromptAdditions());
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
// Per-chat settings override global config
|
|
52
|
+
function buildSdkOptions(chatId: string) {
|
|
73
53
|
const chatSettings = getChatSettings(chatId);
|
|
74
54
|
const activeModel = chatSettings.model ?? config.model;
|
|
75
55
|
const activeEffort = chatSettings.effort ?? "adaptive";
|
|
@@ -91,13 +71,18 @@ export async function handleMessage(
|
|
|
91
71
|
thinking: { type: "adaptive" as const },
|
|
92
72
|
};
|
|
93
73
|
|
|
74
|
+
const supports1m =
|
|
75
|
+
!activeModel.includes("haiku") && !activeModel.includes("[1m]");
|
|
76
|
+
const sdkModel = supports1m ? `${activeModel}[1m]` : activeModel;
|
|
77
|
+
|
|
78
|
+
const session = getSession(chatId);
|
|
79
|
+
|
|
94
80
|
const options = {
|
|
95
|
-
model:
|
|
81
|
+
model: sdkModel,
|
|
96
82
|
systemPrompt: config.systemPrompt,
|
|
97
83
|
cwd: config.workspace,
|
|
98
84
|
permissionMode: "bypassPermissions" as const,
|
|
99
85
|
allowDangerouslySkipPermissions: true,
|
|
100
|
-
betas: ["context-1m-2025-08-07"],
|
|
101
86
|
...(config.claudeBinary
|
|
102
87
|
? { pathToClaudeCodeExecutable: config.claudeBinary }
|
|
103
88
|
: {}),
|
|
@@ -115,93 +100,172 @@ export async function handleMessage(
|
|
|
115
100
|
"TaskOutput",
|
|
116
101
|
"TaskStop",
|
|
117
102
|
"AskUserQuestion",
|
|
103
|
+
"WebSearch",
|
|
104
|
+
"WebFetch",
|
|
118
105
|
],
|
|
119
106
|
...thinkingConfig,
|
|
120
107
|
mcpServers: {
|
|
121
|
-
// Register frontend-specific MCP tools based on active frontend
|
|
122
108
|
...(() => {
|
|
123
|
-
const
|
|
109
|
+
const allFrontends = Array.isArray(config.frontend)
|
|
124
110
|
? config.frontend
|
|
125
111
|
: [config.frontend];
|
|
112
|
+
const frontends = allFrontends.filter((f) => f !== "terminal");
|
|
126
113
|
const bridgeUrl = `http://127.0.0.1:${bridgePortFn()}`;
|
|
127
|
-
const mcpEnv = { TALON_BRIDGE_URL: bridgeUrl, TALON_CHAT_ID: chatId };
|
|
128
114
|
const servers: Record<
|
|
129
115
|
string,
|
|
130
116
|
{ command: string; args: string[]; env: Record<string, string> }
|
|
131
117
|
> = {};
|
|
132
|
-
// Resolve tsx from Talon's node_modules (cwd may be ~/.talon/workspace/ which has no node_modules)
|
|
133
|
-
// Resolve tsx from the package root (3 levels up from src/backend/claude-sdk/)
|
|
134
118
|
const tsxImport = resolve(
|
|
135
119
|
import.meta.dirname ?? ".",
|
|
136
120
|
"../../../node_modules/tsx/dist/esm/index.mjs",
|
|
137
121
|
);
|
|
122
|
+
const mcpServerPath = resolve(
|
|
123
|
+
import.meta.dirname ?? ".",
|
|
124
|
+
"../../core/tools/mcp-server.ts",
|
|
125
|
+
);
|
|
138
126
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
: [
|
|
146
|
-
"--import",
|
|
147
|
-
tsxImport,
|
|
148
|
-
resolve(import.meta.dirname ?? ".", "tools.ts"),
|
|
149
|
-
],
|
|
150
|
-
env: mcpEnv,
|
|
127
|
+
for (const frontend of frontends) {
|
|
128
|
+
const serverName = `${frontend}-tools`;
|
|
129
|
+
const mcpEnv = {
|
|
130
|
+
TALON_BRIDGE_URL: bridgeUrl,
|
|
131
|
+
TALON_CHAT_ID: chatId,
|
|
132
|
+
TALON_FRONTEND: frontend,
|
|
151
133
|
};
|
|
152
|
-
|
|
153
|
-
if (frontends.includes("teams")) {
|
|
154
|
-
servers["teams-tools"] = {
|
|
134
|
+
servers[serverName] = {
|
|
155
135
|
command: process.platform === "win32" ? "npx" : "node",
|
|
156
136
|
args:
|
|
157
137
|
process.platform === "win32"
|
|
158
|
-
? [
|
|
159
|
-
|
|
160
|
-
resolve(
|
|
161
|
-
import.meta.dirname ?? ".",
|
|
162
|
-
"../../frontend/teams/tools.ts",
|
|
163
|
-
),
|
|
164
|
-
]
|
|
165
|
-
: [
|
|
166
|
-
"--import",
|
|
167
|
-
tsxImport,
|
|
168
|
-
resolve(
|
|
169
|
-
import.meta.dirname ?? ".",
|
|
170
|
-
"../../frontend/teams/tools.ts",
|
|
171
|
-
),
|
|
172
|
-
],
|
|
138
|
+
? ["tsx", mcpServerPath]
|
|
139
|
+
: ["--import", tsxImport, mcpServerPath],
|
|
173
140
|
env: mcpEnv,
|
|
174
141
|
};
|
|
175
142
|
}
|
|
176
143
|
return servers;
|
|
177
144
|
})(),
|
|
145
|
+
...(config.braveApiKey
|
|
146
|
+
? {
|
|
147
|
+
"brave-search": {
|
|
148
|
+
command: resolve(
|
|
149
|
+
import.meta.dirname ?? ".",
|
|
150
|
+
"../../../node_modules/.bin/brave-search-mcp-server",
|
|
151
|
+
),
|
|
152
|
+
args: [],
|
|
153
|
+
env: { BRAVE_API_KEY: config.braveApiKey },
|
|
154
|
+
},
|
|
155
|
+
}
|
|
156
|
+
: {}),
|
|
178
157
|
...getPluginMcpServers(`http://127.0.0.1:${bridgePortFn()}`, chatId),
|
|
179
158
|
},
|
|
180
159
|
...(session.sessionId ? { resume: session.sessionId } : {}),
|
|
181
160
|
};
|
|
182
161
|
|
|
183
|
-
|
|
184
|
-
|
|
162
|
+
return { options, activeModel, session };
|
|
163
|
+
}
|
|
185
164
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
165
|
+
// ── Session warm-up ─────────────────────────────────────────────────────────
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Cold-start a session by spawning an SDK subprocess in streaming input mode,
|
|
169
|
+
* calling getContextUsage() to populate contextWindow and baseline contextTokens,
|
|
170
|
+
* then tearing it down. Fire-and-forget — does not block the caller.
|
|
171
|
+
*/
|
|
172
|
+
export async function warmSession(chatId: string): Promise<void> {
|
|
173
|
+
if (!config) return;
|
|
174
|
+
const abort = new AbortController();
|
|
175
|
+
try {
|
|
176
|
+
rebuildSystemPrompt(config, getPluginPromptAdditions());
|
|
177
|
+
const { options } = buildSdkOptions(chatId);
|
|
178
|
+
|
|
179
|
+
// Streaming input mode: pass an async iterable that never yields a user message
|
|
180
|
+
const neverYield = async function* (): AsyncGenerator<never> {
|
|
181
|
+
await new Promise<never>((_, reject) => {
|
|
182
|
+
abort.signal.addEventListener("abort", () =>
|
|
183
|
+
reject(new Error("aborted")),
|
|
184
|
+
);
|
|
185
|
+
});
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
const q = query({
|
|
189
|
+
prompt: neverYield(),
|
|
190
|
+
options: {
|
|
191
|
+
...options,
|
|
192
|
+
abortController: abort,
|
|
193
|
+
} as Parameters<typeof query>[0]["options"],
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
// Drain the stream in the background so the SDK's internal message loop
|
|
197
|
+
// doesn't stall — control responses are processed in readMessages() which
|
|
198
|
+
// needs the inputStream consumer to not back-pressure.
|
|
199
|
+
const drainPromise = (async () => {
|
|
200
|
+
try {
|
|
201
|
+
for await (const _ of q) {
|
|
202
|
+
// discard SDK messages; we only care about the control response
|
|
203
|
+
}
|
|
204
|
+
} catch {
|
|
205
|
+
// expected: abort causes the stream to end with an error
|
|
206
|
+
}
|
|
207
|
+
})();
|
|
208
|
+
|
|
209
|
+
// Race getContextUsage against a timeout so /reset doesn't hang
|
|
210
|
+
const timeout = new Promise<never>((_, reject) =>
|
|
211
|
+
setTimeout(() => reject(new Error("warm-up timed out")), 15_000),
|
|
212
|
+
);
|
|
213
|
+
const ctx = await Promise.race([q.getContextUsage(), timeout]);
|
|
214
|
+
const session = getSession(chatId);
|
|
215
|
+
if (ctx.maxTokens > 0) session.usage.contextWindow = ctx.maxTokens;
|
|
216
|
+
if (ctx.totalTokens > 0) session.usage.contextTokens = ctx.totalTokens;
|
|
217
|
+
log(
|
|
218
|
+
"agent",
|
|
219
|
+
`[${chatId}] warm-up: context ${ctx.totalTokens}/${ctx.maxTokens} (${ctx.percentage.toFixed(1)}%) model=${ctx.model}`,
|
|
220
|
+
);
|
|
221
|
+
|
|
222
|
+
abort.abort();
|
|
223
|
+
await drainPromise;
|
|
224
|
+
} catch (err) {
|
|
225
|
+
abort.abort();
|
|
226
|
+
// Non-fatal — /status will just show 0 until first real message
|
|
227
|
+
logWarn(
|
|
228
|
+
"agent",
|
|
229
|
+
`[${chatId}] warm-up failed: ${err instanceof Error ? err.message : err}`,
|
|
230
|
+
);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// ── Main handler ─────────────────────────────────────────────────────────────
|
|
235
|
+
|
|
236
|
+
export async function handleMessage(
|
|
237
|
+
params: QueryParams,
|
|
238
|
+
_retried = false,
|
|
239
|
+
): Promise<QueryResult> {
|
|
240
|
+
if (!config)
|
|
241
|
+
throw new Error("Agent not initialized. Call initAgent() first.");
|
|
242
|
+
|
|
243
|
+
const {
|
|
244
|
+
chatId,
|
|
245
|
+
text,
|
|
246
|
+
senderName,
|
|
247
|
+
isGroup,
|
|
248
|
+
onTextBlock,
|
|
249
|
+
onStreamDelta,
|
|
250
|
+
onToolUse,
|
|
251
|
+
} = params;
|
|
252
|
+
const session = getSession(chatId);
|
|
253
|
+
const t0 = Date.now();
|
|
254
|
+
|
|
255
|
+
// Rebuild system prompt on first turn of a new/reset session so identity,
|
|
256
|
+
// memory, and workspace listing are fresh
|
|
257
|
+
if (session.turns === 0) {
|
|
258
|
+
rebuildSystemPrompt(config, getPluginPromptAdditions());
|
|
200
259
|
}
|
|
201
260
|
|
|
261
|
+
const { options, activeModel } = buildSdkOptions(chatId);
|
|
262
|
+
|
|
263
|
+
const msgIdHint = params.messageId ? ` [msg_id:${params.messageId}]` : "";
|
|
264
|
+
const nowTag = `[${formatFullDatetime()}]`;
|
|
265
|
+
|
|
202
266
|
const prompt = isGroup
|
|
203
|
-
? `${
|
|
204
|
-
: `${
|
|
267
|
+
? `${nowTag} [${senderName}]${msgIdHint}: ${text}`
|
|
268
|
+
: `${nowTag}${msgIdHint} ${text}`;
|
|
205
269
|
log("agent", `[${chatId}] <- (${text.length} chars)`);
|
|
206
270
|
traceMessage(chatId, "in", text, { senderName, isGroup });
|
|
207
271
|
|
|
@@ -214,11 +278,16 @@ export async function handleMessage(
|
|
|
214
278
|
let currentBlockText = "";
|
|
215
279
|
let allResponseText = "";
|
|
216
280
|
let newSessionId: string | undefined;
|
|
217
|
-
let inputTokens = 0;
|
|
218
|
-
let outputTokens = 0;
|
|
219
|
-
let cacheRead = 0;
|
|
220
|
-
let cacheWrite = 0;
|
|
221
281
|
let toolCalls = 0;
|
|
282
|
+
// Populated from SDK result message
|
|
283
|
+
let contextTokens = 0; // actual context fill from last iteration
|
|
284
|
+
let contextWindow: number | undefined;
|
|
285
|
+
let numApiCalls = 0;
|
|
286
|
+
// Cumulative token counts from SDK modelUsage (aggregated across models)
|
|
287
|
+
let sdkInputTokens = 0;
|
|
288
|
+
let sdkOutputTokens = 0;
|
|
289
|
+
let sdkCacheRead = 0;
|
|
290
|
+
let sdkCacheWrite = 0;
|
|
222
291
|
|
|
223
292
|
// Streaming throttle
|
|
224
293
|
let lastStreamUpdate = 0;
|
|
@@ -313,15 +382,64 @@ export async function handleMessage(
|
|
|
313
382
|
}
|
|
314
383
|
}
|
|
315
384
|
|
|
316
|
-
// Final result
|
|
385
|
+
// Final result — read all data from SDK result fields
|
|
317
386
|
if (type === "result") {
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
387
|
+
numApiCalls =
|
|
388
|
+
((msg as Record<string, unknown>).num_turns as number) ?? 0;
|
|
389
|
+
|
|
390
|
+
// Context fill from last API iteration (only available in raw usage)
|
|
391
|
+
const usage = msg.usage as
|
|
392
|
+
| {
|
|
393
|
+
iterations?: Array<{
|
|
394
|
+
input_tokens: number;
|
|
395
|
+
cache_read_input_tokens: number;
|
|
396
|
+
cache_creation_input_tokens: number;
|
|
397
|
+
}>;
|
|
398
|
+
}
|
|
399
|
+
| undefined;
|
|
400
|
+
if (
|
|
401
|
+
usage &&
|
|
402
|
+
Array.isArray(usage.iterations) &&
|
|
403
|
+
usage.iterations.length > 0
|
|
404
|
+
) {
|
|
405
|
+
const last = usage.iterations[usage.iterations.length - 1];
|
|
406
|
+
contextTokens =
|
|
407
|
+
(last.input_tokens ?? 0) +
|
|
408
|
+
(last.cache_read_input_tokens ?? 0) +
|
|
409
|
+
(last.cache_creation_input_tokens ?? 0);
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// Token counts, context window from SDK modelUsage (aggregated per model)
|
|
413
|
+
type MU = {
|
|
414
|
+
inputTokens?: number;
|
|
415
|
+
outputTokens?: number;
|
|
416
|
+
cacheReadInputTokens?: number;
|
|
417
|
+
cacheCreationInputTokens?: number;
|
|
418
|
+
contextWindow?: number;
|
|
419
|
+
};
|
|
420
|
+
const modelUsage = (msg as Record<string, unknown>).modelUsage as
|
|
421
|
+
| Record<string, MU>
|
|
422
|
+
| undefined;
|
|
423
|
+
if (modelUsage) {
|
|
424
|
+
for (const mu of Object.values(modelUsage)) {
|
|
425
|
+
sdkInputTokens += mu.inputTokens ?? 0;
|
|
426
|
+
sdkOutputTokens += mu.outputTokens ?? 0;
|
|
427
|
+
sdkCacheRead += mu.cacheReadInputTokens ?? 0;
|
|
428
|
+
sdkCacheWrite += mu.cacheCreationInputTokens ?? 0;
|
|
429
|
+
if (
|
|
430
|
+
mu.contextWindow &&
|
|
431
|
+
mu.contextWindow > 0 &&
|
|
432
|
+
contextWindow === undefined
|
|
433
|
+
) {
|
|
434
|
+
contextWindow = mu.contextWindow;
|
|
435
|
+
}
|
|
436
|
+
}
|
|
324
437
|
}
|
|
438
|
+
log(
|
|
439
|
+
"agent",
|
|
440
|
+
`SDK result: modelUsage=${JSON.stringify(modelUsage)}, contextWindow=${contextWindow}, contextTokens=${contextTokens}, numApiCalls=${numApiCalls}`,
|
|
441
|
+
);
|
|
442
|
+
|
|
325
443
|
// If we still have unsent text and no streaming captured it
|
|
326
444
|
if (
|
|
327
445
|
!allResponseText &&
|
|
@@ -383,12 +501,15 @@ export async function handleMessage(
|
|
|
383
501
|
if (newSessionId) setSessionId(chatId, newSessionId);
|
|
384
502
|
incrementTurns(chatId);
|
|
385
503
|
recordUsage(chatId, {
|
|
386
|
-
inputTokens,
|
|
387
|
-
outputTokens,
|
|
388
|
-
cacheRead,
|
|
389
|
-
cacheWrite,
|
|
504
|
+
inputTokens: sdkInputTokens,
|
|
505
|
+
outputTokens: sdkOutputTokens,
|
|
506
|
+
cacheRead: sdkCacheRead,
|
|
507
|
+
cacheWrite: sdkCacheWrite,
|
|
390
508
|
durationMs,
|
|
391
509
|
model: activeModel,
|
|
510
|
+
contextTokens,
|
|
511
|
+
contextWindow,
|
|
512
|
+
numApiCalls,
|
|
392
513
|
});
|
|
393
514
|
|
|
394
515
|
// Set a descriptive session name from the first message
|
|
@@ -408,21 +529,21 @@ export async function handleMessage(
|
|
|
408
529
|
// The remaining currentBlockText is the final response text
|
|
409
530
|
allResponseText += currentBlockText;
|
|
410
531
|
|
|
411
|
-
const totalPrompt =
|
|
532
|
+
const totalPrompt = sdkInputTokens + sdkCacheRead + sdkCacheWrite;
|
|
412
533
|
const cacheHitPct =
|
|
413
|
-
totalPrompt > 0 ? Math.round((
|
|
534
|
+
totalPrompt > 0 ? Math.round((sdkCacheRead / totalPrompt) * 100) : 0;
|
|
414
535
|
|
|
415
536
|
log(
|
|
416
537
|
"agent",
|
|
417
|
-
`[${chatId}] -> (${durationMs}ms, in=${
|
|
538
|
+
`[${chatId}] -> (${durationMs}ms, in=${sdkInputTokens} out=${sdkOutputTokens} cache=${cacheHitPct}%` +
|
|
418
539
|
`${toolCalls > 0 ? ` tools=${toolCalls}` : ""})`,
|
|
419
540
|
);
|
|
420
541
|
traceMessage(chatId, "out", allResponseText, {
|
|
421
542
|
durationMs,
|
|
422
|
-
inputTokens,
|
|
423
|
-
outputTokens,
|
|
424
|
-
cacheRead,
|
|
425
|
-
cacheWrite,
|
|
543
|
+
inputTokens: sdkInputTokens,
|
|
544
|
+
outputTokens: sdkOutputTokens,
|
|
545
|
+
cacheRead: sdkCacheRead,
|
|
546
|
+
cacheWrite: sdkCacheWrite,
|
|
426
547
|
toolCalls,
|
|
427
548
|
model: activeModel,
|
|
428
549
|
});
|
|
@@ -430,9 +551,9 @@ export async function handleMessage(
|
|
|
430
551
|
return {
|
|
431
552
|
text: allResponseText.trim(),
|
|
432
553
|
durationMs,
|
|
433
|
-
inputTokens,
|
|
434
|
-
outputTokens,
|
|
435
|
-
cacheRead,
|
|
436
|
-
cacheWrite,
|
|
554
|
+
inputTokens: sdkInputTokens,
|
|
555
|
+
outputTokens: sdkOutputTokens,
|
|
556
|
+
cacheRead: sdkCacheRead,
|
|
557
|
+
cacheWrite: sdkCacheWrite,
|
|
437
558
|
};
|
|
438
559
|
}
|
|
@@ -17,7 +17,6 @@ import {
|
|
|
17
17
|
resetSession,
|
|
18
18
|
} from "../../storage/sessions.js";
|
|
19
19
|
import { getChatSettings } from "../../storage/chat-settings.js";
|
|
20
|
-
import { getRecentHistory } from "../../storage/history.js";
|
|
21
20
|
import { classify } from "../../core/errors.js";
|
|
22
21
|
import { log, logError, logWarn } from "../../util/log.js";
|
|
23
22
|
import { traceMessage } from "../../util/trace.js";
|
|
@@ -53,7 +52,7 @@ async function ensureServer(): Promise<OpencodeClient> {
|
|
|
53
52
|
|
|
54
53
|
// Register our MCP tools server with OpenCode
|
|
55
54
|
try {
|
|
56
|
-
const toolsPath = new URL("
|
|
55
|
+
const toolsPath = new URL("../../core/tools/mcp-server.ts", import.meta.url)
|
|
57
56
|
.pathname;
|
|
58
57
|
await client.mcp.add({
|
|
59
58
|
body: {
|
|
@@ -63,6 +62,7 @@ async function ensureServer(): Promise<OpencodeClient> {
|
|
|
63
62
|
command: ["node", "--import", "tsx", toolsPath],
|
|
64
63
|
environment: {
|
|
65
64
|
TALON_BRIDGE_URL: `http://127.0.0.1:${gatewayPortFn()}`,
|
|
65
|
+
TALON_FRONTEND: "telegram",
|
|
66
66
|
},
|
|
67
67
|
},
|
|
68
68
|
},
|
|
@@ -149,24 +149,9 @@ export async function handleMessage(
|
|
|
149
149
|
|
|
150
150
|
// Build prompt with group context
|
|
151
151
|
const msgIdHint = params.messageId ? ` [msg_id:${params.messageId}]` : "";
|
|
152
|
-
let continuityPrefix = "";
|
|
153
|
-
const session = getSession(chatId);
|
|
154
|
-
if (!session.sessionId && session.turns > 0) {
|
|
155
|
-
const recent = getRecentHistory(chatId, 3);
|
|
156
|
-
if (recent.length > 0) {
|
|
157
|
-
const ctx = recent
|
|
158
|
-
.map(
|
|
159
|
-
(m) =>
|
|
160
|
-
`[${new Date(m.timestamp).toISOString().slice(11, 16)}] ${m.senderName}: ${m.text.slice(0, 300)}`,
|
|
161
|
-
)
|
|
162
|
-
.join("\n");
|
|
163
|
-
continuityPrefix = `[Session resumed — recent context:\n${ctx}]\n\n`;
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
|
|
167
152
|
const prompt = isGroup
|
|
168
|
-
?
|
|
169
|
-
: `${
|
|
153
|
+
? `[${senderName}]${msgIdHint}: ${text}`
|
|
154
|
+
: `${text}${msgIdHint}`;
|
|
170
155
|
|
|
171
156
|
log("agent", `[${chatId}] <- (${text.length} chars)`);
|
|
172
157
|
traceMessage(chatId, "in", text, { senderName, isGroup });
|
|
@@ -211,7 +196,7 @@ export async function handleMessage(
|
|
|
211
196
|
model: activeModel,
|
|
212
197
|
});
|
|
213
198
|
|
|
214
|
-
if (
|
|
199
|
+
if (getSession(chatId).turns === 0 && text) {
|
|
215
200
|
const cleanText = text
|
|
216
201
|
.replace(/^\[.*?\]\s*/g, "")
|
|
217
202
|
.replace(/\[msg_id:\d+\]\s*/g, "")
|
package/src/bootstrap.ts
CHANGED
|
@@ -58,15 +58,14 @@ export async function bootstrap(
|
|
|
58
58
|
): Promise<BootstrapResult> {
|
|
59
59
|
const config = loadConfig();
|
|
60
60
|
|
|
61
|
-
//
|
|
62
|
-
if (config.braveApiKey) process.env.TALON_BRAVE_API_KEY = config.braveApiKey;
|
|
63
|
-
if (config.searxngUrl) process.env.TALON_SEARXNG_URL = config.searxngUrl;
|
|
64
|
-
|
|
65
|
-
// Load plugins (external tool packages + built-in mempalace)
|
|
61
|
+
// Load plugins (external tool packages + built-in GitHub, MemPalace, Playwright)
|
|
66
62
|
const hasPlugins =
|
|
67
|
-
config.plugins.length > 0 ||
|
|
63
|
+
config.plugins.length > 0 ||
|
|
64
|
+
config.github?.enabled === true ||
|
|
65
|
+
config.mempalace?.enabled === true ||
|
|
66
|
+
config.playwright?.enabled === true;
|
|
68
67
|
if (hasPlugins) {
|
|
69
|
-
const { loadPlugins,
|
|
68
|
+
const { loadPlugins, loadBuiltinPlugins, getPluginPromptAdditions } =
|
|
70
69
|
await import("./core/plugin.js");
|
|
71
70
|
|
|
72
71
|
// External plugins
|
|
@@ -77,43 +76,8 @@ export async function bootstrap(
|
|
|
77
76
|
await loadPlugins(config.plugins, frontends);
|
|
78
77
|
}
|
|
79
78
|
|
|
80
|
-
// Built-in
|
|
81
|
-
|
|
82
|
-
const { createMempalacePlugin } =
|
|
83
|
-
await import("./plugins/mempalace/index.js");
|
|
84
|
-
const { getPlugin } = await import("./core/plugin.js");
|
|
85
|
-
const { dirs, files: pathFiles } = await import("./util/paths.js");
|
|
86
|
-
const pythonPath =
|
|
87
|
-
config.mempalace.pythonPath ?? pathFiles.mempalacePython;
|
|
88
|
-
const palacePath = config.mempalace.palacePath ?? dirs.palace;
|
|
89
|
-
const mempalaceConfig = config.mempalace as unknown as Record<
|
|
90
|
-
string,
|
|
91
|
-
unknown
|
|
92
|
-
>;
|
|
93
|
-
const mp = createMempalacePlugin({ pythonPath, palacePath });
|
|
94
|
-
registerPlugin(mp, mempalaceConfig);
|
|
95
|
-
|
|
96
|
-
// Only call init if registration succeeded (validation passed)
|
|
97
|
-
if (getPlugin("mempalace")) {
|
|
98
|
-
try {
|
|
99
|
-
const MEMPALACE_INIT_TIMEOUT_MS = 30_000;
|
|
100
|
-
await Promise.race([
|
|
101
|
-
mp.init?.(mempalaceConfig),
|
|
102
|
-
new Promise((_, reject) =>
|
|
103
|
-
setTimeout(
|
|
104
|
-
() => reject(new Error("MemPalace init timed out after 30s")),
|
|
105
|
-
MEMPALACE_INIT_TIMEOUT_MS,
|
|
106
|
-
),
|
|
107
|
-
),
|
|
108
|
-
]);
|
|
109
|
-
} catch (err) {
|
|
110
|
-
log(
|
|
111
|
-
"mempalace",
|
|
112
|
-
`Init warning: ${err instanceof Error ? err.message : err}`,
|
|
113
|
-
);
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
}
|
|
79
|
+
// Built-in plugins (GitHub, MemPalace, Playwright) — shared with hot-reload
|
|
80
|
+
await loadBuiltinPlugins(config);
|
|
117
81
|
|
|
118
82
|
rebuildSystemPrompt(config, getPluginPromptAdditions());
|
|
119
83
|
}
|