talon-agent 5.1.0 → 5.2.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.
Files changed (99) hide show
  1. package/README.md +3 -1
  2. package/bin/talon.js +35 -0
  3. package/package.json +2 -2
  4. package/prompts/identity.md +10 -2
  5. package/prompts/system/agent-brief.md +43 -0
  6. package/src/app.ts +19 -26
  7. package/src/backend/builtins.ts +26 -7
  8. package/src/backend/claude-sdk/one-shot.ts +13 -5
  9. package/src/backend/remote-server/index.ts +6 -4
  10. package/src/backend/remote-server/model-catalog/index.ts +4 -10
  11. package/src/backend/remote-server/model-catalog/provider.ts +3 -3
  12. package/src/backend/remote-server/profiles/bind.ts +225 -0
  13. package/src/backend/remote-server/profiles/index.ts +10 -0
  14. package/src/backend/remote-server/profiles/kilo.ts +82 -0
  15. package/src/backend/remote-server/profiles/opencode.ts +61 -0
  16. package/src/backend/remote-server/server-bindings.ts +3 -4
  17. package/src/bootstrap.ts +15 -1
  18. package/src/cli/chat.ts +5 -0
  19. package/src/cli/events.ts +9 -0
  20. package/src/core/agents/context.ts +48 -0
  21. package/src/core/agents/delivery.ts +167 -0
  22. package/src/core/agents/index.ts +37 -0
  23. package/src/core/agents/prompt.ts +116 -0
  24. package/src/core/agents/registry.ts +426 -0
  25. package/src/core/agents/runner.ts +448 -0
  26. package/src/core/agents/types.ts +124 -0
  27. package/src/core/background/cron/job-oneshot.ts +7 -12
  28. package/src/core/background/cron/job-prompt.ts +1 -1
  29. package/src/core/background/{cron/isolated-agent.ts → isolated-agent.ts} +45 -24
  30. package/src/core/background/run-log.ts +33 -0
  31. package/src/core/bus/events.ts +49 -2
  32. package/src/core/config/index.ts +25 -0
  33. package/src/core/engine/gateway-actions/agents/control.ts +299 -0
  34. package/src/core/engine/gateway-actions/agents/index.ts +31 -0
  35. package/src/core/engine/gateway-actions/agents/report.ts +107 -0
  36. package/src/core/engine/gateway-actions/index.ts +30 -0
  37. package/src/core/engine/gateway-actions/native/exec-remote.ts +1 -1
  38. package/src/core/engine/gateway-actions/native/exec.ts +1 -1
  39. package/src/core/engine/gateway-actions/native/read.ts +1 -1
  40. package/src/core/engine/gateway-actions/native/search.ts +1 -1
  41. package/src/core/engine/gateway-actions/native/teleport.ts +1 -1
  42. package/src/core/engine/gateway-actions/native/write.ts +1 -1
  43. package/src/core/engine/gateway-routes.ts +12 -0
  44. package/src/core/engine/gateway.ts +96 -25
  45. package/src/core/frontend-runtime/capabilities.ts +18 -0
  46. package/src/core/frontend-runtime/index.ts +4 -0
  47. package/src/core/frontend-runtime/lifecycle.ts +33 -0
  48. package/src/core/frontend-runtime/registry.ts +3 -3
  49. package/src/core/frontend-runtime/run-loop.ts +59 -0
  50. package/src/core/mesh/{registry.ts → devices/registry.ts} +4 -4
  51. package/src/core/mesh/{service.ts → devices/service.ts} +13 -10
  52. package/src/core/mesh/{teleport.ts → devices/teleport.ts} +2 -2
  53. package/src/core/mesh/index.ts +6 -2
  54. package/src/core/mesh/{bridge-links.ts → links/bridge-links.ts} +1 -1
  55. package/src/core/mesh/{companion-pairing.ts → links/companion-pairing.ts} +1 -1
  56. package/src/core/mesh/{node-binaries.ts → links/node-binaries.ts} +5 -5
  57. package/src/core/mesh/{node-provision.ts → links/node-provision.ts} +1 -1
  58. package/src/core/mesh/{common.ts → tool-surface.ts} +7 -2
  59. package/src/core/mesh/{device-files.ts → transfers/device-files.ts} +5 -5
  60. package/src/core/prompt/embedded-prompts.ts +38 -36
  61. package/src/core/tasks/types.ts +2 -2
  62. package/src/core/tools/index.ts +5 -0
  63. package/src/core/tools/ops/agents.ts +195 -0
  64. package/src/core/tools/ops/bridge.ts +4 -0
  65. package/src/core/tools/types.ts +1 -0
  66. package/src/core/types.ts +1 -1
  67. package/src/frontend/discord/commands/info.ts +1 -1
  68. package/src/frontend/discord/render.ts +1 -1
  69. package/src/frontend/native/bridge/routes/mesh.ts +1 -1
  70. package/src/frontend/native/index.ts +2 -0
  71. package/src/frontend/presentation/reports.ts +1 -1
  72. package/src/frontend/teams/index.ts +4 -3
  73. package/src/frontend/telegram/commands/info.ts +61 -20
  74. package/src/frontend/telegram/index.ts +27 -4
  75. package/src/frontend/telegram/render/reports.ts +1 -1
  76. package/src/frontend/terminal/index.ts +6 -2
  77. package/src/frontend/whatsapp/connection/connection.ts +62 -11
  78. package/src/frontend/whatsapp/index.ts +25 -1
  79. package/src/frontend/whatsapp/runtime.ts +7 -0
  80. package/src/util/log.ts +1 -0
  81. package/src/backend/kilo/factory.ts +0 -53
  82. package/src/backend/kilo/handler/index.ts +0 -2
  83. package/src/backend/kilo/handler/message.ts +0 -44
  84. package/src/backend/kilo/index.ts +0 -61
  85. package/src/backend/kilo/model-provider.ts +0 -36
  86. package/src/backend/kilo/models/index.ts +0 -55
  87. package/src/backend/kilo/one-shot.ts +0 -42
  88. package/src/backend/kilo/server.ts +0 -98
  89. package/src/backend/kilo/sessions.ts +0 -37
  90. package/src/backend/opencode/factory.ts +0 -53
  91. package/src/backend/opencode/handler/index.ts +0 -2
  92. package/src/backend/opencode/handler/message.ts +0 -44
  93. package/src/backend/opencode/index.ts +0 -42
  94. package/src/backend/opencode/model-provider.ts +0 -36
  95. package/src/backend/opencode/models/index.ts +0 -54
  96. package/src/backend/opencode/one-shot.ts +0 -42
  97. package/src/backend/opencode/server.ts +0 -80
  98. package/src/backend/opencode/sessions.ts +0 -35
  99. /package/src/core/mesh/{transfers.ts → transfers/transfers.ts} +0 -0
