@agentproto/runtime 4.0.0 → 4.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/README.md +1 -0
- package/dist/app-ui-build.d.ts +96 -0
- package/dist/app-ui-build.mjs +250 -0
- package/dist/app-ui-build.mjs.map +1 -0
- package/dist/app-ui-placeholder.d.ts +73 -0
- package/dist/app-ui-placeholder.mjs +89 -0
- package/dist/app-ui-placeholder.mjs.map +1 -0
- package/dist/config-schema.mjs +93 -83
- package/dist/config-schema.mjs.map +1 -1
- package/dist/config.d.ts +12 -0
- package/dist/config.mjs +93 -83
- package/dist/config.mjs.map +1 -1
- package/dist/index.d.ts +867 -37
- package/dist/index.mjs +2885 -646
- package/dist/index.mjs.map +1 -1
- package/dist/resume-strategies.mjs +7 -0
- package/dist/resume-strategies.mjs.map +1 -1
- package/dist/session-presence.mjs +93 -83
- package/dist/session-presence.mjs.map +1 -1
- package/docs/TRANSMITTER.md +356 -0
- package/docs/mcp-tools/agent_start.md +476 -0
- package/docs/mcp-tools/app_run.md +47 -0
- package/package.json +26 -15
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
# The agentpush ⇄ agentproto transmitter
|
|
2
|
+
|
|
3
|
+
Bidirectional bridge between an imported agentpush MCP server (Telegram,
|
|
4
|
+
etc.) and a live agentproto session: outbound sends can bind a contact to
|
|
5
|
+
the session that sent them, and inbound replies from that contact then
|
|
6
|
+
route straight into the SAME session — as a real user turn — instead of
|
|
7
|
+
spawning a brand-new agent every time.
|
|
8
|
+
|
|
9
|
+
## The model
|
|
10
|
+
|
|
11
|
+
A single `TransmitterBindingStore` (`transmitter-bindings.ts`) maps
|
|
12
|
+
|
|
13
|
+
```
|
|
14
|
+
(alias, source, contactRef) -> { sessionId, mode, provider?, lastSeenTs }
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
- `alias` — the imported MCP alias (agentpush) or bot token alias (telegram).
|
|
18
|
+
- `source` — the channel/phone/chat id the message is scoped to.
|
|
19
|
+
- `contactRef` — the sender's id within that source.
|
|
20
|
+
- `sessionId` — the agentproto session future inbound replies from this
|
|
21
|
+
contact should route into.
|
|
22
|
+
- `mode` — the routing mode this binding was created under (`"route"` or
|
|
23
|
+
`"route-or-spawn"; see below).
|
|
24
|
+
- `provider` — the outbound provider this binding was created through
|
|
25
|
+
(`"agentpush"`, `"telegram"`, etc.). Defaults to `"agentpush"` for
|
|
26
|
+
backward compatibility.
|
|
27
|
+
|
|
28
|
+
One shared store instance is constructed in `index.ts` and injected into
|
|
29
|
+
three places: the inbound poll watcher, the `transmit_message` tool, and
|
|
30
|
+
the `POST /inbound` push route. Bindings persist (debounced JSON write) to
|
|
31
|
+
`~/.agentproto/transmitter-bindings.json`, load-on-construct, and start
|
|
32
|
+
empty on a missing/corrupt file rather than throwing.
|
|
33
|
+
|
|
34
|
+
## Outbound provider abstraction
|
|
35
|
+
|
|
36
|
+
`sendOutbound` in `outbound-adapters.ts` is the provider-agnostic send primitive.
|
|
37
|
+
It mirrors `inbound-adapters.ts` on the read side: a single dispatch function
|
|
38
|
+
that branches by `OutboundProvider` ("agentpush", "telegram", "whatsapp",
|
|
39
|
+
"slack", "generic", "native"). Adding a new outbound dialect means adding a case
|
|
40
|
+
to the switch in `sendOutbound` — the MCP tool and HTTP routes stay unchanged.
|
|
41
|
+
|
|
42
|
+
## Outbound: `transmit_message`
|
|
43
|
+
|
|
44
|
+
MCP tool that sends a message out through a provider-specific outbound channel
|
|
45
|
+
and, by default, binds the recipient to the calling session:
|
|
46
|
+
|
|
47
|
+
```json
|
|
48
|
+
{
|
|
49
|
+
"provider": "agentpush",
|
|
50
|
+
"alias": "agentpush",
|
|
51
|
+
"source": "+33600000000",
|
|
52
|
+
"contact_ref": "alice",
|
|
53
|
+
"text": "your PR is ready for review",
|
|
54
|
+
"sessionId": "sess_abc123",
|
|
55
|
+
"bind": true
|
|
56
|
+
}
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
- `provider` — optional, defaults to `"agentpush"`. Supported values:
|
|
60
|
+
`"agentpush"`, `"telegram"`, `"whatsapp"`, `"slack"`, `"generic"`, `"native"`.
|
|
61
|
+
- `alias` — for `agentpush`, the imported MCP alias (required). For
|
|
62
|
+
`telegram`, the bot-token alias (defaults to `"default"`).
|
|
63
|
+
- `source` — for `agentpush`, the channel/phone. For `telegram`, the chat id.
|
|
64
|
+
- `contact_ref` — recipient id (`agentpush` contact_ref) or chat id (`telegram`).
|
|
65
|
+
- `text` — message text. Optional when `attachments` is provided; at least one
|
|
66
|
+
of `text` or `attachments` is required.
|
|
67
|
+
- `attachments` — optional array of local files to send as media:
|
|
68
|
+
```json
|
|
69
|
+
{
|
|
70
|
+
"type": "photo",
|
|
71
|
+
"path": "/tmp/screenshot.png",
|
|
72
|
+
"caption": "optional caption"
|
|
73
|
+
}
|
|
74
|
+
```
|
|
75
|
+
Supported types: `photo`, `document`, `video`, `audio`. Multiple attachments
|
|
76
|
+
are sent as an album/media group where the provider supports it.
|
|
77
|
+
- `bind` defaults to `true` — on a successful send, upserts a binding
|
|
78
|
+
`(alias, source, contact_ref) -> sessionId` with `mode: "route-or-spawn"` and
|
|
79
|
+
the chosen `provider`. Pass `bind: false` to send without binding.
|
|
80
|
+
- Returns `{ sent: boolean, bound: boolean, message_id?: string }` on success,
|
|
81
|
+
or `{ sent: false, bound: false, error: string, blocked_reason?: string,
|
|
82
|
+
suggestion?: string }` on failure. A send failure (`sent: false`) never
|
|
83
|
+
binds. `message_id` is the provider's own id for the sent message — pass it
|
|
84
|
+
to `check_delivery` to verify the send landed.
|
|
85
|
+
- **agentpush never throws for a send it can't make** — it replies HTTP 200
|
|
86
|
+
with `status: "blocked"` (opt-out, expired session, unapproved template…)
|
|
87
|
+
or `status: "failed"`. `transmit_message` reads that status instead of
|
|
88
|
+
treating MCP transport success as delivery: a `blocked` or `failed` reply
|
|
89
|
+
is surfaced as `sent: false` with `blocked_reason`/`suggestion` (or
|
|
90
|
+
`error`) propagated, never as a false `sent: true`. Telegram gets the same
|
|
91
|
+
treatment for its own backward-compat case — an HTTP 200 body with
|
|
92
|
+
`ok: false` is also reported as `sent: false`, not `sent: true`.
|
|
93
|
+
|
|
94
|
+
### Provider-specific behaviour
|
|
95
|
+
|
|
96
|
+
**agentpush** calls the real `send_message` MCP tool on the imported alias:
|
|
97
|
+
```json
|
|
98
|
+
{
|
|
99
|
+
"to": { "channel": "<source>", "address": "<contact_ref>" },
|
|
100
|
+
"content": { "text": "<text>" }
|
|
101
|
+
}
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
When `attachments` are provided, each file is first uploaded via
|
|
105
|
+
`upload_media` (channel = `<source>`), then referenced by `providerMediaId`
|
|
106
|
+
in `content.media`:
|
|
107
|
+
```json
|
|
108
|
+
{
|
|
109
|
+
"to": { "channel": "<source>", "address": "<contact_ref>" },
|
|
110
|
+
"content": {
|
|
111
|
+
"text": "<text>",
|
|
112
|
+
"media": [
|
|
113
|
+
{ "type": "image", "providerMediaId": "<media_id>", "caption": "..." }
|
|
114
|
+
]
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
**telegram** POSTs directly to the Telegram Bot API. Text-only messages use
|
|
120
|
+
`sendMessage`:
|
|
121
|
+
```
|
|
122
|
+
POST https://api.telegram.org/bot<token>/sendMessage
|
|
123
|
+
{ "chat_id": "<source>", "text": "<text>" }
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
With one attachment, the matching method is used (`sendPhoto`,
|
|
127
|
+
`sendDocument`, `sendVideo`, or `sendAudio`) with a `multipart/form-data`
|
|
128
|
+
body. With several attachments, `sendMediaGroup` is used. The bot token is
|
|
129
|
+
resolved from a `TelegramBotCredsStore` (configured separately via
|
|
130
|
+
`telegram_bot_token_set`).
|
|
131
|
+
|
|
132
|
+
### Telegram bot wiring recipe
|
|
133
|
+
|
|
134
|
+
1. Create the bot with @BotFather and copy the token.
|
|
135
|
+
2. Store the token in the daemon's credential store:
|
|
136
|
+
```json
|
|
137
|
+
{ "tool": "telegram_bot_token_set", "arguments": { "token": "..." } }
|
|
138
|
+
```
|
|
139
|
+
3. Create a provider-agnostic inbound endpoint:
|
|
140
|
+
```json
|
|
141
|
+
{
|
|
142
|
+
"tool": "inbound_endpoint_create",
|
|
143
|
+
"arguments": {
|
|
144
|
+
"slug": "telegram-agentproto",
|
|
145
|
+
"provider": "telegram",
|
|
146
|
+
"alias": "default",
|
|
147
|
+
"mode": "route-or-spawn"
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
```
|
|
151
|
+
The response includes the endpoint `secret`.
|
|
152
|
+
4. Start the narrow public ingress proxy (see below) and get its public URL.
|
|
153
|
+
5. Point Telegram's webhook at the proxy:
|
|
154
|
+
```json
|
|
155
|
+
{
|
|
156
|
+
"tool": "telegram_bot_set_webhook",
|
|
157
|
+
"arguments": {
|
|
158
|
+
"url": "https://<proxy-public-url>/inbound/telegram-agentproto",
|
|
159
|
+
"secret_token": "<endpoint-secret>"
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
## Inbound routing modes
|
|
165
|
+
|
|
166
|
+
Both the poll watcher (`inbound-watcher.ts`, `inbound_watcher_start`'s
|
|
167
|
+
`mode` field) and `POST /inbound` share one decision function,
|
|
168
|
+
`routeInboundMessage` (`inbound-router.ts`):
|
|
169
|
+
|
|
170
|
+
| mode | binding exists, session alive | binding exists, session dead | no binding |
|
|
171
|
+
|---|---|---|---|
|
|
172
|
+
| `spawn` (poll watcher default) | *(ignored)* spawns a fresh agent | *(ignored)* spawns a fresh agent | spawns a fresh agent |
|
|
173
|
+
| `route` | routes into it (`"routed"`) | resurrects it, then routes (`"restarted-routed"`) | skips (`"skipped"`) |
|
|
174
|
+
| `route-or-spawn` (`POST /inbound` default) | routes into it (`"routed"`) | resurrects it, then routes (`"restarted-routed"`) | spawns a fresh agent if a spawn path is configured, else skips |
|
|
175
|
+
|
|
176
|
+
"Resurrects" means: an agent-cli session that's no longer alive is
|
|
177
|
+
restarted in place (same primitive `cron-scheduler.ts`'s `prompt-session`
|
|
178
|
+
job uses — `restartAgentSession(..., { forceAgentResume: true })`), and the
|
|
179
|
+
reply is delivered to the (possibly new) resulting session id. A session
|
|
180
|
+
counts as alive unless it's missing entirely or its `processAlive` field
|
|
181
|
+
is explicitly `false` — a pid-less ACP-native/remote session (no OS
|
|
182
|
+
process to probe) is treated as alive, not dead.
|
|
183
|
+
|
|
184
|
+
Every successful route into a bound session refreshes that binding's
|
|
185
|
+
`lastSeenTs`.
|
|
186
|
+
|
|
187
|
+
## `POST /inbound` — push ingress contract
|
|
188
|
+
|
|
189
|
+
Bearer-gated exactly like the mutating `/sessions/*` routes (read the
|
|
190
|
+
token from `<workspace>/.agentproto/runtime.json`):
|
|
191
|
+
|
|
192
|
+
```
|
|
193
|
+
POST /inbound
|
|
194
|
+
Authorization: Bearer <token>
|
|
195
|
+
Content-Type: application/json
|
|
196
|
+
|
|
197
|
+
{
|
|
198
|
+
"alias": "agentpush",
|
|
199
|
+
"source": "+33600000000",
|
|
200
|
+
"contact_ref": "alice",
|
|
201
|
+
"text": "sounds good, ship it",
|
|
202
|
+
"messages": [ /* optional: raw agentpush events, for the spawn-fallback template */ ],
|
|
203
|
+
"mode": "route-or-spawn" // optional, default "route-or-spawn"
|
|
204
|
+
}
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
Responses:
|
|
208
|
+
|
|
209
|
+
- `200 { "action": "routed" | "spawned" | "restarted-routed" | "skipped", "sessionId"?: string }`
|
|
210
|
+
- `400` — a required field (`alias`/`source`/`contact_ref`/`text`) is
|
|
211
|
+
missing, or `mode` isn't one of the three valid values.
|
|
212
|
+
- `401` — missing/invalid bearer token.
|
|
213
|
+
- `501` — the daemon wasn't wired with a `routeInboundMessage` handler
|
|
214
|
+
(i.e. `POST /inbound` isn't enabled on this host).
|
|
215
|
+
|
|
216
|
+
Note: the daemon's own wiring never sets a `spawnForContact` for this
|
|
217
|
+
route — an unauthenticated webhook payload carries no adapter/prompt
|
|
218
|
+
template to spawn an agent with, so an unbound contact under
|
|
219
|
+
`route-or-spawn` is `"skipped"` rather than spawning an arbitrary agent
|
|
220
|
+
from push input. Only the poll watcher (which already has an
|
|
221
|
+
`adapter`/`promptTemplate` configured via `inbound_watcher_start`) spawns.
|
|
222
|
+
|
|
223
|
+
## Simulating an inbound message
|
|
224
|
+
|
|
225
|
+
### 1. The sim script (no live daemon needed)
|
|
226
|
+
|
|
227
|
+
`packages/runtime/scripts/simulate-inbound.mjs` composes the real wiring
|
|
228
|
+
in-process — real `SessionsRegistry`, real `TransmitterBindingStore`, real
|
|
229
|
+
`routeInboundMessage`, real `startHttpServer` — with one fake session
|
|
230
|
+
double standing in for a real adapter, so you can see the whole path
|
|
231
|
+
(HTTP → routing → the session actually receiving the turn) without an
|
|
232
|
+
agentpush server, an adapter install, or a running daemon:
|
|
233
|
+
|
|
234
|
+
```sh
|
|
235
|
+
pnpm --filter @agentproto/runtime sim:inbound
|
|
236
|
+
# or with overrides:
|
|
237
|
+
pnpm --filter @agentproto/runtime sim:inbound -- --contact=bob --text="ping from bob"
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
Flags (or matching env vars `ALIAS`/`SOURCE`/`CONTACT`/`TEXT`/
|
|
241
|
+
`SESSION_ID`/`PORT`/`TOKEN`): `--alias`, `--source`, `--contact`, `--text`,
|
|
242
|
+
`--session-id`, `--port`, `--token`. It prints the HTTP status, the
|
|
243
|
+
`{action, sessionId}` response, and the exact text the bound session
|
|
244
|
+
received, then exits non-zero if either doesn't match expectations.
|
|
245
|
+
|
|
246
|
+
### 2. `curl` against a live daemon
|
|
247
|
+
|
|
248
|
+
Bind a contact first (via `transmit_message`, or by hand-seeding
|
|
249
|
+
`~/.agentproto/transmitter-bindings.json`), then:
|
|
250
|
+
|
|
251
|
+
```sh
|
|
252
|
+
TOKEN=$(jq -r .token <workspace>/.agentproto/runtime.json)
|
|
253
|
+
|
|
254
|
+
curl -sS -X POST "http://127.0.0.1:<port>/inbound" \
|
|
255
|
+
-H "Authorization: Bearer $TOKEN" \
|
|
256
|
+
-H "Content-Type: application/json" \
|
|
257
|
+
-d '{
|
|
258
|
+
"alias": "agentpush",
|
|
259
|
+
"source": "+33600000000",
|
|
260
|
+
"contact_ref": "alice",
|
|
261
|
+
"text": "sounds good, ship it"
|
|
262
|
+
}'
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
A `200` with `{"action":"routed","sessionId":"..."}` means the reply
|
|
266
|
+
landed as a user turn in that session — check it with `agent_output` /
|
|
267
|
+
`GET /sessions/:id/stream`.
|
|
268
|
+
|
|
269
|
+
## Provider-agnostic inbound endpoints (`POST /inbound/:slug`)
|
|
270
|
+
|
|
271
|
+
As an alternative to the single shared bearer-gated `POST /inbound`, the
|
|
272
|
+
daemon now supports per-provider webhook endpoints:
|
|
273
|
+
|
|
274
|
+
```
|
|
275
|
+
POST /inbound/<slug>
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
Each endpoint is configured with dialect (`agentpush`, `telegram`,
|
|
279
|
+
`whatsapp`, `slack`, `generic`, `native`), an imported MCP alias, an
|
|
280
|
+
optional forced source, an optional webhook signing secret, and a routing
|
|
281
|
+
mode. The MCP tools `inbound_endpoint_create`, `inbound_endpoint_list`,
|
|
282
|
+
and `inbound_endpoint_delete` manage them. All endpoints are created via
|
|
283
|
+
`createInboundEndpointStore()` in `createGateway()` and share the same
|
|
284
|
+
`routeInboundMessage` router as the poll watcher and the legacy push route.
|
|
285
|
+
|
|
286
|
+
### Signature verification
|
|
287
|
+
|
|
288
|
+
When a secret is configured, the secret is REQUIRED and the bearer gate
|
|
289
|
+
is bypassed, so a publicly exposed webhook endpoint is not accidentally
|
|
290
|
+
opened by the loopback Origin allowlist.
|
|
291
|
+
|
|
292
|
+
| provider | header | algorithm/format |
|
|
293
|
+
|---|---|---|
|
|
294
|
+
| `agentpush` | `X-Agentpush-Signature` | `sha256=<hex-hmac-sha256>` |
|
|
295
|
+
| `telegram` | `X-Telegram-Bot-Api-Secret-Token` | exact secret, constant-time compare |
|
|
296
|
+
| `whatsapp` | `X-Hub-Signature-256` | `sha256=<hex-hmac-sha256>` |
|
|
297
|
+
| `slack` | `X-Slack-Signature` | Slack request signature v0 with replay window |
|
|
298
|
+
| `generic` | `X-Agentproto-Signature` | `sha256=<hex-hmac-sha256>` |
|
|
299
|
+
| `native` | n/a | uses the sessions bearer gate |
|
|
300
|
+
|
|
301
|
+
### Normalized envelope
|
|
302
|
+
|
|
303
|
+
After verification, the webhook JSON is normalized by `normalizeInbound()"
|
|
304
|
+
(`inbound-adapters.ts`) into the same `InboundMessage` shape the rest of
|
|
305
|
+
the transmitter expects:
|
|
306
|
+
|
|
307
|
+
```json
|
|
308
|
+
{
|
|
309
|
+
"alias": "<configured alias>",
|
|
310
|
+
"source": "<channel/phone/chat id>",
|
|
311
|
+
"contactRef": "<sender id>",
|
|
312
|
+
"text": "<message text>",
|
|
313
|
+
"messages": [ /* optional: raw provider event */ ]
|
|
314
|
+
}
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
Provider-specific behaviour:
|
|
318
|
+
|
|
319
|
+
- `agentpush` and `slack` support a `challenge` handshake; the challenge is
|
|
320
|
+
echoed as `{ "challenge": "..." }` and no session is touched.
|
|
321
|
+
- `telegram` and `slack` ignore bot/self messages.
|
|
322
|
+
- Missing text returns `200 { "action": "ignored", "reason": "no_text" }` so
|
|
323
|
+
webhook providers do not retry forever.
|
|
324
|
+
|
|
325
|
+
### Deduplication
|
|
326
|
+
|
|
327
|
+
Each endpoint keeps an in-memory FIFO of the last ~500 seen provider
|
|
328
|
+
message ids via `markSeen()`. A duplicate returns `200 { "action":
|
|
329
|
+
"duplicate" }`.
|
|
330
|
+
|
|
331
|
+
### MCP tools
|
|
332
|
+
|
|
333
|
+
```json
|
|
334
|
+
{
|
|
335
|
+
"tool": "inbound_endpoint_create",
|
|
336
|
+
"arguments": {
|
|
337
|
+
"slug": "telegram-bot",
|
|
338
|
+
"provider": "telegram",
|
|
339
|
+
"alias": "agentpush",
|
|
340
|
+
"source": "@mybot",
|
|
341
|
+
"mode": "route-or-spawn",
|
|
342
|
+
"secret": "<optional>"
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
`inbound_endpoint_list` returns endpoints without exposing the secret.
|
|
348
|
+
`inbound_endpoint_delete` removes by slug.
|
|
349
|
+
|
|
350
|
+
## Follow-up: agentpush-side wiring (not covered by this repo)
|
|
351
|
+
|
|
352
|
+
This repo only adds the ingress endpoint. For the push path to actually
|
|
353
|
+
fire in production, agentpush itself needs to be configured to `POST` a
|
|
354
|
+
webhook payload to `<daemon-url>/inbound` (with the daemon's bearer
|
|
355
|
+
token) whenever it receives an inbound message, instead of relying solely
|
|
356
|
+
on `inbound_watcher_start`'s 5-second poll loop.
|