talon-agent 5.29.0 → 5.30.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 +2 -1
- package/prompts/system/agent-brief.md +9 -6
- package/src/backend/claude-sdk/constants.ts +22 -0
- package/src/backend/claude-sdk/models/discovery.ts +3 -0
- package/src/backend/claude-sdk/one-shot.ts +3 -1
- package/src/backend/claude-sdk/options.ts +7 -1
- package/src/core/agents/index.ts +2 -0
- package/src/core/agents/prompt.ts +70 -2
- package/src/core/agents/registry.ts +47 -5
- package/src/core/agents/runner.ts +169 -31
- package/src/core/agents/trail.ts +141 -0
- package/src/core/agents/types.ts +29 -3
- package/src/core/agents/watchdog.ts +70 -0
- package/src/core/background/isolated-agent.ts +6 -2
- package/src/core/backup/plan.ts +4 -0
- package/src/core/config/index.ts +13 -4
- package/src/core/engine/gateway-actions/agents/control.ts +9 -2
- package/src/core/engine/gateway-actions/agents/preflight.ts +17 -1
- package/src/core/engine/gateway-actions/agents/report.ts +81 -24
- package/src/core/engine/gateway-actions/index.ts +3 -0
- package/src/core/mcp-hub/guest-scope.ts +3 -1
- package/src/core/mesh/devices/service.ts +7 -0
- package/src/core/mesh/links/bridge-links.ts +20 -0
- package/src/core/secrets/actions.ts +18 -0
- package/src/core/secrets/drop.ts +176 -0
- package/src/core/secrets/index.ts +11 -0
- package/src/core/secrets/service.ts +248 -0
- package/src/core/secrets/store.ts +100 -0
- package/src/core/tools/index.ts +2 -0
- package/src/core/tools/ops/agents.ts +26 -8
- package/src/core/tools/ops/secrets.ts +36 -0
- package/src/core/tools/types.ts +2 -1
- package/src/frontend/discord/commands/definitions.ts +21 -0
- package/src/frontend/discord/commands/router.ts +3 -0
- package/src/frontend/discord/commands/secret.ts +26 -0
- package/src/frontend/native/bridge/routes/host.ts +10 -0
- package/src/frontend/native/bridge/routes/pre-auth.ts +82 -1
- package/src/frontend/native/bridge/routes/table.ts +5 -0
- package/src/frontend/native/bridge/server.ts +32 -4
- package/src/frontend/native/commands/definitions.ts +6 -0
- package/src/frontend/native/commands/index.ts +12 -0
- package/src/frontend/native/surface/handlers.ts +9 -0
- package/src/frontend/telegram/commands/definitions.ts +4 -0
- package/src/frontend/telegram/commands/index.ts +3 -0
- package/src/frontend/telegram/commands/secret.ts +24 -0
- package/src/frontend/whatsapp/commands.ts +16 -1
- package/src/util/log.ts +1 -0
- package/src/util/paths.ts +6 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "talon-agent",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.30.0",
|
|
4
4
|
"description": "Multi-frontend AI agent with full tool access, streaming, cron jobs, and plugin system",
|
|
5
5
|
"author": "The Falconry",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -91,6 +91,7 @@
|
|
|
91
91
|
"format": "prettier --write src/ prompts/",
|
|
92
92
|
"format:check": "prettier --check src/ prompts/",
|
|
93
93
|
"preflight": "bash scripts/preflight.sh",
|
|
94
|
+
"worktree": "node scripts/worktree.mjs",
|
|
94
95
|
"ci:protect": "node .github/scripts/enforce-ci-gate.mjs",
|
|
95
96
|
"build:gleam": "cd native/scheduler-core && gleam build --target javascript && node embed.mjs",
|
|
96
97
|
"build:zig": "node native/textops-wasm/build.mjs",
|
|
@@ -33,16 +33,18 @@ a useful result; silence is not.
|
|
|
33
33
|
Your parent may have spawned others alongside you. `list_peers()` shows them —
|
|
34
34
|
id, label, and what each was asked to do — and `message_peer(agent_id, text)`
|
|
35
35
|
sends one of them a note directly, without going through your parent.
|
|
36
|
+
`list_peers(scope: "tree")` widens the view to every live agent working for
|
|
37
|
+
the same chat (your parent agent, children, cousins); any of them can be
|
|
38
|
+
addressed the same way, by id or by label.
|
|
36
39
|
|
|
37
40
|
Use it when something you found changes _their_ work and waiting would waste
|
|
38
41
|
it: a fact you both need, a dead end worth not repeating, a correction to
|
|
39
42
|
something you told them earlier. Don't narrate your progress at them — a peer
|
|
40
43
|
pays for every message with context it could have spent on its own job.
|
|
41
44
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
`report_result`.
|
|
45
|
+
They see your note at their next `check_inbox`, so it is not an interrupt.
|
|
46
|
+
Your report still goes to your parent: peer messages are for coordination,
|
|
47
|
+
never a substitute for `report_result`.
|
|
46
48
|
|
|
47
49
|
## Delegating further
|
|
48
50
|
|
|
@@ -56,5 +58,6 @@ parent: peer messages are for coordination, never a substitute for
|
|
|
56
58
|
- You have the full background tool surface (files, shell, web, plugins, and
|
|
57
59
|
the messaging tools with an explicit `chat_id`). Use it, but stay inside
|
|
58
60
|
the brief — you were spawned for one job.
|
|
59
|
-
- Be efficient.
|
|
60
|
-
|
|
61
|
+
- Be efficient. A run may have a wall-clock timeout, and a watchdog pings,
|
|
62
|
+
then ends, one that makes no tool call or output for too long; a partial
|
|
63
|
+
result reported in time beats a perfect one that never arrives.
|
|
@@ -47,3 +47,25 @@ export const EFFORT_MAP: Record<
|
|
|
47
47
|
|
|
48
48
|
/** Minimum interval (ms) between streaming delta callbacks to avoid flooding frontends. */
|
|
49
49
|
export const STREAM_INTERVAL = 1000;
|
|
50
|
+
|
|
51
|
+
// ── Transcript retention ───────────────────────────────────────────────────
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Settings layered onto every Claude Code process Talon spawns, as the
|
|
55
|
+
* SDK's `settings` option (the CLI's `--settings` flag).
|
|
56
|
+
*
|
|
57
|
+
* Claude Code runs a background retention sweep (at most once a day, shared
|
|
58
|
+
* across every CLI process through `~/.claude/.last-cleanup`) that deletes
|
|
59
|
+
* session transcripts in `~/.claude/projects` older than `cleanupPeriodDays`
|
|
60
|
+
* — 30 days when nothing sets it. Talon resumes chats by session id, so a
|
|
61
|
+
* swept transcript silently breaks a long-lived chat and loses its history.
|
|
62
|
+
*
|
|
63
|
+
* The user's own `~/.claude/settings.json` only applies while user settings
|
|
64
|
+
* are loaded, the file parses, and HOME / CLAUDE_CONFIG_DIR match the
|
|
65
|
+
* user's. Setting it per run removes that dependency. The CLI rejects 0 and
|
|
66
|
+
* recommends a large value for long retention; 100000 days is effectively
|
|
67
|
+
* forever. A managed policy-tier `cleanupPeriodDays` still takes precedence.
|
|
68
|
+
*/
|
|
69
|
+
export const CLAUDE_RETENTION_SETTINGS = {
|
|
70
|
+
cleanupPeriodDays: 100_000,
|
|
71
|
+
} as const;
|
|
@@ -14,6 +14,7 @@ import type { ModelInfo } from "../../../core/models/catalog.js";
|
|
|
14
14
|
import { log, logError } from "../../../util/log.js";
|
|
15
15
|
import { describeSdkModel, type SdkModelInfo } from "./parsing.js";
|
|
16
16
|
import { convertSdkModels } from "./convert.js";
|
|
17
|
+
import { CLAUDE_RETENTION_SETTINGS } from "../constants.js";
|
|
17
18
|
|
|
18
19
|
type ProbeOptions = {
|
|
19
20
|
cwd?: string;
|
|
@@ -62,6 +63,8 @@ async function probeSupportedModels(
|
|
|
62
63
|
prompt: neverYield(),
|
|
63
64
|
options: {
|
|
64
65
|
...probeOptions,
|
|
66
|
+
// Any spawned CLI may run the daily retention sweep.
|
|
67
|
+
settings: { ...CLAUDE_RETENTION_SETTINGS },
|
|
65
68
|
model: seedModel,
|
|
66
69
|
abortController: abort,
|
|
67
70
|
} as Parameters<typeof query>[0]["options"],
|
|
@@ -17,7 +17,7 @@ import type { SDKMessage } from "@anthropic-ai/claude-agent-sdk";
|
|
|
17
17
|
import type { OneShotAgentParams, OneShotUsage } from "../../core/types.js";
|
|
18
18
|
import { log, logWarn } from "../../util/log.js";
|
|
19
19
|
import { ALLOWED_TOOLS_BACKGROUND } from "../../core/constants.js";
|
|
20
|
-
import { EFFORT_MAP } from "./constants.js";
|
|
20
|
+
import { CLAUDE_RETENTION_SETTINGS, EFFORT_MAP } from "./constants.js";
|
|
21
21
|
import { buildMcpServers, buildPluginMcpServers } from "./options.js";
|
|
22
22
|
import { isBackgroundToolContext } from "../../core/agents/context.js";
|
|
23
23
|
import { warnIfBelowCacheMinimum } from "../runtime/cache/cache-telemetry.js";
|
|
@@ -97,6 +97,8 @@ export async function runOneShotAgent(
|
|
|
97
97
|
...(oneShotConfig.claudeBinary
|
|
98
98
|
? { pathToClaudeCodeExecutable: oneShotConfig.claudeBinary }
|
|
99
99
|
: {}),
|
|
100
|
+
// Keep session transcripts: interrupted sub-agents resume from them.
|
|
101
|
+
settings: { ...CLAUDE_RETENTION_SETTINGS },
|
|
100
102
|
mcpServers: assembleMcpServers(contextLabel),
|
|
101
103
|
// Whitelist of SDK built-in tools for background contexts (heartbeat,
|
|
102
104
|
// dream). Same as chat minus `Agent` — nested sub-agent dispatch from
|
|
@@ -37,7 +37,11 @@ import {
|
|
|
37
37
|
gatewayToken,
|
|
38
38
|
} from "../../core/engine/gateway-auth.js";
|
|
39
39
|
import { getConfig, getBridgePort } from "./state.js";
|
|
40
|
-
import {
|
|
40
|
+
import {
|
|
41
|
+
ALLOWED_TOOLS_CHAT,
|
|
42
|
+
CLAUDE_RETENTION_SETTINGS,
|
|
43
|
+
EFFORT_MAP,
|
|
44
|
+
} from "./constants.js";
|
|
41
45
|
import {
|
|
42
46
|
isGuestTurn,
|
|
43
47
|
isGuestPluginAllowed,
|
|
@@ -482,6 +486,8 @@ export function buildSdkOptions(
|
|
|
482
486
|
...(config.claudeBinary
|
|
483
487
|
? { pathToClaudeCodeExecutable: config.claudeBinary }
|
|
484
488
|
: {}),
|
|
489
|
+
// Keep session transcripts: Talon resumes chats from them.
|
|
490
|
+
settings: { ...CLAUDE_RETENTION_SETTINGS },
|
|
485
491
|
// Whitelist of SDK built-in tools. Anything not listed (e.g. WebSearch,
|
|
486
492
|
// WebFetch, Monitor, PushNotification, RemoteTrigger, Plan/Worktree/Todo
|
|
487
493
|
// helpers, AskUserQuestion, ScheduleWakeup) is unavailable to the model.
|
package/src/core/agents/index.ts
CHANGED
|
@@ -21,6 +21,7 @@ export { AgentRegistry, agentRegistry } from "./registry.js";
|
|
|
21
21
|
export {
|
|
22
22
|
DEFAULT_AGENT_CAPS,
|
|
23
23
|
clampTimeout,
|
|
24
|
+
describeTimeout,
|
|
24
25
|
getAgentCaps,
|
|
25
26
|
initAgents,
|
|
26
27
|
interruptAgentsForRestart,
|
|
@@ -36,4 +37,5 @@ export {
|
|
|
36
37
|
initAgentDelivery,
|
|
37
38
|
} from "./delivery.js";
|
|
38
39
|
export { describeParent, wantsPreflight } from "./prompt.js";
|
|
40
|
+
export { recordInterimMessage } from "./trail.js";
|
|
39
41
|
export type { AgentCaps, AgentParent, AgentRecord } from "./types.js";
|
|
@@ -11,6 +11,7 @@ import { resolve } from "node:path";
|
|
|
11
11
|
import { dirs } from "../../util/paths.js";
|
|
12
12
|
import { loadSystemTemplate } from "../prompt/templates.js";
|
|
13
13
|
import type { AgentParent, AgentRecord } from "./types.js";
|
|
14
|
+
import { trailIsEmpty } from "./trail.js";
|
|
14
15
|
|
|
15
16
|
/** Where sub-agent run logs live: `~/.talon/workspace/logs/agents/`. */
|
|
16
17
|
const AGENT_LOGS_DIR = resolve(dirs.logs, "agents");
|
|
@@ -60,7 +61,11 @@ const PREFLIGHT_INSTRUCTION =
|
|
|
60
61
|
"the repo (or call the run_preflight tool with cwd set to your checkout). " +
|
|
61
62
|
"Push only when it is green. If it is red, fix it — or, when a failure " +
|
|
62
63
|
"is genuinely out of scope, say in the PR body which step failed and why " +
|
|
63
|
-
"you pushed anyway."
|
|
64
|
+
"you pushed anyway. Make your checkout with `node scripts/worktree.mjs " +
|
|
65
|
+
"add /tmp/fix-<name> <branch>` (run it in an existing talon checkout): " +
|
|
66
|
+
"its node_modules is hardlinked from a shared store, so do NOT run " +
|
|
67
|
+
"`npm ci` there (~1.2 GB each). Clean up with `node scripts/worktree.mjs " +
|
|
68
|
+
"remove <path>`.";
|
|
64
69
|
|
|
65
70
|
/** A brief that opens, updates or talks about a pull request. */
|
|
66
71
|
const PR_BRIEF = /\bPRs?\b|pull[ -]requests?/i;
|
|
@@ -181,6 +186,69 @@ function usageLine(record: AgentRecord): string {
|
|
|
181
186
|
);
|
|
182
187
|
}
|
|
183
188
|
|
|
189
|
+
/** Human duration for prompts: "45 min" / "30s". */
|
|
190
|
+
function minutes(ms: number): string {
|
|
191
|
+
return ms >= 60_000
|
|
192
|
+
? `${Math.round(ms / 60_000)} min`
|
|
193
|
+
: `${Math.round(ms / 1000)}s`;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* The "what it was doing" block appended to a report when the run did not
|
|
198
|
+
* end `done` — so a kill or a timeout never throws the work away.
|
|
199
|
+
*/
|
|
200
|
+
function renderTrail(record: AgentRecord): string {
|
|
201
|
+
const trail = record.trail;
|
|
202
|
+
if (record.state === "done" || trailIsEmpty(trail) || !trail) return "";
|
|
203
|
+
const parts: string[] = [];
|
|
204
|
+
if (trail.messages.length > 0) {
|
|
205
|
+
parts.push(
|
|
206
|
+
`Its last interim messages:\n` +
|
|
207
|
+
trail.messages.map((m) => `- ${m}`).join("\n"),
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
if (trail.notes.length > 0) {
|
|
211
|
+
parts.push(
|
|
212
|
+
`Its last progress notes:\n` +
|
|
213
|
+
trail.notes.map((n) => `- ${n}`).join("\n"),
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
if (trail.files.length > 0) {
|
|
217
|
+
parts.push(
|
|
218
|
+
`Files it wrote or edited:\n` +
|
|
219
|
+
trail.files.map((f) => `- ${f}`).join("\n"),
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
return (
|
|
223
|
+
`\n\n--- What it had done before it ended (run log: ` +
|
|
224
|
+
`${agentLogPath(record.id)}) ---\n\n${parts.join("\n\n")}`
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/** Mailbox note the watchdog leaves an agent that has gone quiet. */
|
|
229
|
+
export function buildStallPing(idleMs: number, stallMs: number): string {
|
|
230
|
+
return (
|
|
231
|
+
`[Watchdog] No tool call or output from you for ${minutes(idleMs)}. ` +
|
|
232
|
+
`If you are stuck, report_result now with what you have. Your parent ` +
|
|
233
|
+
`is told at ${minutes(2 * stallMs)} of silence and the run is killed ` +
|
|
234
|
+
`at ${minutes(3 * stallMs)}.`
|
|
235
|
+
);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/** The interim note a parent gets when its agent has stalled. */
|
|
239
|
+
export function buildStallWarning(
|
|
240
|
+
record: AgentRecord,
|
|
241
|
+
idleMs: number,
|
|
242
|
+
killInMs: number,
|
|
243
|
+
): string {
|
|
244
|
+
return (
|
|
245
|
+
`[Watchdog] Agent ${record.id} "${record.label}" has made no progress ` +
|
|
246
|
+
`(no tool call or output) for ${minutes(idleMs)}. It will be killed in ` +
|
|
247
|
+
`${minutes(killInMs)} unless it resumes. Use send_to_agent to nudge it, ` +
|
|
248
|
+
`or kill_agent to end it now.`
|
|
249
|
+
);
|
|
250
|
+
}
|
|
251
|
+
|
|
184
252
|
/**
|
|
185
253
|
* The wake prompt a parent chat receives when one of its agents settles.
|
|
186
254
|
* Shaped like the trigger wake-up: a `[System: …]` header telling the model
|
|
@@ -203,7 +271,7 @@ export function buildSettlementPrompt(record: AgentRecord): string {
|
|
|
203
271
|
`an agent you spawned earlier. Decide whether to act on it, tell the ` +
|
|
204
272
|
`user, or do nothing.]\n\n` +
|
|
205
273
|
`[Agent "${record.label}" (${record.id}) — ${record.state}]\n\n` +
|
|
206
|
-
`${body}${usageLine(record)}`
|
|
274
|
+
`${body}${renderTrail(record)}${usageLine(record)}`
|
|
207
275
|
);
|
|
208
276
|
}
|
|
209
277
|
|
|
@@ -22,6 +22,7 @@ import type {
|
|
|
22
22
|
AgentRecord,
|
|
23
23
|
AgentResult,
|
|
24
24
|
AgentState,
|
|
25
|
+
AgentTrail,
|
|
25
26
|
} from "./types.js";
|
|
26
27
|
import type { TaskUsage } from "../tasks/types.js";
|
|
27
28
|
import type { AgentSettledEvent, AgentSpawnedEvent } from "../bus/events.js";
|
|
@@ -73,6 +74,8 @@ export interface AgentSettlement {
|
|
|
73
74
|
readonly result?: AgentResult;
|
|
74
75
|
readonly error?: string;
|
|
75
76
|
readonly usage?: TaskUsage;
|
|
77
|
+
/** What the run had been doing — see `trail.ts`. */
|
|
78
|
+
readonly trail?: AgentTrail;
|
|
76
79
|
}
|
|
77
80
|
|
|
78
81
|
export type RegisterOutcome =
|
|
@@ -559,6 +562,7 @@ export class AgentRegistry {
|
|
|
559
562
|
if (patch.result !== undefined) record.result = patch.result;
|
|
560
563
|
if (patch.error !== undefined) record.error = patch.error;
|
|
561
564
|
if (patch.usage !== undefined) record.usage = patch.usage;
|
|
565
|
+
if (patch.trail !== undefined) record.trail = patch.trail;
|
|
562
566
|
|
|
563
567
|
const settled = snapshot(entry);
|
|
564
568
|
this.persist(entry);
|
|
@@ -677,11 +681,9 @@ export class AgentRegistry {
|
|
|
677
681
|
/**
|
|
678
682
|
* An agent's live **peers** — the other agents sharing its parent.
|
|
679
683
|
*
|
|
680
|
-
*
|
|
681
|
-
*
|
|
682
|
-
*
|
|
683
|
-
* widening to the whole chat tree would let an agent reach a cousin from an
|
|
684
|
-
* unrelated piece of work it knows nothing about.
|
|
684
|
+
* The default view of `list_peers`: a swarm is a set of siblings spawned
|
|
685
|
+
* for one job, so siblings are the useful unit to show first. Messaging
|
|
686
|
+
* reaches the whole tree (`treeOf`).
|
|
685
687
|
*
|
|
686
688
|
* Live only: a settled agent has no mailbox to deliver into, and offering
|
|
687
689
|
* it as a peer would only produce a delivery failure one call later.
|
|
@@ -698,6 +700,46 @@ export class AgentRegistry {
|
|
|
698
700
|
return peers.sort((a, b) => a.createdAt - b.createdAt);
|
|
699
701
|
}
|
|
700
702
|
|
|
703
|
+
/**
|
|
704
|
+
* Every other live agent in this agent's tree — the agents whose ancestry
|
|
705
|
+
* roots in the same chat (parent, children, siblings, cousins), oldest
|
|
706
|
+
* first. This is the addressing scope of `message_peer`: an agent working
|
|
707
|
+
* for one chat can reach any live agent working for that chat, and none
|
|
708
|
+
* working for another.
|
|
709
|
+
*/
|
|
710
|
+
treeOf(id: string): AgentRecord[] {
|
|
711
|
+
const self = this.get(id);
|
|
712
|
+
if (!self) return [];
|
|
713
|
+
const root = this.rootChat(self);
|
|
714
|
+
if (root === null) return [];
|
|
715
|
+
const tree: AgentRecord[] = [];
|
|
716
|
+
for (const entry of this.live.values()) {
|
|
717
|
+
const record = snapshot(entry);
|
|
718
|
+
if (record.id === id) continue;
|
|
719
|
+
if (this.rootChat(record) === root) tree.push(record);
|
|
720
|
+
}
|
|
721
|
+
return tree.sort((a, b) => a.createdAt - b.createdAt);
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
/**
|
|
725
|
+
* Resolve `target` — an agent id, else an exact label — to a live agent
|
|
726
|
+
* in `id`'s tree. A label two live agents share is ambiguous and resolves
|
|
727
|
+
* to nothing, with both candidates returned so the caller can say which.
|
|
728
|
+
*/
|
|
729
|
+
findInTree(
|
|
730
|
+
id: string,
|
|
731
|
+
target: string,
|
|
732
|
+
):
|
|
733
|
+
| { readonly ok: true; readonly record: AgentRecord }
|
|
734
|
+
| { readonly ok: false; readonly candidates: AgentRecord[] } {
|
|
735
|
+
const tree = this.treeOf(id);
|
|
736
|
+
const byId = tree.find((record) => record.id === target);
|
|
737
|
+
if (byId) return { ok: true, record: byId };
|
|
738
|
+
const byLabel = tree.filter((record) => record.label === target);
|
|
739
|
+
if (byLabel.length === 1) return { ok: true, record: byLabel[0]! };
|
|
740
|
+
return { ok: false, candidates: byLabel };
|
|
741
|
+
}
|
|
742
|
+
|
|
701
743
|
/** Live agents plus the bounded settled ring, oldest first. */
|
|
702
744
|
list(): AgentRecord[] {
|
|
703
745
|
const records = [...this.history];
|