@@ -13,8 +13,8 @@
13
13
 
14
14
  import { readFile } from "node:fs/promises";
15
15
  import { resolve } from "node:path";
16
- import { dirs } from "../../util/paths.js";
17
- import { writePrivateJson } from "./persist.js";
16
+ import { dirs } from "../../../util/paths.js";
17
+ import { writePrivateJson } from "../persist.js";
18
18
 
19
19
  export type TeleportState = {
20
20
  /** Target device id (as registered in the mesh). */
@@ -6,5 +6,9 @@
6
6
  * from here.
7
7
  */
8
8
 
9
- export { MeshRegistry } from "./registry.js";
10
- export { MeshService, getMeshService, setMeshService } from "./service.js";
9
+ export { MeshRegistry } from "./devices/registry.js";
10
+ export {
11
+ MeshService,
12
+ getMeshService,
13
+ setMeshService,
14
+ } from "./devices/service.js";
@@ -16,7 +16,7 @@ import {
16
16
  pairPage,
17
17
  type CompanionPairPayload,
18
18
  } from "./companion-pairing.js";
19
- import { formatBytes, type MeshToolResult } from "./common.js";
19
+ import { formatBytes, type MeshToolResult } from "../tool-surface.js";
20
20
  import {
21
21
  NODE_TARGETS,
22
22
  normalizeGoarch,
@@ -11,7 +11,7 @@
11
11
  * So the daemon mints a grant and hands out ONE link. The bridge serves it
12
12
  * on a route that must work pre-auth — the phone holds no credential yet, so
13
13
  * the grant token IS the authorization, the same trust model as node
14
- * provisioning (node-provision.ts) and streamed transfers (transfers.ts):
14
+ * provisioning (node-provision.ts) and streamed transfers (transfers/transfers.ts):
15
15
  * random 192-bit, single-use, expiring unused.
16
16
  *
17
17
  * GET /pair?grant=<token> → the pairing page (once)
@@ -34,11 +34,11 @@ import {
34
34
  writeFile,
35
35
  } from "node:fs/promises";
36
36
  import { join, resolve } from "node:path";
37
- import { TalonError } from "../errors.js";
38
- import { getRepoRoot } from "../update/self-update.js";
39
- import { dirs } from "../../util/paths.js";
40
- import { talonVersion } from "../../util/version.js";
41
- import type { DevicePlatform } from "./types.js";
37
+ import { TalonError } from "../../errors.js";
38
+ import { getRepoRoot } from "../../update/self-update.js";
39
+ import { dirs } from "../../../util/paths.js";
40
+ import { talonVersion } from "../../../util/version.js";
41
+ import type { DevicePlatform } from "../types.js";
42
42
 
43
43
  /** A cross-compile target, named exactly as Go (and the release assets) do. */
44
44
  export type NodeTarget = {
@@ -11,7 +11,7 @@
11
11
  *
12
12
  * The routes must work pre-auth — the whole point is a host that holds no
13
13
  * bridge credential yet — so the grant token IS the authorization, exactly
14
- * like streamed-transfer tokens (transfers.ts): random 192-bit, single-use
14
+ * like streamed-transfer tokens (transfers/transfers.ts): random 192-bit, single-use
15
15
  * per leg, expiring unused. The script carries the bridge bearer token and
16
16
  * pinned TLS fingerprint into the node's config, verifies the downloaded
17
17
  * binary against the grant's digest (integrity is the script's own sha256
@@ -1,6 +1,11 @@
1
1
  /**
2
- * Small helpers shared by the mesh service and its collaborators
3
- * (device-files.ts, bridge-links.ts).
2
+ * The mesh's model-facing tool surface — the vocabulary every mesh tool
3
+ * answers in: the result shape, the timeout a single device filesystem
4
+ * command runs under, and the formatters those answer texts are written
5
+ * with.
6
+ *
7
+ * All three concerns speak it (devices/service.ts, links/bridge-links.ts,
8
+ * transfers/device-files.ts), so it sits at the component root.
4
9
  */
5
10
 
6
11
  /** The model-facing shape every mesh tool answers with. */
@@ -21,20 +21,20 @@ import { mkdir, readFile, rm, stat, writeFile } from "node:fs/promises";
21
21
  import { tmpdir } from "node:os";
22
22
  import { basename, dirname, join, resolve } from "node:path";
23
23
  import type { Readable } from "node:stream";
24
- import { dirs } from "../../util/paths.js";
24
+ import { dirs } from "../../../util/paths.js";
25
25
  import {
26
26
  formatBytes,
27
27
  FS_COMMAND_TIMEOUT_MS,
28
28
  requirePath,
29
29
  type MeshToolResult,
30
- } from "./common.js";
30
+ } from "../tool-surface.js";
31
31
  import {
32
32
  normalizeGoarch,
33
33
  platformToGoos,
34
34
  type NodeBinaryResolver,
35
- } from "./node-binaries.js";
35
+ } from "../links/node-binaries.js";
36
36
  import { TransferStore } from "./transfers.js";
37
- import type { DeviceCommandResult, DeviceInfo } from "./types.js";
37
+ import type { DeviceCommandResult, DeviceInfo } from "../types.js";
38
38
 
39
39
  /** The slice of MeshService a transfer needs. */
40
40
  export interface DeviceFilesHost {
@@ -531,7 +531,7 @@ export class DeviceFiles {
531
531
  *
532
532
  * With no binary_path, the replacement is auto-resolved for the node's
533
533
  * registered platform/arch (source build in a dev checkout, else the
534
- * version-matched release download — see node-binaries.ts).
534
+ * version-matched release download — see links/node-binaries.ts).
535
535
  */
536
536
  async updateNodeBinary(
537
537
  query: unknown,
@@ -19,24 +19,25 @@ import asset5 from "../../../prompts/identity.md" with { type: "file" };
19
19
  import asset6 from "../../../prompts/mem0.md" with { type: "file" };
20
20
  import asset7 from "../../../prompts/mempalace.md" with { type: "file" };
21
21
  import asset8 from "../../../prompts/native.md" with { type: "file" };
22
- import asset9 from "../../../prompts/system/contract-text-or-tools.md" with { type: "file" };
23
- import asset10 from "../../../prompts/system/contract-text-preferred.md" with { type: "file" };
24
- import asset11 from "../../../prompts/system/contract-tool-only.md" with { type: "file" };
25
- import asset12 from "../../../prompts/system/cron.md" with { type: "file" };
26
- import asset13 from "../../../prompts/system/daily-memory.md" with { type: "file" };
27
- import asset14 from "../../../prompts/system/goals.md" with { type: "file" };
28
- import asset15 from "../../../prompts/system/heartbeat-agent.md" with { type: "file" };
29
- import asset16 from "../../../prompts/system/live-state.md" with { type: "file" };
30
- import asset17 from "../../../prompts/system/memory-core-view.md" with { type: "file" };
31
- import asset18 from "../../../prompts/system/memory-recall.md" with { type: "file" };
32
- import asset19 from "../../../prompts/system/persistent-memory.md" with { type: "file" };
33
- import asset20 from "../../../prompts/system/skills.md" with { type: "file" };
34
- import asset21 from "../../../prompts/system/triggers.md" with { type: "file" };
35
- import asset22 from "../../../prompts/system/workspace.md" with { type: "file" };
36
- import asset23 from "../../../prompts/teams.md" with { type: "file" };
37
- import asset24 from "../../../prompts/telegram.md" with { type: "file" };
38
- import asset25 from "../../../prompts/terminal.md" with { type: "file" };
39
- import asset26 from "../../../prompts/whatsapp.md" with { type: "file" };
22
+ import asset9 from "../../../prompts/system/agent-brief.md" with { type: "file" };
23
+ import asset10 from "../../../prompts/system/contract-text-or-tools.md" with { type: "file" };
24
+ import asset11 from "../../../prompts/system/contract-text-preferred.md" with { type: "file" };
25
+ import asset12 from "../../../prompts/system/contract-tool-only.md" with { type: "file" };
26
+ import asset13 from "../../../prompts/system/cron.md" with { type: "file" };
27
+ import asset14 from "../../../prompts/system/daily-memory.md" with { type: "file" };
28
+ import asset15 from "../../../prompts/system/goals.md" with { type: "file" };
29
+ import asset16 from "../../../prompts/system/heartbeat-agent.md" with { type: "file" };
30
+ import asset17 from "../../../prompts/system/live-state.md" with { type: "file" };
31
+ import asset18 from "../../../prompts/system/memory-core-view.md" with { type: "file" };
32
+ import asset19 from "../../../prompts/system/memory-recall.md" with { type: "file" };
33
+ import asset20 from "../../../prompts/system/persistent-memory.md" with { type: "file" };
34
+ import asset21 from "../../../prompts/system/skills.md" with { type: "file" };
35
+ import asset22 from "../../../prompts/system/triggers.md" with { type: "file" };
36
+ import asset23 from "../../../prompts/system/workspace.md" with { type: "file" };
37
+ import asset24 from "../../../prompts/teams.md" with { type: "file" };
38
+ import asset25 from "../../../prompts/telegram.md" with { type: "file" };
39
+ import asset26 from "../../../prompts/terminal.md" with { type: "file" };
40
+ import asset27 from "../../../prompts/whatsapp.md" with { type: "file" };
40
41
 
41
42
  /** rel path (posix, under prompts/) → embedded file path (/$bunfs/… when compiled). */
42
43
  const ASSETS: Record<string, string> = {
@@ -49,24 +50,25 @@ const ASSETS: Record<string, string> = {
49
50
  "mem0.md": asset6,
50
51
  "mempalace.md": asset7,
51
52
  "native.md": asset8,
52
- "system/contract-text-or-tools.md": asset9,
53
- "system/contract-text-preferred.md": asset10,
54
- "system/contract-tool-only.md": asset11,
55
- "system/cron.md": asset12,
56
- "system/daily-memory.md": asset13,
57
- "system/goals.md": asset14,
58
- "system/heartbeat-agent.md": asset15,
59
- "system/live-state.md": asset16,
60
- "system/memory-core-view.md": asset17,
61
- "system/memory-recall.md": asset18,
62
- "system/persistent-memory.md": asset19,
63
- "system/skills.md": asset20,
64
- "system/triggers.md": asset21,
65
- "system/workspace.md": asset22,
66
- "teams.md": asset23,
67
- "telegram.md": asset24,
68
- "terminal.md": asset25,
69
- "whatsapp.md": asset26,
53
+ "system/agent-brief.md": asset9,
54
+ "system/contract-text-or-tools.md": asset10,
55
+ "system/contract-text-preferred.md": asset11,
56
+ "system/contract-tool-only.md": asset12,
57
+ "system/cron.md": asset13,
58
+ "system/daily-memory.md": asset14,
59
+ "system/goals.md": asset15,
60
+ "system/heartbeat-agent.md": asset16,
61
+ "system/live-state.md": asset17,
62
+ "system/memory-core-view.md": asset18,
63
+ "system/memory-recall.md": asset19,
64
+ "system/persistent-memory.md": asset20,
65
+ "system/skills.md": asset21,
66
+ "system/triggers.md": asset22,
67
+ "system/workspace.md": asset23,
68
+ "teams.md": asset24,
69
+ "telegram.md": asset25,
70
+ "terminal.md": asset26,
71
+ "whatsapp.md": asset27,
70
72
  };
71
73
 
72
74
  /** Read an embedded prompt by its rel path (e.g. "system/cron.md"). */
@@ -2,7 +2,7 @@
2
2
  * Task table vocabulary — the daemon's registry of agent work.
3
3
  *
4
4
  * A task is one bounded run of agent work: a chat turn, a heartbeat pass, a
5
- * dream consolidation, or an isolated cron/trigger job. The table gives every
5
+ * dream consolidation, an isolated cron/trigger job, or a sub-agent run. The table gives every
6
6
  * such run an id, a lifecycle, an owner chat, and — where the run can be
7
7
  * aborted — a kill switch. It is the process-table analogue for the agent
8
8
  * runtime, with tokens (not CPU) as the accounted resource.
@@ -13,7 +13,7 @@
13
13
  */
14
14
 
15
15
  /** Which unit of agent work a task represents. */
16
- type TaskKind = "turn" | "heartbeat" | "dream" | "cron" | "trigger";
16
+ type TaskKind = "turn" | "heartbeat" | "dream" | "cron" | "trigger" | "agent";
17
17
 
18
18
  /** Task lifecycle. `done`, `failed`, and `killed` are terminal. */
19
19
  export type TaskState = "queued" | "running" | "done" | "failed" | "killed";
@@ -15,6 +15,7 @@ import { mediaTools } from "./chat/media.js";
15
15
  import { stickerTools } from "./chat/stickers.js";
16
16
  import { schedulingTools } from "./ops/scheduling.js";
17
17
  import { triggerTools } from "./ops/triggers.js";
18
+ import { agentTools } from "./ops/agents.js";
18
19
  import { goalTools } from "./ops/goals.js";
19
20
  import { memoryTools } from "./content/memory.js";
20
21
  import { scriptTools } from "./ops/scripts.js";
@@ -49,6 +50,10 @@ export const ALL_TOOLS: readonly ToolDefinition[] = [
49
50
  ...crossSendTools,
50
51
  ...whatsappTools,
51
52
  ...moderationTools,
53
+ // Appended, not inserted: ALL_TOOLS is serialised into the prompt-cache
54
+ // prefix, so a new family goes on the end or every live chat re-bills its
55
+ // system prompt (see compose-tools.test.ts).
56
+ ...agentTools,
52
57
  ];
53
58
 
54
59
  /**
@@ -0,0 +1,195 @@
1
+ /**
2
+ * Sub-agent tools — delegate work to isolated agents and talk to them.
3
+ *
4
+ * Two halves. The **parent** half (`spawn_agent`, `list_agents`,
5
+ * `agent_status`, `wait_for_agent`, `send_to_agent`, `kill_agent`) is what a
6
+ * chat turn — or an agent that is itself delegating — uses. The **agent**
7
+ * half (`report_result`, `message_parent`, `check_inbox`) only works inside a
8
+ * sub-agent run; called from a chat it is refused, because there is no parent
9
+ * to report to.
10
+ *
11
+ * Talon owns the mechanism, not the backend: a sub-agent is an isolated
12
+ * one-shot run, so this works identically on Claude, Codex, Kilo and
13
+ * OpenCode, and a chat on one backend can spawn an agent on another.
14
+ */
15
+
16
+ import { z } from "zod";
17
+ import type { ToolDefinition } from "../types.js";
18
+
19
+ const SPAWN_DESCRIPTION = `Delegate a self-contained piece of work to a sub-agent and keep working.
20
+
21
+ The agent runs isolated: its own backend, model, context window and tool
22
+ surface, no conversation history, only the brief you write. It reports back
23
+ by calling report_result, which wakes this chat with a system message
24
+ carrying its summary — you do NOT have to wait for it.
25
+
26
+ Write the brief as if to a capable colleague who knows nothing about this
27
+ conversation: the objective, the context they need, what "done" looks like,
28
+ and what to report. Everything they should know must be in the brief.
29
+
30
+ Good uses: research that would flood this context, a long build/test/verify
31
+ loop, several independent investigations at once, work that should run on a
32
+ different model than this chat.
33
+
34
+ Bad uses: anything needing a back-and-forth with the user, trivial work you
35
+ could do in one tool call, or work whose result you need in the next second
36
+ (spawning costs a model cold-start).
37
+
38
+ Backend and model default to this chat's; pass them to put the agent
39
+ somewhere else (e.g. a cheap model for a mechanical sweep, or a backend with
40
+ a bigger context window). Returns the agent id immediately.`;
41
+
42
+ export const agentTools: ToolDefinition[] = [
43
+ {
44
+ name: "spawn_agent",
45
+ description: SPAWN_DESCRIPTION,
46
+ schema: {
47
+ brief: z
48
+ .string()
49
+ .min(1)
50
+ .describe(
51
+ "The full instructions for the agent. Self-contained: it has no history, only this.",
52
+ ),
53
+ label: z
54
+ .string()
55
+ .min(1)
56
+ .max(48)
57
+ .describe(
58
+ "Short content-free name for listings and logs (e.g. 'pr-triage', 'docs-audit'). Never message content.",
59
+ ),
60
+ backend: z
61
+ .string()
62
+ .optional()
63
+ .describe(
64
+ "Backend id to run on. Unset = this chat's backend (agents inherit their parent's). Must have a background capability.",
65
+ ),
66
+ model: z
67
+ .string()
68
+ .optional()
69
+ .describe(
70
+ "Model id on the chosen backend. Unset = that backend's default model. Call list_models for valid ids.",
71
+ ),
72
+ effort: z
73
+ .enum(["minimal", "low", "medium", "high", "xhigh"])
74
+ .optional()
75
+ .describe(
76
+ "Reasoning effort, where the backend has that knob (Claude, Codex). Ignored elsewhere.",
77
+ ),
78
+ timeout_s: z
79
+ .number()
80
+ .int()
81
+ .positive()
82
+ .optional()
83
+ .describe(
84
+ "Hard wall-clock cap in seconds (default 900, min 30, max 3600). On timeout the agent is aborted and you are told.",
85
+ ),
86
+ },
87
+ execute: (params, bridge) => bridge("spawn_agent", params),
88
+ tag: "agents",
89
+ },
90
+
91
+ {
92
+ name: "list_agents",
93
+ description:
94
+ "List this chat's sub-agents (descendants included) with their state, backend, model, age and whether a result is available.",
95
+ schema: {},
96
+ execute: (_params, bridge) => bridge("list_agents", {}),
97
+ tag: "agents",
98
+ },
99
+
100
+ {
101
+ name: "agent_status",
102
+ description:
103
+ "Full status of one sub-agent: state, backend/model, timings, inbox depth, children, and its result or error once it has settled.",
104
+ schema: {
105
+ agent_id: z.string().describe("Agent id returned by spawn_agent"),
106
+ },
107
+ execute: (params, bridge) => bridge("agent_status", params),
108
+ tag: "agents",
109
+ },
110
+
111
+ {
112
+ name: "wait_for_agent",
113
+ description:
114
+ "Block until one agent settles, or until the timeout — whichever comes first. This is a convenience for short waits only: the primary completion channel is the wake-up message the agent's report triggers, which arrives whether or not anyone is waiting. Prefer finishing your turn and letting the report wake you; use this only when the very next thing you do depends on the answer.",
115
+ schema: {
116
+ agent_id: z.string().describe("Agent id to wait on"),
117
+ timeout_s: z
118
+ .number()
119
+ .int()
120
+ .positive()
121
+ .max(120)
122
+ .optional()
123
+ .describe(
124
+ "Seconds to wait (default 60, max 120). Returns the current state on timeout.",
125
+ ),
126
+ },
127
+ execute: (params, bridge) => bridge("wait_for_agent", params),
128
+ tag: "agents",
129
+ },
130
+
131
+ {
132
+ name: "send_to_agent",
133
+ description:
134
+ "Put an instruction in a running agent's inbox. The agent sees it the next time it calls check_inbox — this is not an interrupt, so do not expect an immediate change of course.",
135
+ schema: {
136
+ agent_id: z.string().describe("Agent id to message"),
137
+ text: z.string().min(1).describe("The instruction or context to send"),
138
+ },
139
+ execute: (params, bridge) => bridge("send_to_agent", params),
140
+ tag: "agents",
141
+ },
142
+
143
+ {
144
+ name: "kill_agent",
145
+ description:
146
+ "Abort a running agent. It settles as 'killed' and you are still told — with whatever it had reported before it died.",
147
+ schema: {
148
+ agent_id: z.string().describe("Agent id to abort"),
149
+ },
150
+ execute: (params, bridge) => bridge("kill_agent", params),
151
+ tag: "agents",
152
+ },
153
+
154
+ // ── Agent-side: only valid inside a sub-agent run ─────────────────────────
155
+
156
+ {
157
+ name: "report_result",
158
+ description:
159
+ "Sub-agents only. Report your result to whoever spawned you. Call this exactly once, when you are done — it is the ONLY channel your result reaches your parent through. Reporting a failure with what you tried is a valid result; silence is not. Calling this outside a sub-agent run is an error.",
160
+ schema: {
161
+ summary: z
162
+ .string()
163
+ .min(1)
164
+ .describe(
165
+ "A few sentences your parent can act on: what you found or did.",
166
+ ),
167
+ details: z
168
+ .string()
169
+ .optional()
170
+ .describe("Optional evidence: paths, commands, numbers, excerpts."),
171
+ },
172
+ execute: (params, bridge) => bridge("report_result", params),
173
+ tag: "agents",
174
+ },
175
+
176
+ {
177
+ name: "message_parent",
178
+ description:
179
+ "Sub-agents only. Send an interim note to whoever spawned you — a finding worth acting on now, a question, or a heads-up. It wakes them, so use it sparingly. It does not end your run and does not count as your result.",
180
+ schema: {
181
+ text: z.string().min(1).describe("The note to send"),
182
+ },
183
+ execute: (params, bridge) => bridge("message_parent", params),
184
+ tag: "agents",
185
+ },
186
+
187
+ {
188
+ name: "check_inbox",
189
+ description:
190
+ "Sub-agents only. Drain any instructions your parent has sent you. Messages are delivered no other way, so check at milestones: after a phase of work, before a long operation, and before you report.",
191
+ schema: {},
192
+ execute: (_params, bridge) => bridge("check_inbox", {}),
193
+ tag: "agents",
194
+ },
195
+ ];
@@ -20,6 +20,10 @@ const DEFAULT_TIMEOUT_MS = 120_000;
20
20
  * engaged, so they inherit transfer-grade budgets too.
21
21
  */
22
22
  const LONG_ACTION_TIMEOUTS_MS: Record<string, number> = {
23
+ // `wait_for_agent` blocks for up to its own 120s cap; the bridge budget
24
+ // must sit above it so the layer that gives up is the tool (which reports
25
+ // the agent's live state), never the transport.
26
+ wait_for_agent: 180_000,
23
27
  device_pull_file: 3_600_000,
24
28
  device_push_file: 3_600_000,
25
29
  device_read_file: 3_600_000,
@@ -21,6 +21,7 @@ export type ToolTag =
21
21
  | "stickers"
22
22
  | "scheduling"
23
23
  | "triggers"
24
+ | "agents"
24
25
  | "goals"
25
26
  | "memory"
26
27
  | "scripts"
package/src/core/types.ts CHANGED
@@ -230,7 +230,7 @@ export type ExecuteParams = {
230
230
  isGroup: boolean;
231
231
  /** Provider message ID. Numeric for Telegram, string snowflake for Discord. */
232
232
  messageId?: number | string;
233
- source: "message" | "pulse" | "cron" | "trigger";
233
+ source: "message" | "pulse" | "cron" | "trigger" | "agent";
234
234
  /**
235
235
  * Optional per-run model override (a model id valid on the chat's backend).
236
236
  * When set and resolvable, the turn runs on this model instead of the chat's
@@ -16,7 +16,7 @@ import type { TalonConfig } from "../../../core/config/index.js";
16
16
  import { collectPlanUsage } from "../../presentation/plan-usage-report.js";
17
17
  import { getLoadedPlugins } from "../../../core/plugin/index.js";
18
18
  import { getMeshService } from "../../../core/mesh/index.js";
19
- import type { MeshPingResult } from "../../../core/mesh/service.js";
19
+ import type { MeshPingResult } from "../../../core/mesh/devices/service.js";
20
20
  import { reply } from "./interaction.js";
21
21
 
22
22
  export async function handleStart(
@@ -19,7 +19,7 @@ import {
19
19
  splitMessage,
20
20
  } from "./formatting.js";
21
21
  import type { DoctorReport } from "../../core/doctor/index.js";
22
- import type { MeshPingResult } from "../../core/mesh/service.js";
22
+ import type { MeshPingResult } from "../../core/mesh/devices/service.js";
23
23
  import type { BackendUsageEntry } from "../presentation/plan-usage-report.js";
24
24
  import {
25
25
  renderDoctorReport,
@@ -3,7 +3,7 @@ import type { BridgeRoutes } from "./table.js";
3
3
  import type { ServerResponse } from "node:http";
4
4
  import { deviceIdParam } from "./params.js";
5
5
 
6
- // Streamed device file transfers (see core/mesh/transfers.ts). The
6
+ // Streamed device file transfers (see core/mesh/transfers/transfers.ts). The
7
7
  // one-time `transfer` token authorizes exactly one direction+path; the
8
8
  // caller names itself so the token's device binding can be checked.
9
9
  function transferToken(
@@ -195,6 +195,8 @@ export function createNativeFrontend(
195
195
  });
196
196
  },
197
197
 
198
+ // Nothing to run: init() already bound the bridge server, so the
199
+ // frontend is listening the moment start() is called.
198
200
  async start() {
199
201
  log(
200
202
  "native",
@@ -23,7 +23,7 @@
23
23
  */
24
24
 
25
25
  import type { DoctorReport } from "../../core/doctor/index.js";
26
- import type { MeshPingResult } from "../../core/mesh/service.js";
26
+ import type { MeshPingResult } from "../../core/mesh/devices/service.js";
27
27
  import type { BackendUsageEntry } from "./plan-usage-report.js";
28
28
  import {
29
29
  DEFAULT_PULSE_INTERVAL_MS,
@@ -97,10 +97,11 @@ export function createTeamsFrontend(
97
97
  `Receive: Graph API chat polling every ${runtime.pollIntervalMs / 1000}s`,
98
98
  );
99
99
 
100
+ // The receive side is a timer on the runtime, not a loop to sit in:
101
+ // once the first poll is done the frontend is listening, and
102
+ // start() is finished. (It used to park on a promise that never
103
+ // resolved, which made the boot end at shutdown.)
100
104
  await startPolling(runtime, chatId);
101
-
102
- // Hold process open
103
- await new Promise(() => {});
104
105
  },
105
106
 
106
107
  async stop() {