@agentproto/runtime 0.1.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/LICENSE +21 -0
- package/README.md +88 -0
- package/dist/config.d.ts +144 -0
- package/dist/config.mjs +76 -0
- package/dist/config.mjs.map +1 -0
- package/dist/conversations.d.ts +60 -0
- package/dist/conversations.mjs +146 -0
- package/dist/conversations.mjs.map +1 -0
- package/dist/heartbeat-COGpMrJS.d.ts +120 -0
- package/dist/heartbeat.d.ts +2 -0
- package/dist/heartbeat.mjs +185 -0
- package/dist/heartbeat.mjs.map +1 -0
- package/dist/index.d.ts +546 -0
- package/dist/index.mjs +4350 -0
- package/dist/index.mjs.map +1 -0
- package/dist/mcp-imports.d.ts +117 -0
- package/dist/mcp-imports.mjs +82 -0
- package/dist/mcp-imports.mjs.map +1 -0
- package/dist/resume-strategies.d.ts +66 -0
- package/dist/resume-strategies.mjs +64 -0
- package/dist/resume-strategies.mjs.map +1 -0
- package/dist/workspace-fs.d.ts +26 -0
- package/dist/workspace-fs.mjs +60 -0
- package/dist/workspace-fs.mjs.map +1 -0
- package/dist/workspaces-config.d.ts +81 -0
- package/dist/workspaces-config.mjs +136 -0
- package/dist/workspaces-config.mjs.map +1 -0
- package/package.json +103 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,546 @@
|
|
|
1
|
+
import { DoctypeSpec } from '@agentproto/manifest';
|
|
2
|
+
import { B as BuildHeartbeatAgent } from './heartbeat-COGpMrJS.js';
|
|
3
|
+
export { H as HeartbeatAgent, a as HeartbeatRunner, R as RuntimeEvent, b as RuntimeEvents, p as parseDuration } from './heartbeat-COGpMrJS.js';
|
|
4
|
+
import { ChildProcess } from 'node:child_process';
|
|
5
|
+
import { WorkspaceFs } from './workspace-fs.js';
|
|
6
|
+
export { createWorkspaceFs } from './workspace-fs.js';
|
|
7
|
+
export { ConversationMeta, ConversationStore, ConversationTurn, fileConversationStore } from './conversations.js';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Sessions registry — tracks long-lived child processes spawned by
|
|
11
|
+
* the agentproto daemon (terminals, agent CLIs, custom commands).
|
|
12
|
+
*
|
|
13
|
+
* The registry lives in-memory for the daemon's lifetime; a recent
|
|
14
|
+
* snapshot also persists to `~/.agentproto/sessions.json` so a fresh
|
|
15
|
+
* daemon restart can show "you had these running before" instead of
|
|
16
|
+
* losing all visibility on a crash.
|
|
17
|
+
*
|
|
18
|
+
* Each session owns:
|
|
19
|
+
* - identity: id, kind, workspace slug, command + args, started time
|
|
20
|
+
* - lifecycle: status (starting/running/exited/killed/error)
|
|
21
|
+
* - output: last N lines (ring buffer) for instant attach + a
|
|
22
|
+
* Node EventEmitter for live streaming consumers
|
|
23
|
+
*
|
|
24
|
+
* Consumers:
|
|
25
|
+
* - HTTP routes (GET /sessions, /sessions/:id, /sessions/:id/stream,
|
|
26
|
+
* POST /sessions/:id/kill)
|
|
27
|
+
* - CLI `agentproto sessions` (TUI navigation + attach)
|
|
28
|
+
* - guilde-web Active tab (session cards + terminal viewer)
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Minimal shape we need from a driver-agent-cli session — kept as a
|
|
33
|
+
* structural type so the runtime package doesn't take a hard
|
|
34
|
+
* dependency on @agentproto/driver-agent-cli (the http-server route
|
|
35
|
+
* imports it and passes the constructed session in).
|
|
36
|
+
*/
|
|
37
|
+
interface AgentSessionLike {
|
|
38
|
+
sessionId: string;
|
|
39
|
+
send(message: unknown): AsyncIterable<AgentStreamEvent>;
|
|
40
|
+
cancel(): Promise<void>;
|
|
41
|
+
close(): Promise<void>;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Minimal PTY surface — structurally compatible with
|
|
45
|
+
* @agentproto/acp/tunnel's PtyProcess (node-pty's IPty wrapper). The
|
|
46
|
+
* runtime declares its own so this package stays native-free; the
|
|
47
|
+
* cli's pty-factory util constructs values that satisfy both.
|
|
48
|
+
*/
|
|
49
|
+
interface PtyProcess {
|
|
50
|
+
readonly pid: number;
|
|
51
|
+
write(data: string): void;
|
|
52
|
+
resize(cols: number, rows: number): void;
|
|
53
|
+
kill(signal?: string): void;
|
|
54
|
+
onData(handler: (data: string) => void): void;
|
|
55
|
+
onExit(handler: (event: {
|
|
56
|
+
exitCode: number;
|
|
57
|
+
signal?: number;
|
|
58
|
+
}) => void): void;
|
|
59
|
+
}
|
|
60
|
+
interface PtyFactoryOptions {
|
|
61
|
+
command: string;
|
|
62
|
+
args: string[];
|
|
63
|
+
cwd?: string;
|
|
64
|
+
env?: Record<string, string>;
|
|
65
|
+
cols: number;
|
|
66
|
+
rows: number;
|
|
67
|
+
}
|
|
68
|
+
type PtyFactory = (opts: PtyFactoryOptions) => PtyProcess;
|
|
69
|
+
interface AgentStreamEvent {
|
|
70
|
+
kind: string;
|
|
71
|
+
text?: string;
|
|
72
|
+
toolName?: string;
|
|
73
|
+
isError?: boolean;
|
|
74
|
+
reason?: string;
|
|
75
|
+
error?: {
|
|
76
|
+
message: string;
|
|
77
|
+
code?: number;
|
|
78
|
+
data?: unknown;
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
type SessionKind = "terminal" | "agent-cli" | "command";
|
|
82
|
+
type SessionStatus = "starting" | "running" | "exited" | "killed" | "error";
|
|
83
|
+
interface SessionDescriptor {
|
|
84
|
+
id: string;
|
|
85
|
+
kind: SessionKind;
|
|
86
|
+
workspaceSlug: string;
|
|
87
|
+
/** What was actually run — quoted joined for display purposes
|
|
88
|
+
* (`bash -lc "claude --resume xyz"`). */
|
|
89
|
+
command: string;
|
|
90
|
+
pid: number | null;
|
|
91
|
+
status: SessionStatus;
|
|
92
|
+
startedAt: string;
|
|
93
|
+
endedAt?: string;
|
|
94
|
+
exitCode?: number;
|
|
95
|
+
/** Last time anything was written to stdout/stderr. Lets the UI
|
|
96
|
+
* spot stuck sessions ("running for 2h, last output 12min ago"). */
|
|
97
|
+
lastOutputAt?: string;
|
|
98
|
+
/** Free-text label the spawner can attach (e.g. conversation id,
|
|
99
|
+
* operator name) so the UI can group/filter. */
|
|
100
|
+
label?: string;
|
|
101
|
+
/** True when the session was spawned under a real PTY (node-pty)
|
|
102
|
+
* instead of `child_process.spawn`. PTY sessions carry raw ANSI
|
|
103
|
+
* bytes (alt-screen, key bindings, colors); attach goes through
|
|
104
|
+
* WS /sessions/:id/pty rather than SSE /sessions/:id/stream. */
|
|
105
|
+
pty?: boolean;
|
|
106
|
+
/** User-friendly slug supplied at spawn time. Optional; when set,
|
|
107
|
+
* attach/stop/kill commands accept it as an alias for the id. */
|
|
108
|
+
name?: string;
|
|
109
|
+
/** Argv that was spawned. Persisted so `agentproto sessions restart`
|
|
110
|
+
* can clone the original shape without re-tokenizing the display
|
|
111
|
+
* `command` string. Empty for agent-cli sessions (we record
|
|
112
|
+
* `adapterSlug` separately). */
|
|
113
|
+
argv?: readonly string[];
|
|
114
|
+
/** Working directory the session was spawned in. Cloned by restart. */
|
|
115
|
+
cwd?: string;
|
|
116
|
+
/** Adapter slug for agent-cli sessions — restart uses this with
|
|
117
|
+
* `/sessions/agent` to spin up a fresh ACP runtime. Undefined for
|
|
118
|
+
* pty/command kinds. */
|
|
119
|
+
adapterSlug?: string;
|
|
120
|
+
/** ACP-level session id (the adapter's own handle — claude-code's
|
|
121
|
+
* conversation id, hermes' chat id, …). Set at spawnAgent time
|
|
122
|
+
* from `agentSession.sessionId`. Survives across daemon restarts
|
|
123
|
+
* via sessions.json so `restart` can pass it as `resumeSessionId`
|
|
124
|
+
* and reattach to the prior conversation history. */
|
|
125
|
+
adapterSessionId?: string;
|
|
126
|
+
/** Provider-specific resume hints sniffed from the session's
|
|
127
|
+
* output. claude-code prints `claude --resume <uuid>` on exit;
|
|
128
|
+
* we capture that uuid as `claudeResumeId`. On `restart`, when a
|
|
129
|
+
* hint is present, agentproto prefers a PTY spawn with the
|
|
130
|
+
* provider's native resume command (`claude --resume <id>`) over
|
|
131
|
+
* the ACP-level resume — works wherever the provider persisted
|
|
132
|
+
* the session, not just when the ACP wrapper did.
|
|
133
|
+
*
|
|
134
|
+
* Keys are adapter-specific so future adapters can add their own
|
|
135
|
+
* ("hermesResumeId", etc.) without changing this type. */
|
|
136
|
+
resumeMetadata?: Record<string, string>;
|
|
137
|
+
}
|
|
138
|
+
interface SessionsRegistry {
|
|
139
|
+
spawn(input: SpawnSessionInput): SessionDescriptor;
|
|
140
|
+
/** Adopt a ChildProcess that was spawned outside the registry —
|
|
141
|
+
* e.g. by the tunnel server when the host dispatches a spawn
|
|
142
|
+
* frame. The registry attaches its stdout/stderr listeners and
|
|
143
|
+
* tracks the descriptor so the child shows up in /sessions and
|
|
144
|
+
* the LocalDaemonSessionsCard. The caller keeps lifecycle
|
|
145
|
+
* ownership (kill / wait); this is purely for observability.
|
|
146
|
+
* Returns the descriptor for the registered session. */
|
|
147
|
+
register(input: RegisterSessionInput): SessionDescriptor;
|
|
148
|
+
/** Register an already-built agent session (caller resolves the
|
|
149
|
+
* adapter + builds the runtime). Sets kind="agent-cli", consumes
|
|
150
|
+
* the optional initial prompt synchronously, returns the
|
|
151
|
+
* descriptor. The agent stays alive — call `sendPrompt(id, ...)`
|
|
152
|
+
* for follow-up turns until `kill(id)` closes it. */
|
|
153
|
+
spawnAgent(input: SpawnAgentInput): SessionDescriptor;
|
|
154
|
+
/** Spawn a process under a real PTY (node-pty). Bytes flow through
|
|
155
|
+
* the registry's byte ring buffer + emitter; attach with
|
|
156
|
+
* `attachPty(id, ...)`. Throws when the registry was constructed
|
|
157
|
+
* without a `spawnPty` factory (node-pty optional dep missing). */
|
|
158
|
+
spawnPty(input: SpawnPtyInput): SessionDescriptor;
|
|
159
|
+
/** Send a follow-up turn to a live agent session. Throws when the
|
|
160
|
+
* session is missing, not an agent-cli kind, or busy. The events
|
|
161
|
+
* stream into the existing ring buffer + line emitter so /stream
|
|
162
|
+
* consumers see them as they arrive. */
|
|
163
|
+
sendPrompt(id: string, message: unknown): Promise<void>;
|
|
164
|
+
/** Fire-and-forget variant of `sendPrompt`. Validates the same
|
|
165
|
+
* preconditions synchronously — throws on missing session / wrong
|
|
166
|
+
* kind / busy — but returns immediately after the turn is started,
|
|
167
|
+
* letting the caller stream output via the SSE endpoint instead of
|
|
168
|
+
* blocking on the full turn. Async errors during the turn are
|
|
169
|
+
* pushed into the ring buffer as `[error]` lines so `/stream`
|
|
170
|
+
* subscribers see them. Used by the web UI's chat input where a
|
|
171
|
+
* long turn would otherwise freeze the textbox. */
|
|
172
|
+
enqueuePrompt(id: string, message: unknown): void;
|
|
173
|
+
list(): SessionDescriptor[];
|
|
174
|
+
get(id: string): SessionDescriptor | undefined;
|
|
175
|
+
/** Subscribe to a session's output. Returns an unsubscribe fn.
|
|
176
|
+
* Initial backfill: synchronously invokes `onLine` once for each
|
|
177
|
+
* line currently in the ring buffer so attaches show context. */
|
|
178
|
+
attach(id: string, onLine: (line: string, stream: "stdout" | "stderr") => void): (() => void) | null;
|
|
179
|
+
/** Subscribe to a PTY session's byte stream. Returns a control
|
|
180
|
+
* handle (write/resize/detach) and null when the session is
|
|
181
|
+
* missing or not a PTY kind. Replays the ring buffer
|
|
182
|
+
* synchronously before live frames start. Initial `cols`/`rows`
|
|
183
|
+
* are required so the registry can compute min-size across
|
|
184
|
+
* subscribers; recompute on every `resize` call. */
|
|
185
|
+
attachPty(id: string, initial: {
|
|
186
|
+
cols: number;
|
|
187
|
+
rows: number;
|
|
188
|
+
}, onData: (chunk: Buffer) => void, onExit: (event: {
|
|
189
|
+
exitCode: number;
|
|
190
|
+
signal?: number;
|
|
191
|
+
}) => void): {
|
|
192
|
+
write(data: string): void;
|
|
193
|
+
resize(cols: number, rows: number): void;
|
|
194
|
+
detach(): void;
|
|
195
|
+
} | null;
|
|
196
|
+
/** Find a session by exact id OR exact name match. Returns the
|
|
197
|
+
* descriptor or undefined. Used by HTTP/MCP routes so callers
|
|
198
|
+
* can refer to "claude-main" instead of "sess_a3f8c1b2". */
|
|
199
|
+
findByIdOrName(query: string): SessionDescriptor | undefined;
|
|
200
|
+
/** Write a chunk of text to a PTY session's stdin. Returns true
|
|
201
|
+
* on success, false when the session is missing or not a PTY.
|
|
202
|
+
* Single-shot variant of attachPty().write — useful for MCP
|
|
203
|
+
* tools that drive a session through tool calls. */
|
|
204
|
+
writeTerminalInput(id: string, data: string): boolean;
|
|
205
|
+
/** Snapshot the recent PTY byte buffer as one Buffer. Newest
|
|
206
|
+
* bytes at the end; `lastBytes` caps the returned size from the
|
|
207
|
+
* tail. Returns null when the session is missing or not a PTY. */
|
|
208
|
+
readTerminalOutput(id: string, lastBytes?: number): Buffer | null;
|
|
209
|
+
kill(id: string, signal?: NodeJS.Signals): boolean;
|
|
210
|
+
/** Stop tracking a session (after it exited and the user clicked
|
|
211
|
+
* "clear"). Doesn't kill — use `kill` first. */
|
|
212
|
+
forget(id: string): boolean;
|
|
213
|
+
/** Stop persisting + close all sessions. Killed children are not
|
|
214
|
+
* awaited — the daemon shutdown loop handles process tree teardown. */
|
|
215
|
+
shutdown(): void;
|
|
216
|
+
}
|
|
217
|
+
interface RegisterSessionInput {
|
|
218
|
+
/** Pre-existing child process to adopt (e.g. from a tunnel spawn). */
|
|
219
|
+
child: ChildProcess;
|
|
220
|
+
/** Stable id — typically the tunnel's execId so callers cross-ref. */
|
|
221
|
+
id: string;
|
|
222
|
+
workspaceSlug: string;
|
|
223
|
+
/** Display label for the descriptor's `command` field. */
|
|
224
|
+
command: string;
|
|
225
|
+
kind?: SessionKind;
|
|
226
|
+
label?: string;
|
|
227
|
+
}
|
|
228
|
+
interface SpawnAgentInput {
|
|
229
|
+
workspaceSlug: string;
|
|
230
|
+
cwd: string;
|
|
231
|
+
/** Driver-built session ready to receive turns. Caller resolves
|
|
232
|
+
* the adapter, calls createAgentCliRuntime(handle).start({cwd}),
|
|
233
|
+
* and hands the result here. */
|
|
234
|
+
agentSession: AgentSessionLike;
|
|
235
|
+
/** Adapter slug for the descriptor (display only). */
|
|
236
|
+
adapterSlug: string;
|
|
237
|
+
/** Optional initial prompt to dispatch immediately. The promise
|
|
238
|
+
* the registry returns resolves AFTER the spawn — the prompt
|
|
239
|
+
* runs in the background, projecting events into the ring
|
|
240
|
+
* buffer. Skip to spawn idle. */
|
|
241
|
+
initialPrompt?: string;
|
|
242
|
+
label?: string;
|
|
243
|
+
/** Pretty command for the descriptor (display only). */
|
|
244
|
+
commandPreview?: string;
|
|
245
|
+
}
|
|
246
|
+
interface SpawnSessionInput {
|
|
247
|
+
kind: SessionKind;
|
|
248
|
+
workspaceSlug: string;
|
|
249
|
+
/** Path of the workspace root (for the child's cwd). The registry
|
|
250
|
+
* doesn't validate it against the workspaces config — caller has
|
|
251
|
+
* already resolved this. */
|
|
252
|
+
cwd: string;
|
|
253
|
+
/** Argv. First element is the binary; rest are passed verbatim. */
|
|
254
|
+
argv: string[];
|
|
255
|
+
/** Extra env on top of the daemon's process.env. */
|
|
256
|
+
env?: Record<string, string>;
|
|
257
|
+
/** Optional pre-set id (lets the spawner reference the session
|
|
258
|
+
* before the spawn returns — useful for telemetry). */
|
|
259
|
+
id?: string;
|
|
260
|
+
label?: string;
|
|
261
|
+
/** Keep the child's stdin open (default: closed). Most spawned
|
|
262
|
+
* agents are non-interactive and emit a warning when stdin is a
|
|
263
|
+
* pipe with no writer (claude-code prints "no stdin data
|
|
264
|
+
* received in 3s, proceeding without it"). Closing stdin lets
|
|
265
|
+
* them skip the wait. PTY/terminal kinds that genuinely need
|
|
266
|
+
* bidirectional IO should opt in via `keepStdin: true` and the
|
|
267
|
+
* caller is then responsible for writing to `child.stdin`. */
|
|
268
|
+
keepStdin?: boolean;
|
|
269
|
+
}
|
|
270
|
+
interface SpawnPtyInput {
|
|
271
|
+
workspaceSlug: string;
|
|
272
|
+
cwd: string;
|
|
273
|
+
argv: string[];
|
|
274
|
+
/** Initial PTY dimensions. The registry uses min(cols, rows) across
|
|
275
|
+
* active subscribers thereafter — these are the seed values used
|
|
276
|
+
* before anyone attaches. */
|
|
277
|
+
cols: number;
|
|
278
|
+
rows: number;
|
|
279
|
+
/** Extra env on top of process.env. TERM/LANG/PATH are forced by
|
|
280
|
+
* the factory layer (node-pty.spawn options); don't try to clear
|
|
281
|
+
* them here. */
|
|
282
|
+
env?: Record<string, string>;
|
|
283
|
+
/** User-friendly slug. Used by `findByIdOrName(query)`. Must not
|
|
284
|
+
* collide with an existing session's name. */
|
|
285
|
+
name?: string;
|
|
286
|
+
label?: string;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Tiny node:http server that fronts the runtime gateway.
|
|
291
|
+
*
|
|
292
|
+
* Routes:
|
|
293
|
+
* GET /health — { status, workspace, registered, uptime }
|
|
294
|
+
* GET /events — SSE stream of RuntimeEvent
|
|
295
|
+
* POST /mcp — MCP Streamable HTTP transport (POST)
|
|
296
|
+
* GET /mcp — MCP SSE response stream (GET)
|
|
297
|
+
* DELETE /mcp — close MCP session
|
|
298
|
+
* GET /conversations — JSON list of conversation summaries
|
|
299
|
+
* GET /conversations/<id> — markdown body of one conversation
|
|
300
|
+
* POST /heartbeat/tick — force-fire one heartbeat tick
|
|
301
|
+
*
|
|
302
|
+
* No auth in `mode: "none"` (loopback default). `mode: "bearer"`
|
|
303
|
+
* checks `Authorization: Bearer <token>` against the configured
|
|
304
|
+
* token. Health is always public so external monitors can probe.
|
|
305
|
+
*
|
|
306
|
+
* MCP transport: stateless mode for v1 — each request is independent.
|
|
307
|
+
* Stateful session pinning can come later when long-running streaming
|
|
308
|
+
* tool calls become a real use case.
|
|
309
|
+
*/
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Pluggable adapter resolver — keeps the runtime package free of any
|
|
313
|
+
* @agentproto/cli dep. The host (cli `serve`, playground, embedding
|
|
314
|
+
* apps) builds the resolver and hands it in. Returning null means
|
|
315
|
+
* "adapter not found" → 404.
|
|
316
|
+
*/
|
|
317
|
+
type AgentAdapterResolver = (slug: string) => Promise<{
|
|
318
|
+
/** Build a fresh AgentSessionLike for the given cwd. The driver
|
|
319
|
+
* spawns the adapter binary, opens the protocol, and the
|
|
320
|
+
* registry holds the result.
|
|
321
|
+
*
|
|
322
|
+
* When `resumeSessionId` is set, the driver reattaches to that
|
|
323
|
+
* pre-existing adapter session (claude-code's conversation id,
|
|
324
|
+
* hermes' chat handle, …) rather than starting blank. Same
|
|
325
|
+
* mechanism as `agentproto run --resume <id>` — exposed over
|
|
326
|
+
* HTTP so `agentproto sessions restart` works on agent-cli
|
|
327
|
+
* sessions, not just PTY ones. */
|
|
328
|
+
startSession(opts: {
|
|
329
|
+
cwd: string;
|
|
330
|
+
resumeSessionId?: string;
|
|
331
|
+
/** Model identifier forwarded from `start_agent_session`. Adapters
|
|
332
|
+
* that support model selection (e.g. via a `--model` CLI flag) may
|
|
333
|
+
* honour this; others silently ignore it. */
|
|
334
|
+
model?: string;
|
|
335
|
+
}): Promise<AgentSessionLike>;
|
|
336
|
+
/** Display label for the descriptor's `command` field. */
|
|
337
|
+
commandPreview?: string;
|
|
338
|
+
} | null>;
|
|
339
|
+
/**
|
|
340
|
+
* Compact adapter metadata for the discovery endpoints. Independent
|
|
341
|
+
* of the resolver function above — hosts that can list installed
|
|
342
|
+
* adapters wire this; hosts that can only resolve by-slug skip it
|
|
343
|
+
* (the routes 501).
|
|
344
|
+
*/
|
|
345
|
+
interface AdapterListEntry {
|
|
346
|
+
slug: string;
|
|
347
|
+
name: string;
|
|
348
|
+
version: string;
|
|
349
|
+
description: string;
|
|
350
|
+
protocol: string;
|
|
351
|
+
streaming: boolean;
|
|
352
|
+
packageName: string;
|
|
353
|
+
}
|
|
354
|
+
type AgentAdapterLister = () => Promise<AdapterListEntry[]>;
|
|
355
|
+
interface AuthOptions {
|
|
356
|
+
mode: "none" | "bearer";
|
|
357
|
+
token?: string;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* `.agentproto/` config dir — runtime-managed state at the root of
|
|
362
|
+
* every workspace. Mirrors the `.git/` model: user-content stays at
|
|
363
|
+
* the workspace root (HEARTBEAT.md, conversations/, .agents/) while
|
|
364
|
+
* everything the runtime owns lives in `<workspace>/.agentproto/`.
|
|
365
|
+
*
|
|
366
|
+
* What goes here today:
|
|
367
|
+
* - `runtime.json` — boot-time snapshot of the live config (PID,
|
|
368
|
+
* port, resolved workspace, MCP server name, startedAt). Useful
|
|
369
|
+
* for `cat .agentproto/runtime.json` diagnostics + future tooling
|
|
370
|
+
* that wants to discover a running gateway from disk.
|
|
371
|
+
*
|
|
372
|
+
* What does NOT go here:
|
|
373
|
+
* - HEARTBEAT.md — user-edited content
|
|
374
|
+
* - conversations/ — user-readable chat history
|
|
375
|
+
* - .agents/ — user-authored manifests
|
|
376
|
+
*
|
|
377
|
+
* The `.agentproto/` dir is safe to gitignore. The `runtime.json`
|
|
378
|
+
* file contains a per-boot bearer token used to gate mutating HTTP
|
|
379
|
+
* routes (POST /sessions/*, WS /sessions/:id/pty). The token lives
|
|
380
|
+
* in RAM + on disk only — never logged. File mode 0600 below; only
|
|
381
|
+
* processes running as the workspace user (and the daemon itself)
|
|
382
|
+
* can read it. A browser-loaded localhost page CAN'T read this file,
|
|
383
|
+
* which is what defends against the localhost-DNS-rebinding drive-by
|
|
384
|
+
* spawn vector.
|
|
385
|
+
*/
|
|
386
|
+
interface RuntimeMeta {
|
|
387
|
+
/** Absolute path to the workspace root the gateway is bound to. */
|
|
388
|
+
workspace: string;
|
|
389
|
+
/** HTTP listener port. */
|
|
390
|
+
port: number;
|
|
391
|
+
/** HTTP bind address (loopback or `0.0.0.0`). */
|
|
392
|
+
bind: string;
|
|
393
|
+
/** Process id of the running gateway. */
|
|
394
|
+
pid: number;
|
|
395
|
+
/** Wall-clock ISO timestamp at boot. */
|
|
396
|
+
startedAt: string;
|
|
397
|
+
/** MCP server name advertised in the initialize handshake. */
|
|
398
|
+
name: string;
|
|
399
|
+
/** AIP doctype names + extensions registered as MCP CRUD tools. */
|
|
400
|
+
registered: readonly string[];
|
|
401
|
+
/**
|
|
402
|
+
* Per-boot bearer token. Required on mutating routes (POST
|
|
403
|
+
* /sessions/*, POST /sessions/:id/kill, DELETE /sessions/:id, and
|
|
404
|
+
* WS upgrade to /sessions/:id/pty). Random uuid generated at boot
|
|
405
|
+
* by createGateway; regenerated each restart.
|
|
406
|
+
*/
|
|
407
|
+
token: string;
|
|
408
|
+
}
|
|
409
|
+
/**
|
|
410
|
+
* Delete `<workspace>/.agentproto/runtime.json`. Best-effort — called
|
|
411
|
+
* from the daemon's graceful shutdown path so a CLI looking for the
|
|
412
|
+
* live daemon doesn't pick up a stale token after this process exits.
|
|
413
|
+
* Missing file is a no-op (the daemon may never have written one).
|
|
414
|
+
*/
|
|
415
|
+
declare function unlinkRuntimeMeta(workspace: string): Promise<void>;
|
|
416
|
+
/**
|
|
417
|
+
* Read a runtime.json from `<workspace>/.agentproto/`. Returns the
|
|
418
|
+
* parsed object (no validation) plus the on-disk mtime, or null when
|
|
419
|
+
* the file is missing / unreadable / malformed.
|
|
420
|
+
*/
|
|
421
|
+
declare function readRuntimeMeta(workspace: string): Promise<{
|
|
422
|
+
meta: Partial<RuntimeMeta> & Record<string, unknown>;
|
|
423
|
+
mtime: Date;
|
|
424
|
+
} | null>;
|
|
425
|
+
/**
|
|
426
|
+
* For each `workspace` path, check whether its runtime.json points
|
|
427
|
+
* at a now-dead PID; if so, delete it. Returns the list of paths
|
|
428
|
+
* that were cleaned. Called by `agentproto serve` at boot to keep
|
|
429
|
+
* the discovery layer's view of "live daemons" honest — a stale
|
|
430
|
+
* file with the same port as the new daemon is the classic source
|
|
431
|
+
* of `sessions_unauthorized` 401s.
|
|
432
|
+
*
|
|
433
|
+
* Skips the running daemon's own workspace (passed in as
|
|
434
|
+
* `currentWorkspace`) since that file is OURS to write.
|
|
435
|
+
*/
|
|
436
|
+
declare function sweepStaleRuntimeMetas(workspaces: readonly string[], currentWorkspace: string): Promise<string[]>;
|
|
437
|
+
|
|
438
|
+
/**
|
|
439
|
+
* @agentproto/runtime — long-running gateway around an agentproto
|
|
440
|
+
* workspace dir.
|
|
441
|
+
*
|
|
442
|
+
* Composes:
|
|
443
|
+
* - `@agentproto/mcp-server` (CRUD verbs over registered specs)
|
|
444
|
+
* - HTTP transport (Streamable HTTP) on a configurable port
|
|
445
|
+
* - HEARTBEAT.md autonomy loop
|
|
446
|
+
* - Append-only conversation persistence (`conversations/<id>.md`)
|
|
447
|
+
* - Workspace filesystem adapter (compatible with the
|
|
448
|
+
* `McpWorkspace.filesystem` shape used by `@guilde/mcp`)
|
|
449
|
+
*
|
|
450
|
+
* Single entry point: `createGateway(opts)`. Returns a handle with
|
|
451
|
+
* `url` and `stop()` — the rest of the surface lives on the HTTP
|
|
452
|
+
* server.
|
|
453
|
+
*/
|
|
454
|
+
|
|
455
|
+
type AnySpec = DoctypeSpec<any, any>;
|
|
456
|
+
interface CreateGatewayOptions {
|
|
457
|
+
/** Absolute path to the workspace dir. */
|
|
458
|
+
workspace: string;
|
|
459
|
+
/** AIP doctype specs to expose as MCP CRUD verbs. */
|
|
460
|
+
specs: readonly AnySpec[];
|
|
461
|
+
/** Port to bind. Default 18790. */
|
|
462
|
+
port?: number;
|
|
463
|
+
/** Bind host. Default `127.0.0.1` (loopback). Set `0.0.0.0` for LAN. */
|
|
464
|
+
bind?: string;
|
|
465
|
+
/** Auth mode. Default `none` (safe only on loopback). */
|
|
466
|
+
auth?: AuthOptions;
|
|
467
|
+
/**
|
|
468
|
+
* Resolves a heartbeat-runnable agent from its workspace id.
|
|
469
|
+
* Required for HEARTBEAT.md to do anything; without it ticks emit
|
|
470
|
+
* `heartbeat-error` events instead of generating.
|
|
471
|
+
*/
|
|
472
|
+
buildAgent?: BuildHeartbeatAgent;
|
|
473
|
+
/** Server name advertised over MCP. */
|
|
474
|
+
name?: string;
|
|
475
|
+
/** Server version advertised over MCP. */
|
|
476
|
+
version?: string;
|
|
477
|
+
/**
|
|
478
|
+
* Run BOOT.md once at startup. Pass `false` to disable. Defaults to
|
|
479
|
+
* `true`. The boot file is plain markdown — frontmatter-free; the
|
|
480
|
+
* agent named in `defaultBootAgent` (or skipped if unset) gets the
|
|
481
|
+
* body as a single prompt and the reply is appended to a `boot-<iso>`
|
|
482
|
+
* conversation.
|
|
483
|
+
*/
|
|
484
|
+
boot?: boolean;
|
|
485
|
+
/** Agent id used for BOOT.md if no per-file frontmatter. */
|
|
486
|
+
defaultBootAgent?: string;
|
|
487
|
+
/** Optional adapter resolver — when provided, enables
|
|
488
|
+
* `POST /sessions/agent` (long-running agent CLIs like
|
|
489
|
+
* claude-code / hermes via the @agentproto/cli adapter system).
|
|
490
|
+
* Without this, /sessions still works for raw `argv` spawns. */
|
|
491
|
+
resolveAgentAdapter?: AgentAdapterResolver;
|
|
492
|
+
/** Optional adapter lister — when provided, enables
|
|
493
|
+
* `GET /adapters` HTTP route + `list_adapters` MCP tool so UIs
|
|
494
|
+
* can discover what's installed on the host. */
|
|
495
|
+
listAgentAdapters?: AgentAdapterLister;
|
|
496
|
+
/** Optional PTY factory (node-pty wrapper, typically from the cli
|
|
497
|
+
* layer's `loadNodePtyFactory()`). When provided, enables
|
|
498
|
+
* `POST /sessions/terminal`, the `start_terminal_session` MCP
|
|
499
|
+
* tool family, and the `/sessions/:id/pty` WebSocket. Without it,
|
|
500
|
+
* those routes return 501 / the MCP tools aren't registered. */
|
|
501
|
+
spawnPty?: PtyFactory;
|
|
502
|
+
/** Override the per-boot bearer token. Default: `randomUUID()`.
|
|
503
|
+
* Tests can pin a known value; production should always let
|
|
504
|
+
* the gateway generate fresh. The token is written into
|
|
505
|
+
* `<workspace>/.agentproto/runtime.json` (mode 0600) and required
|
|
506
|
+
* on mutating `/sessions/*` routes + the PTY WS upgrade. */
|
|
507
|
+
token?: string;
|
|
508
|
+
/** Trusted browser origins allowed to drive mutating routes
|
|
509
|
+
* without a bearer token. See `RuntimeHttpServerOptions.allowedOrigins`
|
|
510
|
+
* for match semantics. Default: localhost on any port. */
|
|
511
|
+
allowedOrigins?: readonly string[];
|
|
512
|
+
/** When true, drop the localhost-wildcard defaults — only the
|
|
513
|
+
* explicit `allowedOrigins` list is honoured. */
|
|
514
|
+
strictOrigins?: boolean;
|
|
515
|
+
}
|
|
516
|
+
interface GatewayHandle {
|
|
517
|
+
url: string;
|
|
518
|
+
workspace: string;
|
|
519
|
+
workspaceFs: WorkspaceFs;
|
|
520
|
+
registered: readonly string[];
|
|
521
|
+
/** Sessions registry — exposed so MCP tools / external spawners
|
|
522
|
+
* inside the same process can register their child processes for
|
|
523
|
+
* visibility through /sessions and the CLI TUI. */
|
|
524
|
+
sessions: SessionsRegistry;
|
|
525
|
+
/** Per-boot bearer token required on mutating /sessions/* routes
|
|
526
|
+
* + WS PTY upgrades. Exposed so an embedding host (e.g. the CLI
|
|
527
|
+
* shell that hosts the gateway in-process) can pass it to child
|
|
528
|
+
* tools without re-reading the runtime.json file. */
|
|
529
|
+
token: string;
|
|
530
|
+
stop(): Promise<void>;
|
|
531
|
+
}
|
|
532
|
+
/**
|
|
533
|
+
* Spin up the gateway. Order:
|
|
534
|
+
* 1. Build MCP server + load AIP-40 extensions
|
|
535
|
+
* 2. Build conversation store + workspace fs adapter
|
|
536
|
+
* 3. (optional) Run BOOT.md once
|
|
537
|
+
* 4. Start HTTP server
|
|
538
|
+
* 5. Start heartbeat ticker
|
|
539
|
+
*
|
|
540
|
+
* `stop()` reverses 4–5 (heartbeat first, then HTTP). The MCP server
|
|
541
|
+
* is owned by the HTTP server's per-session transports and is closed
|
|
542
|
+
* implicitly when those close.
|
|
543
|
+
*/
|
|
544
|
+
declare function createGateway(opts: CreateGatewayOptions): Promise<GatewayHandle>;
|
|
545
|
+
|
|
546
|
+
export { type AdapterListEntry, type AgentAdapterLister, type AgentAdapterResolver, type AgentSessionLike, type AgentStreamEvent, BuildHeartbeatAgent, type CreateGatewayOptions, type GatewayHandle, type RuntimeMeta, type SessionDescriptor, type SessionKind, type SessionStatus, type SessionsRegistry, type SpawnAgentInput, type SpawnSessionInput, WorkspaceFs, createGateway, readRuntimeMeta, sweepStaleRuntimeMetas, unlinkRuntimeMeta };
|