talon-agent 1.35.0 → 1.37.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 +3 -3
- package/src/backend/claude-sdk/handler.ts +6 -2
- package/src/backend/claude-sdk/options.ts +41 -1
- package/src/backend/codex/handler/events.ts +1 -1
- package/src/backend/codex/handler/message.ts +1 -0
- package/src/backend/kilo/handler/message.ts +1 -0
- package/src/backend/kilo/handler/turn.ts +1 -0
- package/src/backend/openai-agents/handler/events.ts +1 -1
- package/src/backend/openai-agents/handler/message.ts +5 -1
- package/src/backend/opencode/handler/message.ts +1 -0
- package/src/backend/opencode/handler/turn.ts +1 -0
- package/src/backend/remote-server/events.ts +3 -1
- package/src/backend/shared/metrics.ts +26 -51
- package/src/bootstrap.ts +1 -0
- package/src/core/engine/gateway-actions/index.ts +2 -0
- package/src/core/engine/gateway-actions/mesh.ts +27 -0
- package/src/core/engine/gateway-actions/native.ts +607 -0
- package/src/core/mcp-hub/index.ts +3 -0
- package/src/core/mcp-hub/talon-server.ts +3 -0
- package/src/core/mesh/persist.ts +49 -0
- package/src/core/mesh/registry.ts +10 -18
- package/src/core/mesh/service.ts +754 -42
- package/src/core/mesh/teleport.ts +158 -0
- package/src/core/mesh/transfers.ts +186 -0
- package/src/core/tools/bridge.ts +85 -4
- package/src/core/tools/index.ts +23 -2
- package/src/core/tools/mesh.ts +83 -1
- package/src/core/tools/native.ts +138 -0
- package/src/core/tools/types.ts +2 -1
- package/src/frontend/discord/commands/admin.ts +9 -2
- package/src/frontend/discord/helpers.ts +7 -6
- package/src/frontend/native/chats.ts +2 -2
- package/src/frontend/native/index.ts +2 -0
- package/src/frontend/native/server.ts +40 -1
- package/src/frontend/telegram/commands/admin.ts +10 -2
- package/src/frontend/telegram/helpers/diagnostics.ts +7 -6
- package/src/storage/db.ts +6 -1
- package/src/storage/repositories/sessions-repo.ts +20 -1
- package/src/storage/sessions.ts +348 -4
- package/src/storage/sql/db.sql +5 -0
- package/src/storage/sql/schema.sql +2 -1
- package/src/storage/sql/sessions.sql +3 -3
- package/src/storage/sql/statements.generated.ts +8 -4
- package/src/util/config.ts +10 -0
- package/src/util/exec-output.ts +64 -0
- package/src/util/metrics.ts +148 -60
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "talon-agent",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.37.0",
|
|
4
4
|
"description": "Multi-frontend AI agent with full tool access, streaming, cron jobs, and plugin system",
|
|
5
5
|
"author": "Dylan Neve",
|
|
6
6
|
"license": "MIT",
|
|
@@ -101,7 +101,7 @@
|
|
|
101
101
|
"@kilocode/sdk": "^7.2.22",
|
|
102
102
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
103
103
|
"@openai/agents": "^0.13.0",
|
|
104
|
-
"@openai/codex-sdk": "^0.
|
|
104
|
+
"@openai/codex-sdk": "^0.144.0",
|
|
105
105
|
"@opencode-ai/sdk": "^1.17.4",
|
|
106
106
|
"@playwright/mcp": "^0.0.77",
|
|
107
107
|
"@types/cross-spawn": "^6.0.6",
|
|
@@ -133,7 +133,7 @@
|
|
|
133
133
|
"knip": "^6.3.1",
|
|
134
134
|
"oxlint": "^1.59.0",
|
|
135
135
|
"prettier": "^3.8.1",
|
|
136
|
-
"typescript": "^
|
|
136
|
+
"typescript": "^7.0.2",
|
|
137
137
|
"vitest": "^4.1.3"
|
|
138
138
|
},
|
|
139
139
|
"overrides": {
|
|
@@ -336,7 +336,7 @@ export async function* runChatTurn(
|
|
|
336
336
|
}
|
|
337
337
|
|
|
338
338
|
for (const tool of result.tools) {
|
|
339
|
-
recordToolCall(tool.name, "claude");
|
|
339
|
+
recordToolCall(chatId, tool.name, "claude");
|
|
340
340
|
captureIntoState(tool.name, tool.input);
|
|
341
341
|
if (isTurnTerminator(tool.name, tool.input)) {
|
|
342
342
|
state.turnTerminated = true;
|
|
@@ -487,6 +487,7 @@ export async function* runChatTurn(
|
|
|
487
487
|
|
|
488
488
|
const durationMs = Date.now() - t0;
|
|
489
489
|
recordTurnMetrics({
|
|
490
|
+
chatId,
|
|
490
491
|
backend: "claude",
|
|
491
492
|
durationMs,
|
|
492
493
|
toolCalls: state.toolCalls,
|
|
@@ -543,7 +544,10 @@ export async function* runChatTurn(
|
|
|
543
544
|
: ({ violated: false } as const);
|
|
544
545
|
|
|
545
546
|
if (violation.violated) {
|
|
546
|
-
recordFlowViolation(
|
|
547
|
+
recordFlowViolation(
|
|
548
|
+
chatId,
|
|
549
|
+
violation.shouldRetry ? "retried" : "cap_exhausted",
|
|
550
|
+
);
|
|
547
551
|
log(
|
|
548
552
|
"agent",
|
|
549
553
|
`[${chatId}] flow violation: ${violation.reason}. ${
|
|
@@ -30,6 +30,23 @@ import { log, logError } from "../../util/log.js";
|
|
|
30
30
|
import { getConfig, getBridgePort } from "./state.js";
|
|
31
31
|
import { ALLOWED_TOOLS_CHAT, EFFORT_MAP } from "./constants.js";
|
|
32
32
|
|
|
33
|
+
/**
|
|
34
|
+
* Built-in SDK tools that Talon's native tool set replaces when
|
|
35
|
+
* `config.nativeTools` is on. The shell/filesystem built-ins map 1:1 to
|
|
36
|
+
* bash/read/write/edit/glob/search; `Agent` (sub-agent dispatch) is dropped
|
|
37
|
+
* alongside them per the owner's preference for the native surface.
|
|
38
|
+
*/
|
|
39
|
+
const NATIVE_REPLACED_BUILTINS = new Set<string>([
|
|
40
|
+
"Read",
|
|
41
|
+
"Write",
|
|
42
|
+
"Edit",
|
|
43
|
+
"NotebookEdit",
|
|
44
|
+
"Bash",
|
|
45
|
+
"Glob",
|
|
46
|
+
"Grep",
|
|
47
|
+
"Agent",
|
|
48
|
+
]);
|
|
49
|
+
|
|
33
50
|
// ── Types ────────────────────────────────────────────────────────────────────
|
|
34
51
|
|
|
35
52
|
export type BuildSdkOptionsResult = {
|
|
@@ -61,8 +78,19 @@ export type HubMcpEntry = {
|
|
|
61
78
|
type: "http";
|
|
62
79
|
url: string;
|
|
63
80
|
alwaysLoad?: boolean;
|
|
81
|
+
/** Per-server tool-call timeout in ms (overrides MCP_TOOL_TIMEOUT). */
|
|
82
|
+
timeout?: number;
|
|
64
83
|
};
|
|
65
84
|
|
|
85
|
+
/**
|
|
86
|
+
* Tool-call timeout for the Talon frontend tool servers. Must sit ABOVE the
|
|
87
|
+
* bridge's largest per-action budget (1h for uncapped device file transfers)
|
|
88
|
+
* so the layer that times out is the bridge — whose error names the action,
|
|
89
|
+
* the budget, and warns the operation may still be running — rather than the
|
|
90
|
+
* SDK's generic MCP timeout, which tells the model nothing actionable.
|
|
91
|
+
*/
|
|
92
|
+
const FRONTEND_TOOL_CALL_TIMEOUT_MS = 3_900_000; // 65 min
|
|
93
|
+
|
|
66
94
|
/**
|
|
67
95
|
* Build the MCP servers map for a chat query.
|
|
68
96
|
* Includes frontend-specific tool servers and Brave Search, if configured.
|
|
@@ -102,6 +130,8 @@ export function buildMcpServers(chatId: string): Record<string, HubMcpEntry> {
|
|
|
102
130
|
// lever here. Cost: ~50 frontend tool schemas loaded every turn (~10k
|
|
103
131
|
// tokens, cached, negligible on the 1M-context models Talon runs).
|
|
104
132
|
alwaysLoad: true,
|
|
133
|
+
// Outlast the bridge's 1h transfer budget — see the constant's doc.
|
|
134
|
+
timeout: FRONTEND_TOOL_CALL_TIMEOUT_MS,
|
|
105
135
|
};
|
|
106
136
|
}
|
|
107
137
|
|
|
@@ -388,7 +418,17 @@ export function buildSdkOptions(
|
|
|
388
418
|
// WebFetch, Monitor, PushNotification, RemoteTrigger, Plan/Worktree/Todo
|
|
389
419
|
// helpers, AskUserQuestion, ScheduleWakeup) is unavailable to the model.
|
|
390
420
|
// MCP tools are governed independently via `mcpServers` below.
|
|
391
|
-
|
|
421
|
+
//
|
|
422
|
+
// When `config.nativeTools` is on, the SDK's built-in shell/filesystem
|
|
423
|
+
// tools are dropped in favour of Talon's own native tools (which also
|
|
424
|
+
// teleport onto companion devices), and Agent (sub-agent dispatch) is
|
|
425
|
+
// removed too — the owner prefers the native surface without nested
|
|
426
|
+
// agents. Flip the flag back off to restore the built-ins instantly.
|
|
427
|
+
tools: config.nativeTools
|
|
428
|
+
? ALLOWED_TOOLS_CHAT.filter(
|
|
429
|
+
(t) => !NATIVE_REPLACED_BUILTINS.has(t as string),
|
|
430
|
+
)
|
|
431
|
+
: [...ALLOWED_TOOLS_CHAT],
|
|
392
432
|
...thinkingConfig,
|
|
393
433
|
mcpServers: {
|
|
394
434
|
...buildMcpServers(chatId),
|
|
@@ -182,7 +182,7 @@ function recordCodexToolMetric(
|
|
|
182
182
|
// Shared vocabulary: `tool_calls.<bare>` (prefix-stripped, so codex
|
|
183
183
|
// MCP calls land on the same keys as every other backend) plus the
|
|
184
184
|
// `backend.codex.tool_calls` dimension.
|
|
185
|
-
recordToolCall(toolName, "codex");
|
|
185
|
+
recordToolCall(ctx.chatId, toolName, "codex");
|
|
186
186
|
ctx.codexToolMetrics.count += 1;
|
|
187
187
|
}
|
|
188
188
|
|
|
@@ -103,7 +103,7 @@ function handleToolCalled(item: unknown, ctx: HandleRunItemContext): void {
|
|
|
103
103
|
}
|
|
104
104
|
}
|
|
105
105
|
|
|
106
|
-
recordToolCall(toolName, "openai-agents");
|
|
106
|
+
recordToolCall(ctx.chatId, toolName, "openai-agents");
|
|
107
107
|
recordToolUse(ctx.state, toolName, input);
|
|
108
108
|
|
|
109
109
|
if (ctx.onToolUse) {
|
|
@@ -398,6 +398,7 @@ export async function handleMessage(
|
|
|
398
398
|
const responseText = finalizeResponseText(streamState);
|
|
399
399
|
const durationMs = Date.now() - t0;
|
|
400
400
|
recordTurnMetrics({
|
|
401
|
+
chatId,
|
|
401
402
|
backend: "openai-agents",
|
|
402
403
|
durationMs,
|
|
403
404
|
toolCalls: streamState.toolCalls,
|
|
@@ -443,7 +444,10 @@ export async function handleMessage(
|
|
|
443
444
|
: ({ violated: false } as const);
|
|
444
445
|
|
|
445
446
|
if (violation.violated) {
|
|
446
|
-
recordFlowViolation(
|
|
447
|
+
recordFlowViolation(
|
|
448
|
+
chatId,
|
|
449
|
+
violation.shouldRetry ? "retried" : "cap_exhausted",
|
|
450
|
+
);
|
|
447
451
|
log(
|
|
448
452
|
"agent",
|
|
449
453
|
`[${chatId}] flow violation: trailing prose (${violation.trailing.length} chars) without end_turn/send. ${
|
|
@@ -63,6 +63,8 @@ export type ProcessEventOutcome =
|
|
|
63
63
|
|
|
64
64
|
/** Common context passed to the per-event helper. */
|
|
65
65
|
export interface EventProcessingContext {
|
|
66
|
+
/** Chat id for session-scoped metrics. */
|
|
67
|
+
chatId: string;
|
|
66
68
|
/** Session id we're scoped to — events for other sessions are dropped. */
|
|
67
69
|
sessionId: string;
|
|
68
70
|
/** Stream state accumulator (shared/). */
|
|
@@ -277,7 +279,7 @@ async function processPartUpdate(
|
|
|
277
279
|
}
|
|
278
280
|
// Count every tool the model calls — shared vocabulary across
|
|
279
281
|
// backends (prefix-stripped name + backend dimension).
|
|
280
|
-
recordToolCall(toolName, ctx.backendLabel.toLowerCase());
|
|
282
|
+
recordToolCall(ctx.chatId, toolName, ctx.backendLabel.toLowerCase());
|
|
281
283
|
recordToolUse(ctx.state, toolName, input);
|
|
282
284
|
if (ctx.onToolUse) {
|
|
283
285
|
try {
|
|
@@ -38,10 +38,14 @@
|
|
|
38
38
|
* registry. Never interpolate chat ids, model ids, or user input.
|
|
39
39
|
*/
|
|
40
40
|
|
|
41
|
-
import { incrementCounter, recordHistogram } from "../../util/metrics.js";
|
|
42
41
|
import { stripMcpPrefix } from "../../core/tools/index.js";
|
|
43
|
-
import {
|
|
44
|
-
import {
|
|
42
|
+
import type { TokenUsageSnapshot } from "./usage.js";
|
|
43
|
+
import {
|
|
44
|
+
clearLiveTurn,
|
|
45
|
+
recordSessionMetricEvent,
|
|
46
|
+
recordSessionMetrics,
|
|
47
|
+
recordUsage,
|
|
48
|
+
} from "../../storage/sessions.js";
|
|
45
49
|
|
|
46
50
|
/**
|
|
47
51
|
* Count one tool call. `toolName` may be raw from any backend —
|
|
@@ -49,13 +53,21 @@ import { clearLiveTurn, recordUsage } from "../../storage/sessions.js";
|
|
|
49
53
|
* (`talon-tools-123_send`), or bare — it is normalised so every
|
|
50
54
|
* backend lands on the same `tool_calls.<bare>` key.
|
|
51
55
|
*/
|
|
52
|
-
export function recordToolCall(
|
|
53
|
-
|
|
54
|
-
|
|
56
|
+
export function recordToolCall(
|
|
57
|
+
chatId: string,
|
|
58
|
+
toolName: string,
|
|
59
|
+
backend?: string,
|
|
60
|
+
): void {
|
|
61
|
+
recordSessionMetricEvent(chatId, {
|
|
62
|
+
toolCalls: 1,
|
|
63
|
+
toolName: stripMcpPrefix(toolName),
|
|
64
|
+
backend,
|
|
65
|
+
});
|
|
55
66
|
}
|
|
56
67
|
|
|
57
68
|
/** Per-turn rollup recorded once at the end of every chat turn. */
|
|
58
69
|
export type TurnMetricInputs = {
|
|
70
|
+
chatId: string;
|
|
59
71
|
/** Backend id, e.g. "claude", "codex", "kilo", "openai-agents", "opencode". */
|
|
60
72
|
backend: string;
|
|
61
73
|
durationMs: number;
|
|
@@ -78,45 +90,7 @@ export type TurnMetricInputs = {
|
|
|
78
90
|
* ("queries_total")` pairs (and codex's private `codex.*` family).
|
|
79
91
|
*/
|
|
80
92
|
export function recordTurnMetrics(inputs: TurnMetricInputs): void {
|
|
81
|
-
|
|
82
|
-
recordHistogram(
|
|
83
|
-
`backend.${inputs.backend}.response_latency_ms`,
|
|
84
|
-
inputs.durationMs,
|
|
85
|
-
);
|
|
86
|
-
incrementCounter("queries_total");
|
|
87
|
-
incrementCounter(`backend.${inputs.backend}.queries`);
|
|
88
|
-
|
|
89
|
-
if (inputs.failed) incrementCounter(`backend.${inputs.backend}.turn_failed`);
|
|
90
|
-
|
|
91
|
-
if (inputs.toolCalls !== undefined) {
|
|
92
|
-
recordHistogram("tool_calls_per_turn", inputs.toolCalls);
|
|
93
|
-
if (inputs.toolCalls > 0) incrementCounter("turns_with_tools_total");
|
|
94
|
-
}
|
|
95
|
-
if (inputs.apiCalls !== undefined && inputs.apiCalls > 0) {
|
|
96
|
-
incrementCounter("api_calls_total", inputs.apiCalls);
|
|
97
|
-
recordHistogram("api_calls_per_turn", inputs.apiCalls);
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
if (inputs.usage) {
|
|
101
|
-
const u = inputs.usage;
|
|
102
|
-
const b = `backend.${inputs.backend}`;
|
|
103
|
-
incrementCounter("tokens.input_total", u.inputTokens);
|
|
104
|
-
incrementCounter("tokens.output_total", u.outputTokens);
|
|
105
|
-
incrementCounter("tokens.cache_read_total", u.cacheRead);
|
|
106
|
-
incrementCounter("tokens.cache_write_total", u.cacheWrite);
|
|
107
|
-
incrementCounter(`${b}.tokens.input`, u.inputTokens);
|
|
108
|
-
incrementCounter(`${b}.tokens.output`, u.outputTokens);
|
|
109
|
-
incrementCounter(`${b}.tokens.cache_read`, u.cacheRead);
|
|
110
|
-
incrementCounter(`${b}.tokens.cache_write`, u.cacheWrite);
|
|
111
|
-
// Only meaningful when the turn actually consumed input — an
|
|
112
|
-
// all-zero snapshot (failed turn, usage-less backend) would skew
|
|
113
|
-
// the distribution with hard zeros.
|
|
114
|
-
if (u.inputTokens + u.cacheRead > 0) {
|
|
115
|
-
const pct = cacheHitPercent(u);
|
|
116
|
-
recordHistogram("cache_hit_percent", pct);
|
|
117
|
-
recordHistogram(`${b}.cache_hit_percent`, pct);
|
|
118
|
-
}
|
|
119
|
-
}
|
|
93
|
+
recordSessionMetrics(inputs.chatId, inputs);
|
|
120
94
|
}
|
|
121
95
|
|
|
122
96
|
/**
|
|
@@ -152,6 +126,7 @@ export function recordFailedTurnAccounting(inputs: {
|
|
|
152
126
|
contextWindow?: number;
|
|
153
127
|
}): void {
|
|
154
128
|
recordTurnMetrics({
|
|
129
|
+
chatId: inputs.chatId,
|
|
155
130
|
backend: inputs.backend,
|
|
156
131
|
durationMs: inputs.durationMs,
|
|
157
132
|
toolCalls: inputs.toolCalls,
|
|
@@ -188,12 +163,12 @@ export function recordFailedTurnAccounting(inputs: {
|
|
|
188
163
|
* reminder" unanswerable for that backend.
|
|
189
164
|
*/
|
|
190
165
|
export function recordFlowViolation(
|
|
166
|
+
chatId: string,
|
|
191
167
|
outcome: "retried" | "cap_exhausted",
|
|
192
168
|
): void {
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
outcome === "retried"
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
);
|
|
169
|
+
recordSessionMetricEvent(chatId, {
|
|
170
|
+
trailingTextDropped: 1,
|
|
171
|
+
flowViolationRetries: outcome === "retried" ? 1 : 0,
|
|
172
|
+
flowViolationCapExhausted: outcome === "cap_exhausted" ? 1 : 0,
|
|
173
|
+
});
|
|
199
174
|
}
|
package/src/bootstrap.ts
CHANGED
|
@@ -32,6 +32,7 @@ import { skillHandlers } from "./skills.js";
|
|
|
32
32
|
import { pluginHandlers } from "./plugins.js";
|
|
33
33
|
import { modelHandlers } from "./models.js";
|
|
34
34
|
import { meshHandlers } from "./mesh.js";
|
|
35
|
+
import { nativeHandlers } from "./native.js";
|
|
35
36
|
|
|
36
37
|
// Null-prototype so a request `action` of "toString" / "constructor" / etc.
|
|
37
38
|
// can't resolve an inherited Object.prototype method — `handlers[action]` only
|
|
@@ -47,6 +48,7 @@ const handlers: SharedActionHandlers = Object.assign(Object.create(null), {
|
|
|
47
48
|
...pluginHandlers,
|
|
48
49
|
...modelHandlers,
|
|
49
50
|
...meshHandlers,
|
|
51
|
+
...nativeHandlers,
|
|
50
52
|
});
|
|
51
53
|
|
|
52
54
|
export async function handleSharedAction(
|
|
@@ -19,4 +19,31 @@ export const meshHandlers: SharedActionHandlers = {
|
|
|
19
19
|
getMeshService().deviceHistory(body.device, body.hours),
|
|
20
20
|
ring_device: (body) => getMeshService().ringDevice(body.device, body.message),
|
|
21
21
|
get_device_status: (body) => getMeshService().getDeviceStatus(body.device),
|
|
22
|
+
// Exec + filesystem surface — the substrate teleport routes through.
|
|
23
|
+
device_exec: (body) =>
|
|
24
|
+
getMeshService().execOnDevice(
|
|
25
|
+
body.device,
|
|
26
|
+
body.cmd,
|
|
27
|
+
body.cwd,
|
|
28
|
+
body.timeout_sec,
|
|
29
|
+
),
|
|
30
|
+
device_list_dir: (body) =>
|
|
31
|
+
getMeshService().listDirOnDevice(body.device, body.path),
|
|
32
|
+
device_stat: (body) => getMeshService().statOnDevice(body.device, body.path),
|
|
33
|
+
device_read_file: (body) =>
|
|
34
|
+
getMeshService().readFileFromDevice(body.device, body.path),
|
|
35
|
+
device_write_file: (body) =>
|
|
36
|
+
getMeshService().writeFileToDevice(body.device, body.path, body.content),
|
|
37
|
+
device_pull_file: (body) =>
|
|
38
|
+
getMeshService().pullFileFromDevice(
|
|
39
|
+
body.device,
|
|
40
|
+
body.remote_path,
|
|
41
|
+
body.local_path,
|
|
42
|
+
),
|
|
43
|
+
device_push_file: (body) =>
|
|
44
|
+
getMeshService().pushFileToDevice(
|
|
45
|
+
body.device,
|
|
46
|
+
body.local_path,
|
|
47
|
+
body.remote_path,
|
|
48
|
+
),
|
|
22
49
|
};
|