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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "talon-agent",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.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",
|
|
@@ -51,12 +51,14 @@
|
|
|
51
51
|
"format:check": "prettier --check src/ prompts/"
|
|
52
52
|
},
|
|
53
53
|
"dependencies": {
|
|
54
|
-
"@anthropic-ai/claude-agent-sdk": "^0.2.
|
|
54
|
+
"@anthropic-ai/claude-agent-sdk": "^0.2.104",
|
|
55
|
+
"@brave/brave-search-mcp-server": "^2.0.75",
|
|
55
56
|
"@clack/prompts": "^1.2.0",
|
|
56
57
|
"@grammyjs/auto-retry": "^2.0.2",
|
|
57
58
|
"@grammyjs/transformer-throttler": "^1.2.1",
|
|
58
59
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
59
60
|
"@opencode-ai/sdk": "^1.4.0",
|
|
61
|
+
"@playwright/mcp": "^0.0.70",
|
|
60
62
|
"big-integer": "^1.6.52",
|
|
61
63
|
"cheerio": "^1.2.0",
|
|
62
64
|
"croner": "^10.0.1",
|
package/prompts/heartbeat.md
CHANGED
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
You are Talon's background heartbeat agent. You run periodically (every {{intervalMinutes}} minutes) to perform maintenance tasks defined by the user.
|
|
2
2
|
|
|
3
|
-
You have access
|
|
3
|
+
You have access to filesystem tools (Read, Write, Edit, Bash, Glob, Grep) and all loaded MCP plugins. Do NOT use Telegram messaging tools — you cannot send messages to users.
|
|
4
|
+
|
|
5
|
+
## Available MCP Tools
|
|
6
|
+
|
|
7
|
+
You have access to all registered MCP plugin tools (excluding Telegram messaging tools). The exact set depends on what plugins are enabled in the current configuration, but may include email, memory/knowledge graph, web search, Wikipedia, GitHub, media processing, browser automation, and more.
|
|
8
|
+
|
|
9
|
+
Only use tools that are actually available in your current session. Do not assume any specific tool is present — check what's exposed to you at runtime.
|
|
10
|
+
|
|
11
|
+
Use available tools when they help accomplish the user-defined tasks (e.g. checking email, querying the knowledge graph, searching the web for updates).
|
|
4
12
|
|
|
5
13
|
## Context
|
|
6
14
|
|
|
@@ -20,11 +28,15 @@ If the instructions file does not exist or is empty, perform these default tasks
|
|
|
20
28
|
1. **Review recent logs** — Check `{{logsDir}}/` for log files dated after `{{lastRunIso}}`. If `{{lastRunIso}}` is `never`, treat it as the beginning of time and review all available logs. Extract any new facts, preferences, or notable events.
|
|
21
29
|
2. **Update memory** — Merge any new information into `{{memoryFile}}`, keeping entries concise and factual.
|
|
22
30
|
3. **Update daily notes** — Write today's learnings, observations, corrections, and follow-ups to `{{dailyMemoryFile}}`. Keep entries concise — the bot reads this file on demand for context.
|
|
23
|
-
4. **
|
|
31
|
+
4. **Check email** — If email tools are available, check the inbox for new messages and note anything important.
|
|
32
|
+
5. **Workspace hygiene** — Note any issues but do not delete files unless the instructions explicitly say to.
|
|
24
33
|
|
|
25
34
|
## Rules
|
|
26
35
|
|
|
27
|
-
-
|
|
28
|
-
-
|
|
29
|
-
-
|
|
30
|
-
-
|
|
36
|
+
- Do NOT use Telegram messaging tools — they are not available in heartbeat mode.
|
|
37
|
+
- Be concise in log entries and memory updates.
|
|
38
|
+
- If a task fails, log the error and move on to the next task.
|
|
39
|
+
- Do NOT modify the instructions file — only read it.
|
|
40
|
+
- Be surgical: only make the minimal file changes needed to complete the current task.
|
|
41
|
+
- Do NOT create, modify, move, or delete files outside `{{workspace}}` unless the user-defined instructions explicitly require it.
|
|
42
|
+
- Complete all tasks within the time budget. If running low, prioritize memory updates.
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { ALL_TOOLS, composeTools } from "../core/tools/index.js";
|
|
3
|
+
import type {
|
|
4
|
+
ToolDefinition,
|
|
5
|
+
ToolFrontend,
|
|
6
|
+
ToolTag,
|
|
7
|
+
} from "../core/tools/types.js";
|
|
8
|
+
|
|
9
|
+
describe("ALL_TOOLS registry", () => {
|
|
10
|
+
it("contains tools from every domain", () => {
|
|
11
|
+
const tags = new Set(ALL_TOOLS.map((t) => t.tag));
|
|
12
|
+
expect(tags).toContain("messaging");
|
|
13
|
+
expect(tags).toContain("chat");
|
|
14
|
+
expect(tags).toContain("history");
|
|
15
|
+
expect(tags).toContain("members");
|
|
16
|
+
expect(tags).toContain("media");
|
|
17
|
+
expect(tags).toContain("stickers");
|
|
18
|
+
expect(tags).toContain("scheduling");
|
|
19
|
+
expect(tags).toContain("web");
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it("has no duplicate tool names", () => {
|
|
23
|
+
const names = ALL_TOOLS.map((t) => t.name);
|
|
24
|
+
expect(new Set(names).size).toBe(names.length);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it("every tool has required fields", () => {
|
|
28
|
+
for (const tool of ALL_TOOLS) {
|
|
29
|
+
expect(tool.name).toBeTruthy();
|
|
30
|
+
expect(tool.description).toBeTruthy();
|
|
31
|
+
expect(tool.schema).toBeDefined();
|
|
32
|
+
expect(typeof tool.execute).toBe("function");
|
|
33
|
+
expect(tool.tag).toBeTruthy();
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
describe("composeTools()", () => {
|
|
39
|
+
it("returns all tools when no options are given", () => {
|
|
40
|
+
const tools = composeTools();
|
|
41
|
+
expect(tools).toHaveLength(ALL_TOOLS.length);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("returns a new array (not a reference to ALL_TOOLS)", () => {
|
|
45
|
+
const tools = composeTools();
|
|
46
|
+
expect(tools).not.toBe(ALL_TOOLS);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
// ── Frontend filtering ────────────────────────────────────────────────
|
|
50
|
+
|
|
51
|
+
it("filters tools by telegram frontend", () => {
|
|
52
|
+
const tools = composeTools({ frontend: "telegram" });
|
|
53
|
+
// Should include telegram-specific and universal tools
|
|
54
|
+
expect(tools.length).toBeGreaterThan(0);
|
|
55
|
+
expect(tools.length).toBeLessThan(ALL_TOOLS.length);
|
|
56
|
+
|
|
57
|
+
for (const t of tools) {
|
|
58
|
+
const f = t.frontends;
|
|
59
|
+
expect(!f || f.includes("all") || f.includes("telegram")).toBe(true);
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("filters tools by teams frontend", () => {
|
|
64
|
+
const tools = composeTools({ frontend: "teams" });
|
|
65
|
+
expect(tools.length).toBeGreaterThan(0);
|
|
66
|
+
|
|
67
|
+
for (const t of tools) {
|
|
68
|
+
const f = t.frontends;
|
|
69
|
+
expect(!f || f.includes("all") || f.includes("teams")).toBe(true);
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("excludes telegram-only tools from teams", () => {
|
|
74
|
+
const teamsTools = composeTools({ frontend: "teams" });
|
|
75
|
+
const teamsNames = new Set(teamsTools.map((t) => t.name));
|
|
76
|
+
|
|
77
|
+
// react is telegram-only
|
|
78
|
+
expect(teamsNames.has("react")).toBe(false);
|
|
79
|
+
// send_message is teams-only — should be present
|
|
80
|
+
expect(teamsNames.has("send_message")).toBe(true);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it("excludes teams-only tools from telegram", () => {
|
|
84
|
+
const tgTools = composeTools({ frontend: "telegram" });
|
|
85
|
+
const tgNames = new Set(tgTools.map((t) => t.name));
|
|
86
|
+
|
|
87
|
+
// send_message is teams-only
|
|
88
|
+
expect(tgNames.has("send_message")).toBe(false);
|
|
89
|
+
// send is telegram-only — should be present
|
|
90
|
+
expect(tgNames.has("send")).toBe(true);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it("includes universal tools (no frontends set) for any frontend", () => {
|
|
94
|
+
const universalTools = ALL_TOOLS.filter((t) => !t.frontends);
|
|
95
|
+
expect(universalTools.length).toBeGreaterThan(0);
|
|
96
|
+
|
|
97
|
+
for (const frontend of [
|
|
98
|
+
"telegram",
|
|
99
|
+
"teams",
|
|
100
|
+
"terminal",
|
|
101
|
+
] as ToolFrontend[]) {
|
|
102
|
+
const tools = composeTools({ frontend });
|
|
103
|
+
const names = new Set(tools.map((t) => t.name));
|
|
104
|
+
for (const ut of universalTools) {
|
|
105
|
+
expect(names.has(ut.name)).toBe(true);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it("includes tools with frontends: ['all'] for any frontend", () => {
|
|
111
|
+
const allFrontendTools = ALL_TOOLS.filter((t) =>
|
|
112
|
+
t.frontends?.includes("all"),
|
|
113
|
+
);
|
|
114
|
+
// Even if there are none right now, the filter logic is tested via universal tools
|
|
115
|
+
for (const frontend of [
|
|
116
|
+
"telegram",
|
|
117
|
+
"teams",
|
|
118
|
+
"terminal",
|
|
119
|
+
] as ToolFrontend[]) {
|
|
120
|
+
const tools = composeTools({ frontend });
|
|
121
|
+
const names = new Set(tools.map((t) => t.name));
|
|
122
|
+
for (const t of allFrontendTools) {
|
|
123
|
+
expect(names.has(t.name)).toBe(true);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
// ── Tag filtering ─────────────────────────────────────────────────────
|
|
129
|
+
|
|
130
|
+
it("filters by tags (include)", () => {
|
|
131
|
+
const tools = composeTools({ tags: ["web"] });
|
|
132
|
+
expect(tools.length).toBeGreaterThan(0);
|
|
133
|
+
for (const t of tools) {
|
|
134
|
+
expect(t.tag).toBe("web");
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it("filters by multiple tags", () => {
|
|
139
|
+
const tools = composeTools({ tags: ["web", "scheduling"] });
|
|
140
|
+
expect(tools.length).toBeGreaterThan(0);
|
|
141
|
+
for (const t of tools) {
|
|
142
|
+
expect(["web", "scheduling"]).toContain(t.tag);
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it("filters by excludeTags", () => {
|
|
147
|
+
const tools = composeTools({ excludeTags: ["stickers", "media"] });
|
|
148
|
+
for (const t of tools) {
|
|
149
|
+
expect(t.tag).not.toBe("stickers");
|
|
150
|
+
expect(t.tag).not.toBe("media");
|
|
151
|
+
}
|
|
152
|
+
expect(tools.length).toBeLessThan(ALL_TOOLS.length);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
// ── Name exclusion ────────────────────────────────────────────────────
|
|
156
|
+
|
|
157
|
+
it("excludes tools by name", () => {
|
|
158
|
+
const tools = composeTools({ excludeNames: ["send", "react"] });
|
|
159
|
+
const names = new Set(tools.map((t) => t.name));
|
|
160
|
+
expect(names.has("send")).toBe(false);
|
|
161
|
+
expect(names.has("react")).toBe(false);
|
|
162
|
+
expect(tools.length).toBe(ALL_TOOLS.length - 2);
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
// ── Combined filters ──────────────────────────────────────────────────
|
|
166
|
+
|
|
167
|
+
it("combines frontend + tag filters", () => {
|
|
168
|
+
const tools = composeTools({ frontend: "telegram", tags: ["messaging"] });
|
|
169
|
+
expect(tools.length).toBeGreaterThan(0);
|
|
170
|
+
for (const t of tools) {
|
|
171
|
+
expect(t.tag).toBe("messaging");
|
|
172
|
+
const f = t.frontends;
|
|
173
|
+
expect(!f || f.includes("all") || f.includes("telegram")).toBe(true);
|
|
174
|
+
}
|
|
175
|
+
// Should NOT include teams send_message
|
|
176
|
+
const names = new Set(tools.map((t) => t.name));
|
|
177
|
+
expect(names.has("send_message")).toBe(false);
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it("combines frontend + excludeTags", () => {
|
|
181
|
+
const tools = composeTools({
|
|
182
|
+
frontend: "telegram",
|
|
183
|
+
excludeTags: ["stickers"],
|
|
184
|
+
});
|
|
185
|
+
for (const t of tools) {
|
|
186
|
+
expect(t.tag).not.toBe("stickers");
|
|
187
|
+
}
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it("combines frontend + excludeNames", () => {
|
|
191
|
+
const tools = composeTools({
|
|
192
|
+
frontend: "telegram",
|
|
193
|
+
excludeNames: ["fetch_url"],
|
|
194
|
+
});
|
|
195
|
+
const names = new Set(tools.map((t) => t.name));
|
|
196
|
+
expect(names.has("fetch_url")).toBe(false);
|
|
197
|
+
expect(names.has("send")).toBe(true);
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
// ── Edge cases ────────────────────────────────────────────────────────
|
|
201
|
+
|
|
202
|
+
it("returns empty array when tags match nothing", () => {
|
|
203
|
+
const tools = composeTools({ tags: ["nonexistent" as ToolTag] });
|
|
204
|
+
expect(tools).toEqual([]);
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
it("returns all tools when excludeNames is empty", () => {
|
|
208
|
+
const tools = composeTools({ excludeNames: [] });
|
|
209
|
+
expect(tools).toHaveLength(ALL_TOOLS.length);
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
it("returns all tools when excludeTags is empty", () => {
|
|
213
|
+
const tools = composeTools({ excludeTags: [] });
|
|
214
|
+
expect(tools).toHaveLength(ALL_TOOLS.length);
|
|
215
|
+
});
|
|
216
|
+
});
|
|
@@ -193,7 +193,6 @@ describe("fuzz: handleSharedAction() — unknown actions", () => {
|
|
|
193
193
|
"get_user_messages",
|
|
194
194
|
"list_known_users",
|
|
195
195
|
"list_media",
|
|
196
|
-
"web_search",
|
|
197
196
|
"fetch_url",
|
|
198
197
|
"create_cron_job",
|
|
199
198
|
"list_cron_jobs",
|
|
@@ -224,7 +223,6 @@ describe("fuzz: handleSharedAction() — unknown actions", () => {
|
|
|
224
223
|
"get_user_messages",
|
|
225
224
|
"list_known_users",
|
|
226
225
|
"list_media",
|
|
227
|
-
"web_search",
|
|
228
226
|
"fetch_url",
|
|
229
227
|
"create_cron_job",
|
|
230
228
|
"list_cron_jobs",
|