talon-agent 5.0.0 → 5.0.1
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 +1 -1
- package/src/app.ts +3 -3
- package/src/backend/claude-sdk/factory.ts +21 -41
- package/src/backend/claude-sdk/host/in-process.ts +147 -0
- package/src/backend/codex/mcp-config.ts +1 -1
- package/src/backend/openai-agents/mcp-pool.ts +1 -1
- package/src/backend/runtime/index.ts +2 -2
- package/src/backend/runtime/turn/delivery.ts +1 -1
- package/src/backend/runtime/turn/turn-phases.ts +1 -1
- package/src/bootstrap.ts +2 -2
- package/src/cli.ts +5 -2
- package/src/core/agent-runtime/README.md +15 -0
- package/src/core/agent-runtime/agent-host.ts +387 -0
- package/src/core/background/cron/scheduler.ts +1 -1
- package/src/core/background/triggers/command.ts +1 -1
- package/src/{util → core/config}/harden.ts +1 -1
- package/src/core/config/index.ts +1 -1
- package/src/core/daemon/pidfile.ts +1 -1
- package/src/core/daemon/resource-sampler.ts +2 -2
- package/src/{util → core/daemon}/respawn.ts +1 -1
- package/src/core/doctor/index.ts +1 -1
- package/src/core/engine/gateway-actions/fetch-url.ts +1 -1
- package/src/core/engine/gateway-actions/index.ts +1 -1
- package/src/core/engine/gateway-actions/memory.ts +2 -2
- package/src/core/engine/gateway-actions/native/exec-background.ts +129 -0
- package/src/core/engine/gateway-actions/native/exec-remote.ts +85 -0
- package/src/core/engine/gateway-actions/native/exec.ts +182 -0
- package/src/core/engine/gateway-actions/native/index.ts +46 -0
- package/src/core/engine/gateway-actions/native/params.ts +46 -0
- package/src/core/engine/gateway-actions/native/read.ts +182 -0
- package/src/core/engine/gateway-actions/native/results.ts +34 -0
- package/src/core/engine/gateway-actions/native/search.ts +212 -0
- package/src/core/engine/gateway-actions/native/shell.ts +39 -0
- package/src/core/engine/gateway-actions/native/teleport.ts +55 -0
- package/src/core/engine/gateway-actions/native/write.ts +170 -0
- package/src/core/frontend-runtime/builtins.ts +2 -2
- package/src/core/mcp-hub/index.ts +1 -1
- package/src/core/mcp-hub/talon-server.ts +1 -1
- package/src/core/memory/taps.ts +1 -1
- package/src/core/plugin/mcp.ts +1 -1
- package/src/core/scripts/lua.ts +1 -1
- package/src/core/tools/{chat.ts → chat/chat.ts} +1 -1
- package/src/core/tools/{cross-send.ts → chat/cross-send.ts} +1 -1
- package/src/core/tools/{history.ts → chat/history.ts} +2 -2
- package/src/core/tools/{media.ts → chat/media.ts} +1 -1
- package/src/core/tools/{members.ts → chat/members.ts} +2 -2
- package/src/core/tools/{messaging.ts → chat/messaging.ts} +2 -2
- package/src/core/tools/{moderation.ts → chat/moderation.ts} +2 -2
- package/src/core/tools/{stickers.ts → chat/stickers.ts} +2 -2
- package/src/core/tools/{whatsapp.ts → chat/whatsapp.ts} +1 -1
- package/src/core/tools/{memory.ts → content/memory.ts} +2 -2
- package/src/core/tools/{web.ts → content/web.ts} +1 -1
- package/src/core/tools/index.ts +20 -20
- package/src/core/tools/{admin.ts → ops/admin.ts} +1 -1
- package/src/core/tools/{bridge.ts → ops/bridge.ts} +2 -2
- package/src/core/tools/{goals.ts → ops/goals.ts} +2 -2
- package/src/core/tools/{mesh.ts → ops/mesh.ts} +1 -1
- package/src/core/tools/{models.ts → ops/models.ts} +1 -1
- package/src/core/tools/{native.ts → ops/native.ts} +1 -1
- package/src/core/tools/{scheduling.ts → ops/scheduling.ts} +1 -1
- package/src/core/tools/{scripts.ts → ops/scripts.ts} +1 -1
- package/src/core/tools/{skills.ts → ops/skills.ts} +1 -1
- package/src/core/tools/{triggers.ts → ops/triggers.ts} +1 -1
- package/src/{util → core/vfs}/workspace.ts +2 -2
- package/src/frontend/discord/callbacks/components/effort.ts +1 -1
- package/src/frontend/discord/callbacks/components/index.ts +1 -1
- package/src/frontend/discord/callbacks/components/settings.ts +1 -1
- package/src/frontend/discord/commands/admin.ts +1 -1
- package/src/frontend/discord/commands/info.ts +1 -1
- package/src/frontend/discord/commands/interaction.ts +1 -1
- package/src/frontend/discord/commands/session.ts +3 -3
- package/src/frontend/discord/commands/settings.ts +1 -1
- package/src/frontend/discord/handlers/messages.ts +1 -1
- package/src/frontend/discord/handlers/state.ts +1 -1
- package/src/frontend/discord/middleware.ts +1 -1
- package/src/frontend/discord/ready.ts +1 -1
- package/src/frontend/discord/render.ts +41 -259
- package/src/frontend/native/chats/chats.ts +1 -1
- package/src/frontend/native/surface/models.ts +1 -1
- package/src/frontend/native/turn/context.ts +1 -1
- package/src/frontend/native/turn/emit.ts +1 -1
- package/src/frontend/{shared → presentation}/format.ts +4 -3
- package/src/frontend/{shared → presentation}/reasoning-levels.ts +12 -0
- package/src/frontend/presentation/reports.ts +493 -0
- package/src/frontend/{shared → presentation}/session-status.ts +1 -1
- package/src/frontend/teams/commands.ts +2 -2
- package/src/frontend/teams/turn.ts +1 -1
- package/src/frontend/telegram/admin/health.ts +1 -1
- package/src/frontend/telegram/admin/sessions.ts +1 -1
- package/src/frontend/telegram/callbacks/effort.ts +2 -2
- package/src/frontend/telegram/callbacks/metrics.ts +1 -1
- package/src/frontend/telegram/callbacks/model/views.ts +1 -1
- package/src/frontend/telegram/callbacks/query.ts +1 -1
- package/src/frontend/telegram/callbacks/settings.ts +2 -2
- package/src/frontend/telegram/commands/admin.ts +4 -4
- package/src/frontend/telegram/commands/info.ts +2 -2
- package/src/frontend/telegram/commands/session.ts +4 -4
- package/src/frontend/telegram/commands/settings.ts +4 -2
- package/src/frontend/telegram/handlers/state.ts +1 -1
- package/src/frontend/telegram/model-menu.ts +1 -1
- package/src/frontend/telegram/render/html.ts +30 -0
- package/src/frontend/telegram/{helpers → render}/menu.ts +46 -19
- package/src/frontend/telegram/render/reports.ts +182 -0
- package/src/frontend/terminal/builtins/context.ts +2 -2
- package/src/frontend/terminal/builtins/session.ts +1 -1
- package/src/frontend/terminal/builtins/status.ts +2 -2
- package/src/frontend/terminal/index.ts +2 -2
- package/src/frontend/terminal/renderer.ts +1 -1
- package/src/frontend/whatsapp/commands.ts +5 -5
- package/src/frontend/whatsapp/registry.ts +1 -1
- package/src/index.ts +5 -2
- package/src/util/runtime.ts +1 -1
- package/src/core/engine/gateway-actions/native.ts +0 -1035
- package/src/frontend/telegram/helpers/diagnostics.ts +0 -428
- package/src/frontend/telegram/helpers/format.ts +0 -42
- package/src/frontend/telegram/helpers/index.ts +0 -13
- package/src/util/cleanup-registry.ts +0 -36
- /package/src/{util → core/daemon}/boot-timer.ts +0 -0
- /package/src/{util → core/frontend-runtime}/chat-id.ts +0 -0
- /package/src/{util/mcp-launcher.ts → core/mcp-hub/launcher.ts} +0 -0
- /package/src/{util → core/tools/content}/web-content.ts +0 -0
- /package/src/core/tools/{mcp-env.ts → ops/mcp-env.ts} +0 -0
- /package/src/{util → core/weaver}/session-name.ts +0 -0
- /package/src/frontend/{shared → presentation}/access.ts +0 -0
- /package/src/frontend/{shared → presentation}/model-commands.ts +0 -0
- /package/src/frontend/{shared → presentation}/plan-usage-report.ts +0 -0
- /package/src/frontend/{shared → presentation}/status-context.ts +0 -0
|
@@ -0,0 +1,387 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The agent-host seam — the contract between the daemon and the process
|
|
3
|
+
* that hosts the Claude Agent SDK.
|
|
4
|
+
*
|
|
5
|
+
* `docs/agent-host-sidecar.md` Phase 1. Today the SDK runs inside the
|
|
6
|
+
* daemon: an OOM or an SDK bug takes down every frontend with it. The
|
|
7
|
+
* sidecar moves it into its own process, and this file is the boundary
|
|
8
|
+
* that move happens across — written first, on purpose, so Phase 2 is a
|
|
9
|
+
* transport swap rather than a redesign.
|
|
10
|
+
*
|
|
11
|
+
* Two halves:
|
|
12
|
+
*
|
|
13
|
+
* - **The wire vocabulary.** `HostRequest` / `HostReply` / `HostEvent` /
|
|
14
|
+
* `HostNotice` are the NDJSON messages of the design's protocol
|
|
15
|
+
* table, one TypeScript type per row, plus `parseHostMessage` /
|
|
16
|
+
* `serializeHostMessage` — the codec both sides run, replayed against
|
|
17
|
+
* `protocol/fixtures/agent-host_v1.json` in
|
|
18
|
+
* `src/__tests__/agent-host-protocol.test.ts`.
|
|
19
|
+
* - **The client.** `AgentHostClient` is what the daemon holds. Phase 1
|
|
20
|
+
* ships one implementation (`backend/claude-sdk/host/in-process.ts`,
|
|
21
|
+
* a direct call-through); Phase 2 adds a second that speaks the
|
|
22
|
+
* messages above over a child process's stdio. The `Backend` object
|
|
23
|
+
* the rest of the daemon sees is identical either way.
|
|
24
|
+
*
|
|
25
|
+
* Layering: the interface and the codec live in `core/` because `core/`
|
|
26
|
+
* may not import `backend/` (depcruise `core-not-to-backend`). Every
|
|
27
|
+
* implementation lives under `backend/`.
|
|
28
|
+
*
|
|
29
|
+
* Wire types vs client types
|
|
30
|
+
* ──────────────────────────
|
|
31
|
+
* They are deliberately not the same types. Two client arguments do not
|
|
32
|
+
* survive a process boundary and the wire shapes say so:
|
|
33
|
+
*
|
|
34
|
+
* - `OneShotAgentParams` carries an `AbortController` and an
|
|
35
|
+
* `appendLog` callback. `HostOneShotParams` is the serialisable
|
|
36
|
+
* subset; Phase 2 maps `appendLog` onto `log` notices and the
|
|
37
|
+
* abort onto an `interrupt`-shaped request.
|
|
38
|
+
* - `hello.config` is the `claude-sdk` slice of `TalonConfig` as
|
|
39
|
+
* JSON. The in-process client takes the real `TalonConfig` object.
|
|
40
|
+
*
|
|
41
|
+
* Forward compatibility: unknown fields on a known message type are
|
|
42
|
+
* additive evolution and must be preserved, never rejected. An unknown
|
|
43
|
+
* `type` parses to `HostUnknown`, which callers log and drop — the codec
|
|
44
|
+
* never throws, so one bad line can't kill a turn or the loop reading it.
|
|
45
|
+
*/
|
|
46
|
+
|
|
47
|
+
import type { AgentEvent, AgentError } from "./events.js";
|
|
48
|
+
import type { ChatRunParams, PlanUsage } from "./capabilities.js";
|
|
49
|
+
import type {
|
|
50
|
+
OneShotAgentParams,
|
|
51
|
+
OneShotUsage,
|
|
52
|
+
ReasoningEffortLevel,
|
|
53
|
+
UnifiedModelInfo,
|
|
54
|
+
} from "../types.js";
|
|
55
|
+
|
|
56
|
+
/** Wire-format version. Bump only on a breaking change — prefer additive. */
|
|
57
|
+
export const AGENT_HOST_PROTOCOL_VERSION = 1;
|
|
58
|
+
|
|
59
|
+
// ── Shared payload shapes ───────────────────────────────────────────────────
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* The serialisable half of `OneShotAgentParams`. `abortController` and
|
|
63
|
+
* `appendLog` are host-local concerns (see the file header); everything
|
|
64
|
+
* else is exactly what a background run needs.
|
|
65
|
+
*
|
|
66
|
+
* Unexported on purpose — it is reachable as
|
|
67
|
+
* `Extract<HostRequest, { type: "one_shot" }>["params"]`, and a second
|
|
68
|
+
* name for the same shape is a thing to keep in sync for nothing.
|
|
69
|
+
*/
|
|
70
|
+
interface HostOneShotParams {
|
|
71
|
+
prompt: string;
|
|
72
|
+
systemPrompt: string;
|
|
73
|
+
workspace: string;
|
|
74
|
+
model: string;
|
|
75
|
+
reasoningEffort?: ReasoningEffortLevel;
|
|
76
|
+
contextLabel: string;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* The MCP diff `set_mcp_servers` / `refresh_tools` answer with — the same
|
|
81
|
+
* shape `ToolRuntime.refreshTools` returns to the dispatcher today.
|
|
82
|
+
*/
|
|
83
|
+
export interface HostToolRefresh {
|
|
84
|
+
added: string[];
|
|
85
|
+
removed: string[];
|
|
86
|
+
errors: Record<string, string>;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* What the host knows about one chat's session. `sessionId` is the SDK's
|
|
91
|
+
* resume handle; the context figures are the ones `warm_session` populates
|
|
92
|
+
* — which is why this query exists at all. In-process those numbers land
|
|
93
|
+
* in the daemon's own session store; across a process boundary they have
|
|
94
|
+
* to be asked for.
|
|
95
|
+
*/
|
|
96
|
+
export interface HostSessionInfo {
|
|
97
|
+
chatId: string;
|
|
98
|
+
sessionId?: string;
|
|
99
|
+
turns: number;
|
|
100
|
+
contextTokens: number;
|
|
101
|
+
contextWindow: number;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** `ready`'s payload — the handshake answer, minus the envelope. */
|
|
105
|
+
export interface HostReadyInfo {
|
|
106
|
+
protocol: number;
|
|
107
|
+
/** The host build's version. */
|
|
108
|
+
host: string;
|
|
109
|
+
/**
|
|
110
|
+
* The Claude Agent SDK version the host is running. Absent in-process,
|
|
111
|
+
* where there is no separately-pinned SDK to report (Phase 4 gives the
|
|
112
|
+
* host its own `package.json`).
|
|
113
|
+
*/
|
|
114
|
+
sdk?: string;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// ── Daemon → host ───────────────────────────────────────────────────────────
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Every request the daemon can send. Each carries an `id`; the reply
|
|
121
|
+
* carries the same `id`. Turn-shaped requests (`run_turn`, `one_shot`)
|
|
122
|
+
* additionally carry a `runId`, which every streamed `event` repeats.
|
|
123
|
+
*/
|
|
124
|
+
export type HostRequest =
|
|
125
|
+
| {
|
|
126
|
+
type: "hello";
|
|
127
|
+
id: string;
|
|
128
|
+
protocol: number;
|
|
129
|
+
/** The daemon's version, for the host's compatibility log line. */
|
|
130
|
+
daemon: string;
|
|
131
|
+
/** The `claude-sdk` slice of `TalonConfig`, as JSON. */
|
|
132
|
+
config: Record<string, unknown>;
|
|
133
|
+
}
|
|
134
|
+
| { type: "run_turn"; id: string; runId: string; params: ChatRunParams }
|
|
135
|
+
| { type: "interrupt"; id: string; chatId: string }
|
|
136
|
+
| { type: "one_shot"; id: string; runId: string; params: HostOneShotParams }
|
|
137
|
+
| { type: "warm_session"; id: string; chatId: string }
|
|
138
|
+
| {
|
|
139
|
+
type: "set_mcp_servers";
|
|
140
|
+
id: string;
|
|
141
|
+
chatId: string;
|
|
142
|
+
/** SDK `McpServerConfig` map, opaque here — the host hands it to the SDK. */
|
|
143
|
+
servers: Record<string, unknown>;
|
|
144
|
+
}
|
|
145
|
+
| { type: "refresh_tools"; id: string; chatId: string }
|
|
146
|
+
| { type: "list_models"; id: string; filter?: "free" | "all" }
|
|
147
|
+
| { type: "plan_usage"; id: string }
|
|
148
|
+
| { type: "session_info"; id: string; chatId: string }
|
|
149
|
+
| { type: "reset_session"; id: string; chatId: string }
|
|
150
|
+
| { type: "shutdown"; id: string };
|
|
151
|
+
|
|
152
|
+
// ── Host → daemon ───────────────────────────────────────────────────────────
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Every reply. `ok` is the generic ack, with one optional field per
|
|
156
|
+
* request that has something to say back; the queries get their own
|
|
157
|
+
* types so a reply is never ambiguous with the request that asked for it
|
|
158
|
+
* (`list_models` → `models`, `plan_usage` → `usage`, `session_info` →
|
|
159
|
+
* `session`).
|
|
160
|
+
*/
|
|
161
|
+
export type HostReply =
|
|
162
|
+
| ({ type: "ready"; id: string } & HostReadyInfo)
|
|
163
|
+
| {
|
|
164
|
+
type: "ok";
|
|
165
|
+
id: string;
|
|
166
|
+
/** `interrupt` — a running turn was found and signalled. */
|
|
167
|
+
interrupted?: boolean;
|
|
168
|
+
/** `set_mcp_servers` / `refresh_tools` — `null` when the chat has no live query. */
|
|
169
|
+
tools?: HostToolRefresh | null;
|
|
170
|
+
/** `reset_session` — host-side per-chat state was dropped. */
|
|
171
|
+
cleared?: boolean;
|
|
172
|
+
}
|
|
173
|
+
| { type: "run_done"; id: string; runId: string; usage?: OneShotUsage }
|
|
174
|
+
| { type: "error"; id: string; error: AgentError }
|
|
175
|
+
| { type: "models"; id: string; models: UnifiedModelInfo[]; total: number }
|
|
176
|
+
| { type: "usage"; id: string; usage?: PlanUsage }
|
|
177
|
+
| { type: "session"; id: string; session?: HostSessionInfo }
|
|
178
|
+
| { type: "bye"; id: string };
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* One turn event. The `AgentEvent` union is unchanged and unwrapped —
|
|
182
|
+
* that is the whole point of the seam: the daemon's consumers switch on
|
|
183
|
+
* `event.type` exactly as they do against an in-process backend.
|
|
184
|
+
*/
|
|
185
|
+
export interface HostEvent {
|
|
186
|
+
type: "event";
|
|
187
|
+
runId: string;
|
|
188
|
+
event: AgentEvent;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Unsolicited host → daemon traffic, carrying no `id` because nothing
|
|
193
|
+
* asked for it. `log` is the host's stdout logging (stderr is tailed by
|
|
194
|
+
* the supervisor like an MCP child's); `metric` forwards the `cache.*`
|
|
195
|
+
* and turn counters the host records so the daemon's rollups are
|
|
196
|
+
* unchanged by the move.
|
|
197
|
+
*/
|
|
198
|
+
export type HostNotice =
|
|
199
|
+
| {
|
|
200
|
+
type: "log";
|
|
201
|
+
level: "debug" | "info" | "warn" | "error";
|
|
202
|
+
component: string;
|
|
203
|
+
msg: string;
|
|
204
|
+
}
|
|
205
|
+
| { type: "metric"; name: string; value: number };
|
|
206
|
+
|
|
207
|
+
/** Anything that can appear on the wire, either direction. */
|
|
208
|
+
export type HostMessage = HostRequest | HostReply | HostEvent | HostNotice;
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* A line the codec could not place. Never thrown — returned, so the
|
|
212
|
+
* reader logs it and drops it. `raw` is whatever came off the wire so
|
|
213
|
+
* the log can say what was skipped.
|
|
214
|
+
*/
|
|
215
|
+
export interface HostUnknown {
|
|
216
|
+
type: "unknown";
|
|
217
|
+
reason: "malformed_json" | "not_an_object" | "unknown_type";
|
|
218
|
+
raw: unknown;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// ── The type registry the codec discriminates on ────────────────────────────
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* `satisfies` rejects typos here; the `AssertNever` checks in
|
|
225
|
+
* `src/__tests__/agent-host-protocol.test.ts` fail to compile when a new
|
|
226
|
+
* member joins a union without being listed — which forces a fixture
|
|
227
|
+
* sample too, because the fixture test asserts these lists exactly.
|
|
228
|
+
*/
|
|
229
|
+
export const HOST_REQUEST_TYPES = [
|
|
230
|
+
"hello",
|
|
231
|
+
"run_turn",
|
|
232
|
+
"interrupt",
|
|
233
|
+
"one_shot",
|
|
234
|
+
"warm_session",
|
|
235
|
+
"set_mcp_servers",
|
|
236
|
+
"refresh_tools",
|
|
237
|
+
"list_models",
|
|
238
|
+
"plan_usage",
|
|
239
|
+
"session_info",
|
|
240
|
+
"reset_session",
|
|
241
|
+
"shutdown",
|
|
242
|
+
] as const satisfies readonly HostRequest["type"][];
|
|
243
|
+
|
|
244
|
+
export const HOST_REPLY_TYPES = [
|
|
245
|
+
"ready",
|
|
246
|
+
"ok",
|
|
247
|
+
"run_done",
|
|
248
|
+
"error",
|
|
249
|
+
"models",
|
|
250
|
+
"usage",
|
|
251
|
+
"session",
|
|
252
|
+
"bye",
|
|
253
|
+
] as const satisfies readonly HostReply["type"][];
|
|
254
|
+
|
|
255
|
+
export const HOST_NOTICE_TYPES = [
|
|
256
|
+
"log",
|
|
257
|
+
"metric",
|
|
258
|
+
] as const satisfies readonly HostNotice["type"][];
|
|
259
|
+
|
|
260
|
+
const KNOWN_TYPES: ReadonlySet<string> = new Set<string>([
|
|
261
|
+
...HOST_REQUEST_TYPES,
|
|
262
|
+
...HOST_REPLY_TYPES,
|
|
263
|
+
...HOST_NOTICE_TYPES,
|
|
264
|
+
"event",
|
|
265
|
+
]);
|
|
266
|
+
|
|
267
|
+
// ── Codec ───────────────────────────────────────────────────────────────────
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Render one message as its NDJSON line — no trailing newline, so the
|
|
271
|
+
* transport owns the framing. Symmetric with `parseHostMessage`.
|
|
272
|
+
*/
|
|
273
|
+
export function serializeHostMessage(message: HostMessage): string {
|
|
274
|
+
return JSON.stringify(message);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Parse one NDJSON line. Total: malformed JSON, non-objects and unknown
|
|
279
|
+
* `type`s all come back as `HostUnknown` rather than throwing, because a
|
|
280
|
+
* single bad line from a newer peer must not take down the read loop.
|
|
281
|
+
*
|
|
282
|
+
* Unknown FIELDS on a known type are preserved as-is — additive evolution
|
|
283
|
+
* is the protocol's normal path, and dropping them here would silently
|
|
284
|
+
* downgrade a message the other end meant to send.
|
|
285
|
+
*/
|
|
286
|
+
export function parseHostMessage(line: string): HostMessage | HostUnknown {
|
|
287
|
+
let parsed: unknown;
|
|
288
|
+
try {
|
|
289
|
+
parsed = JSON.parse(line);
|
|
290
|
+
} catch {
|
|
291
|
+
return { type: "unknown", reason: "malformed_json", raw: line };
|
|
292
|
+
}
|
|
293
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
294
|
+
return { type: "unknown", reason: "not_an_object", raw: parsed };
|
|
295
|
+
}
|
|
296
|
+
const type = (parsed as { type?: unknown }).type;
|
|
297
|
+
if (typeof type !== "string" || !KNOWN_TYPES.has(type)) {
|
|
298
|
+
return { type: "unknown", reason: "unknown_type", raw: parsed };
|
|
299
|
+
}
|
|
300
|
+
return parsed as HostMessage;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// ── The client the daemon holds ─────────────────────────────────────────────
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* What the daemon calls instead of reaching into `backend/claude-sdk/`
|
|
307
|
+
* directly. One method per protocol-table row, with the signatures the
|
|
308
|
+
* claude-sdk `BackendFactory` actually uses today — no capability the
|
|
309
|
+
* backend does not have is invented here.
|
|
310
|
+
*
|
|
311
|
+
* Bound into the `Backend` object in Phase 1: `hello`, `runTurn`,
|
|
312
|
+
* `interrupt`, `runOneShot`, `warmSession`, `refreshTools`, `listModels`,
|
|
313
|
+
* `planUsage`. Defined-but-unbound: `setMcpServers` (the primitive
|
|
314
|
+
* `refreshTools` is built from), `sessionInfo` and `resetSession` (the
|
|
315
|
+
* Claude SDK backend exposes neither a `getSessionSnapshot` nor a
|
|
316
|
+
* `resetChat` slot, and Phase 1 may not change the `Backend` object), and
|
|
317
|
+
* `shutdown` (nothing to stop until there is a process).
|
|
318
|
+
*/
|
|
319
|
+
export interface AgentHostClient {
|
|
320
|
+
/**
|
|
321
|
+
* Handshake + initialisation. In-process this is `initAgent(config)`;
|
|
322
|
+
* across a process it is `hello` → `ready`. Must resolve before any
|
|
323
|
+
* other call — model discovery happens here.
|
|
324
|
+
*/
|
|
325
|
+
hello(): Promise<HostReadyInfo>;
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* One chat turn. The returned stream is the canonical `AgentEvent`
|
|
329
|
+
* sequence, `run_started` first and `completed`/`error` last, exactly
|
|
330
|
+
* as `ChatBackend.runChatTurn` promises.
|
|
331
|
+
*/
|
|
332
|
+
runTurn(params: ChatRunParams): AsyncIterable<AgentEvent>;
|
|
333
|
+
|
|
334
|
+
/** Best-effort stop of a chat's in-flight turn. `true` when one was signalled. */
|
|
335
|
+
interrupt(chatId: string): Promise<boolean>;
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* One background run (heartbeat / dream / cron). Resolves with the
|
|
339
|
+
* run's usage when the SDK reports it.
|
|
340
|
+
*
|
|
341
|
+
* Callback-shaped, not a stream: `OneShotAgentParams.appendLog` is how
|
|
342
|
+
* the background producers write their markdown logs today, and turning
|
|
343
|
+
* that into an event stream would be a behaviour change, not a seam.
|
|
344
|
+
* The design's "stream as above" row is Phase 2's problem, and the wire
|
|
345
|
+
* type (`HostOneShotParams`) already records what has to give.
|
|
346
|
+
*/
|
|
347
|
+
runOneShot(params: OneShotAgentParams): Promise<OneShotUsage | void>;
|
|
348
|
+
|
|
349
|
+
/** Cold-start hint: spawn a throwaway query to prime the context figures. */
|
|
350
|
+
warmSession(chatId: string): Promise<void>;
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* Install an MCP server set on the chat's live query. `null` when the
|
|
354
|
+
* chat has no query in flight. The primitive `refreshTools` is built
|
|
355
|
+
* from; the two-phase teardown lives in the host, not the caller,
|
|
356
|
+
* because `Query` handles do not cross a process boundary.
|
|
357
|
+
*/
|
|
358
|
+
setMcpServers(
|
|
359
|
+
chatId: string,
|
|
360
|
+
servers: Record<string, unknown>,
|
|
361
|
+
): Promise<HostToolRefresh | null>;
|
|
362
|
+
|
|
363
|
+
/** Re-derive the chat's MCP config from the live plugin registry. */
|
|
364
|
+
refreshTools(chatId: string): Promise<HostToolRefresh | null>;
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* The model catalog, as the host discovered it from the SDK. The other
|
|
368
|
+
* seven `ModelCatalog` members are pure daemon-side formatting over
|
|
369
|
+
* `core/models/catalog.ts`, which `hello` populates — see
|
|
370
|
+
* `docs/agent-host-sidecar.md` Phase 1.
|
|
371
|
+
*/
|
|
372
|
+
listModels(
|
|
373
|
+
filter?: "free" | "all",
|
|
374
|
+
): Promise<{ models: UnifiedModelInfo[]; total: number }>;
|
|
375
|
+
|
|
376
|
+
/** Subscription rate-limit windows for `/status`. */
|
|
377
|
+
planUsage(): Promise<PlanUsage | undefined>;
|
|
378
|
+
|
|
379
|
+
/** What the host knows about a chat's session, including the context figures. */
|
|
380
|
+
sessionInfo(chatId: string): Promise<HostSessionInfo | undefined>;
|
|
381
|
+
|
|
382
|
+
/** Drop the host's per-chat state. `true` when there was something to drop. */
|
|
383
|
+
resetSession(chatId: string): Promise<boolean>;
|
|
384
|
+
|
|
385
|
+
/** Drain in-flight turns, then stop. A no-op while the host is in-process. */
|
|
386
|
+
shutdown(): Promise<void>;
|
|
387
|
+
}
|
|
@@ -34,7 +34,7 @@ import {
|
|
|
34
34
|
} from "../../../storage/cron.js";
|
|
35
35
|
import { appendDailyLog } from "../../../storage/daily-log.js";
|
|
36
36
|
import { log, logError, logWarn } from "../../../util/log.js";
|
|
37
|
-
import { numericChatIdFor } from "
|
|
37
|
+
import { numericChatIdFor } from "../../frontend-runtime/chat-id.js";
|
|
38
38
|
import { runJobOneShot } from "./job-oneshot.js";
|
|
39
39
|
import {
|
|
40
40
|
jobAllowsRun,
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
import { spawnSync } from "node:child_process";
|
|
8
8
|
import type { Trigger } from "../../../storage/triggers.js";
|
|
9
|
-
import { selfInvocation } from "
|
|
9
|
+
import { selfInvocation } from "../../mcp-hub/launcher.js";
|
|
10
10
|
import { LUA_RUN_SUBCOMMAND } from "../../scripts/lua.js";
|
|
11
11
|
|
|
12
12
|
export function commandForLanguage(
|
package/src/core/config/index.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { existsSync, readFileSync, mkdirSync } from "node:fs";
|
|
|
2
2
|
import writeFileAtomic from "write-file-atomic";
|
|
3
3
|
import { z } from "zod";
|
|
4
4
|
import { dirs, files as pathFiles } from "../../util/paths.js";
|
|
5
|
-
import { hardenTalonPermissions } from "
|
|
5
|
+
import { hardenTalonPermissions } from "./harden.js";
|
|
6
6
|
import { setTimezone } from "../../util/time.js";
|
|
7
7
|
import { BACKEND_IDS } from "../agent-runtime/model-ref.js";
|
|
8
8
|
import { REASONING_LEVEL_ORDER } from "../models/reasoning-levels.js";
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* gateway may fall back to a different port on EADDRINUSE, so the
|
|
12
12
|
* port is only known at runtime).
|
|
13
13
|
* - Removal is guarded by pid. During a `/restart` handoff
|
|
14
|
-
* (
|
|
14
|
+
* (./respawn.ts) the successor overwrites the file with its own
|
|
15
15
|
* pid *before* the dying parent finishes its graceful shutdown — an
|
|
16
16
|
* unconditional unlink there would orphan the new daemon, making
|
|
17
17
|
* `talon stop`/`talon restart` report "not running" and spawn
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
* - `boot.total_ms` — process start → frontends listening. The figure
|
|
14
14
|
* `Ready in …` already logs, kept as a distribution so successive
|
|
15
15
|
* restarts can be compared instead of grepped.
|
|
16
|
-
* - `boot.<phase>_ms` — each awaited startup phase (
|
|
16
|
+
* - `boot.<phase>_ms` — each awaited startup phase (`./boot-timer.ts`
|
|
17
17
|
* records them), so a slow boot names its own culprit.
|
|
18
18
|
* - `boot.rss_mb` / `boot.heap_mb` — what the process costs the moment
|
|
19
19
|
* it is serving, before any turn has run. The floor an alternative
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
* and a metric that can take the process down is worse than no metric.
|
|
28
28
|
*/
|
|
29
29
|
|
|
30
|
-
import { bootPhases } from "
|
|
30
|
+
import { bootPhases } from "./boot-timer.js";
|
|
31
31
|
import { recordHistogram } from "../../storage/metrics.js";
|
|
32
32
|
|
|
33
33
|
/** One minute. Idle RSS moves slowly; a tighter loop would only add noise. */
|
package/src/core/doctor/index.ts
CHANGED
|
@@ -423,7 +423,7 @@ export async function collectDoctorReport(opts: {
|
|
|
423
423
|
// is which is the difference between "why didn't my prompt update?"
|
|
424
424
|
// and a one-line answer.
|
|
425
425
|
{
|
|
426
|
-
const { promptSeedReport } = await import("
|
|
426
|
+
const { promptSeedReport } = await import("../vfs/workspace.js");
|
|
427
427
|
try {
|
|
428
428
|
const { tracking, edited } = promptSeedReport();
|
|
429
429
|
if (tracking.length + edited.length > 0) {
|
|
@@ -18,7 +18,7 @@ import {
|
|
|
18
18
|
isHtmlContent,
|
|
19
19
|
isTextContent,
|
|
20
20
|
matchesBinaryKind,
|
|
21
|
-
} from "
|
|
21
|
+
} from "../../tools/content/web-content.js";
|
|
22
22
|
import { dirs } from "../../../util/paths.js";
|
|
23
23
|
import type { SharedActionHandlers } from "./types.js";
|
|
24
24
|
|
|
@@ -43,7 +43,7 @@ import {
|
|
|
43
43
|
whatsappAccountHandlers,
|
|
44
44
|
whatsappAccountChatFreeActions,
|
|
45
45
|
} from "./whatsapp-account.js";
|
|
46
|
-
import { nativeHandlers } from "./native.js";
|
|
46
|
+
import { nativeHandlers } from "./native/index.js";
|
|
47
47
|
|
|
48
48
|
// Null-prototype so a request `action` of "toString" / "constructor" / etc.
|
|
49
49
|
// can't resolve an inherited Object.prototype method — `handlers[action]` only
|
|
@@ -42,7 +42,7 @@ import {
|
|
|
42
42
|
type MemorySource,
|
|
43
43
|
type MemoryTrust,
|
|
44
44
|
} from "../../../storage/memory.js";
|
|
45
|
-
import { chatScope } from "
|
|
45
|
+
import { chatScope } from "../../frontend-runtime/chat-id.js";
|
|
46
46
|
import { log } from "../../../util/log.js";
|
|
47
47
|
import type { ActionResult } from "../../types.js";
|
|
48
48
|
import type { SharedActionHandlers } from "./types.js";
|
|
@@ -68,7 +68,7 @@ const KIND_LIST = MEMORY_KINDS.join(", ");
|
|
|
68
68
|
* for its own principal, so that is `agent`.
|
|
69
69
|
*
|
|
70
70
|
* "Group" is read off the canonical chat-id grammar (`chatScope` in
|
|
71
|
-
*
|
|
71
|
+
* core/frontend-runtime/chat-id.ts), which is the only identity a gateway action holds.
|
|
72
72
|
* When the grammar cannot tell — Teams' `teams_chat_…` is 1:1 and group
|
|
73
73
|
* alike — this fails closed to `group_chat`: over-restricting a claim
|
|
74
74
|
* costs a pin, under-restricting one is a permanent prompt injection.
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `native_bash` with background:true — the detached local launch.
|
|
3
|
+
*
|
|
4
|
+
* POSIX-only by contract (own process group, `kill -- -pid` to stop, survives
|
|
5
|
+
* a daemon restart), with a short settle window so a command that dies
|
|
6
|
+
* immediately still reports like a normal foreground run.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { spawn } from "node:child_process";
|
|
10
|
+
import { closeSync, openSync } from "node:fs";
|
|
11
|
+
import { mkdir, readFile } from "node:fs/promises";
|
|
12
|
+
import { tmpdir } from "node:os";
|
|
13
|
+
import { join } from "node:path";
|
|
14
|
+
import { renderExec, type Result } from "./results.js";
|
|
15
|
+
|
|
16
|
+
/** How long a background launch waits to catch fast failures. */
|
|
17
|
+
const BACKGROUND_SETTLE_MS = 1_200;
|
|
18
|
+
/** Where background job output lands (one log file per job). */
|
|
19
|
+
const BACKGROUND_LOG_DIR = join(tmpdir(), "talon-bash");
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Launch a command detached from the request cycle: its own process group,
|
|
23
|
+
* stdout+stderr appended to a per-job log file, tool returns immediately.
|
|
24
|
+
* This is the sanctioned path for streaming/long-running commands (adb
|
|
25
|
+
* logcat, dev servers, watchers) that would otherwise burn the whole
|
|
26
|
+
* foreground timeout and come back "killed".
|
|
27
|
+
*
|
|
28
|
+
* A short settle window catches fast failures (typo'd binary, instant
|
|
29
|
+
* non-zero exit) so those still surface as a normal error instead of a
|
|
30
|
+
* "started" message pointing at a log with one line in it.
|
|
31
|
+
*/
|
|
32
|
+
export async function bashBackground(
|
|
33
|
+
cmd: string,
|
|
34
|
+
cwd: string | undefined,
|
|
35
|
+
): Promise<Result> {
|
|
36
|
+
// The background contract is POSIX-shaped end to end: detached process
|
|
37
|
+
// group, `kill -- -pid` to stop, survives daemon restarts. Windows has
|
|
38
|
+
// none of those (and the CI legs showed the detached writer's output not
|
|
39
|
+
// reaching the log) — refuse loudly with the native alternative instead
|
|
40
|
+
// of pretending.
|
|
41
|
+
if (process.platform === "win32") {
|
|
42
|
+
return {
|
|
43
|
+
ok: false,
|
|
44
|
+
text:
|
|
45
|
+
"background:true needs POSIX process groups and isn't supported on a Windows " +
|
|
46
|
+
"daemon host. Run it foreground with a bound command (`timeout 30 …`, `head -n 200`) " +
|
|
47
|
+
"or start it yourself: `powershell Start-Process -WindowStyle Hidden` with output redirected to a file.",
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
try {
|
|
51
|
+
await mkdir(BACKGROUND_LOG_DIR, { recursive: true });
|
|
52
|
+
} catch (err) {
|
|
53
|
+
return {
|
|
54
|
+
ok: false,
|
|
55
|
+
text: `Cannot create log dir ${BACKGROUND_LOG_DIR}: ${(err as Error).message}`,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
const slug =
|
|
59
|
+
cmd
|
|
60
|
+
.replace(/[^a-zA-Z0-9]+/g, "-")
|
|
61
|
+
.replace(/^-+|-+$/g, "")
|
|
62
|
+
.slice(0, 40) || "job";
|
|
63
|
+
const logPath = join(BACKGROUND_LOG_DIR, `${Date.now()}-${slug}.log`);
|
|
64
|
+
let fd: number;
|
|
65
|
+
try {
|
|
66
|
+
fd = openSync(logPath, "a");
|
|
67
|
+
} catch (err) {
|
|
68
|
+
return {
|
|
69
|
+
ok: false,
|
|
70
|
+
text: `Cannot open log file ${logPath}: ${(err as Error).message}`,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
// Always detached: the win32 guard above returned already, so this only
|
|
74
|
+
// runs on POSIX where the job gets its own process group.
|
|
75
|
+
const child = spawn("bash", ["-c", cmd], {
|
|
76
|
+
...(cwd ? { cwd } : {}),
|
|
77
|
+
env: process.env,
|
|
78
|
+
detached: true,
|
|
79
|
+
stdio: ["ignore", fd, fd],
|
|
80
|
+
});
|
|
81
|
+
return new Promise((resolvePromise) => {
|
|
82
|
+
let settled = false;
|
|
83
|
+
const done = (r: Result) => {
|
|
84
|
+
if (settled) return;
|
|
85
|
+
settled = true;
|
|
86
|
+
try {
|
|
87
|
+
closeSync(fd);
|
|
88
|
+
} catch {
|
|
89
|
+
// parent's dup only; the child keeps its own copy either way
|
|
90
|
+
}
|
|
91
|
+
resolvePromise(r);
|
|
92
|
+
};
|
|
93
|
+
child.on("error", (err) =>
|
|
94
|
+
done({ ok: false, text: `Failed to start: ${err.message}` }),
|
|
95
|
+
);
|
|
96
|
+
// Fast failure inside the settle window → report it like a normal run.
|
|
97
|
+
child.on("close", (code) => {
|
|
98
|
+
void (async () => {
|
|
99
|
+
let logged = "";
|
|
100
|
+
try {
|
|
101
|
+
logged = await readFile(logPath, "utf8");
|
|
102
|
+
} catch {
|
|
103
|
+
// log unreadable — report the exit alone
|
|
104
|
+
}
|
|
105
|
+
done({
|
|
106
|
+
ok: (code ?? 0) === 0,
|
|
107
|
+
text:
|
|
108
|
+
`Background command exited almost immediately (exit ${code ?? 0}).\n` +
|
|
109
|
+
renderExec("local", `exit ${code ?? 0}`, logged, "") +
|
|
110
|
+
`\nFull log: ${logPath}`,
|
|
111
|
+
});
|
|
112
|
+
})();
|
|
113
|
+
});
|
|
114
|
+
setTimeout(() => {
|
|
115
|
+
if (settled) return;
|
|
116
|
+
child.unref();
|
|
117
|
+
done({
|
|
118
|
+
ok: true,
|
|
119
|
+
text: [
|
|
120
|
+
`🚀 Started in background [local] — pid ${child.pid}.`,
|
|
121
|
+
`Output (stdout+stderr) → ${logPath}`,
|
|
122
|
+
`Follow it with read/bash (e.g. \`tail -n 50 ${logPath}\`).`,
|
|
123
|
+
`Stop it with \`kill -- -${child.pid}\` (whole process group).`,
|
|
124
|
+
`Unsupervised: it keeps running until it exits or is killed — it even survives a Talon restart.`,
|
|
125
|
+
].join("\n"),
|
|
126
|
+
});
|
|
127
|
+
}, BACKGROUND_SETTLE_MS);
|
|
128
|
+
});
|
|
129
|
+
}
|