@fastagent-sh/fastagent 0.14.0 → 0.16.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 +16 -6
- package/dist/agent.d.ts +20 -0
- package/dist/agent.js +9 -0
- package/dist/channels/agentcore-limits.d.ts +7 -0
- package/dist/channels/agentcore-limits.js +9 -0
- package/dist/channels/agentcore-state.d.ts +83 -0
- package/dist/channels/agentcore-state.js +258 -0
- package/dist/channels/agentcore.d.ts +98 -0
- package/dist/channels/agentcore.js +260 -0
- package/dist/channels/busy.d.ts +23 -0
- package/dist/channels/busy.js +53 -0
- package/dist/channels/context-buffer.d.ts +35 -0
- package/dist/channels/context-buffer.js +103 -0
- package/dist/channels/control.d.ts +28 -0
- package/dist/channels/control.js +214 -0
- package/dist/channels/feishu/card.js +1 -1
- package/dist/channels/feishu/context-buffer.d.ts +15 -13
- package/dist/channels/feishu/context-buffer.js +62 -69
- package/dist/channels/feishu/feishu-api.d.ts +11 -3
- package/dist/channels/feishu/feishu-api.js +24 -3
- package/dist/channels/feishu/feishu.d.ts +27 -27
- package/dist/channels/feishu/feishu.js +318 -207
- package/dist/channels/feishu/invoke-turn.d.ts +7 -20
- package/dist/channels/feishu/invoke-turn.js +44 -79
- package/dist/channels/feishu/model.d.ts +2 -2
- package/dist/channels/feishu/normalize.d.ts +2 -1
- package/dist/channels/feishu/normalize.js +0 -1
- package/dist/channels/feishu/parse.d.ts +22 -8
- package/dist/channels/feishu/parse.js +24 -7
- package/dist/channels/feishu/preview.d.ts +4 -7
- package/dist/channels/feishu/preview.js +27 -142
- package/dist/channels/feishu/register-app.d.ts +2 -1
- package/dist/channels/feishu/register-webhook.d.ts +2 -1
- package/dist/channels/feishu/scaffold/channel.ts +9 -8
- package/dist/channels/feishu/scaffold/feishu-send.ts +6 -4
- package/dist/channels/feishu/setup-mode.d.ts +37 -0
- package/dist/channels/feishu/setup-mode.js +28 -0
- package/dist/channels/feishu/ws-ingress.d.ts +28 -0
- package/dist/channels/feishu/ws-ingress.js +136 -0
- package/dist/channels/github/github.js +16 -7
- package/dist/channels/http.d.ts +14 -0
- package/dist/channels/http.js +36 -3
- package/dist/channels/invoke-turn-kit.d.ts +65 -0
- package/dist/channels/invoke-turn-kit.js +87 -0
- package/dist/channels/lark/lark.d.ts +4 -2
- package/dist/channels/lark/lark.js +4 -1
- package/dist/channels/lark/onboard.d.ts +8 -4
- package/dist/channels/lark/onboard.js +8 -0
- package/dist/channels/lark/scaffold/channel.ts +9 -8
- package/dist/channels/lark/scaffold/lark-send.ts +6 -4
- package/dist/channels/preview-kit.d.ts +115 -0
- package/dist/channels/preview-kit.js +184 -0
- package/dist/channels/seen.d.ts +5 -0
- package/dist/channels/seen.js +35 -0
- package/dist/channels/slack/bot-auth.d.ts +15 -0
- package/dist/channels/slack/bot-auth.js +146 -0
- package/dist/channels/slack/config-api.d.ts +60 -0
- package/dist/channels/slack/config-api.js +149 -0
- package/dist/channels/slack/context-buffer.d.ts +24 -0
- package/dist/channels/slack/context-buffer.js +37 -0
- package/dist/channels/slack/invoke-turn.d.ts +19 -0
- package/dist/channels/slack/invoke-turn.js +63 -0
- package/dist/channels/slack/manifest.d.ts +49 -0
- package/dist/channels/slack/manifest.js +69 -0
- package/dist/channels/slack/model.d.ts +67 -0
- package/dist/channels/slack/model.js +2 -0
- package/dist/channels/slack/onboard.d.ts +41 -0
- package/dist/channels/slack/onboard.js +120 -0
- package/dist/channels/slack/onboarding-state.d.ts +31 -0
- package/dist/channels/slack/onboarding-state.js +69 -0
- package/dist/channels/slack/parse.d.ts +38 -0
- package/dist/channels/slack/parse.js +124 -0
- package/dist/channels/slack/preview.d.ts +23 -0
- package/dist/channels/slack/preview.js +403 -0
- package/dist/channels/slack/reaction.d.ts +24 -0
- package/dist/channels/slack/reaction.js +62 -0
- package/dist/channels/slack/register-webhook.d.ts +10 -0
- package/dist/channels/slack/register-webhook.js +49 -0
- package/dist/channels/slack/scaffold/channel.ts +33 -0
- package/dist/channels/slack/scaffold/slack-send.ts +171 -0
- package/dist/channels/slack/setup-server.d.ts +17 -0
- package/dist/channels/slack/setup-server.js +103 -0
- package/dist/channels/slack/slack-api.d.ts +57 -0
- package/dist/channels/slack/slack-api.js +399 -0
- package/dist/channels/slack/slack.d.ts +51 -0
- package/dist/channels/slack/slack.js +496 -0
- package/dist/channels/slack/welcomed.d.ts +5 -0
- package/dist/channels/slack/welcomed.js +32 -0
- package/dist/channels/state.d.ts +11 -4
- package/dist/channels/state.js +22 -15
- package/dist/channels/stop-command.d.ts +6 -0
- package/dist/channels/stop-command.js +36 -0
- package/dist/channels/tasks.d.ts +7 -0
- package/dist/channels/tasks.js +25 -0
- package/dist/channels/telegram/context-buffer.d.ts +8 -17
- package/dist/channels/telegram/context-buffer.js +6 -85
- package/dist/channels/telegram/invoke-turn.d.ts +5 -22
- package/dist/channels/telegram/invoke-turn.js +11 -58
- package/dist/channels/telegram/parse.d.ts +0 -7
- package/dist/channels/telegram/parse.js +4 -2
- package/dist/channels/telegram/preview.d.ts +4 -7
- package/dist/channels/telegram/preview.js +24 -142
- package/dist/channels/telegram/scaffold/telegram-send.ts +6 -3
- package/dist/channels/telegram/telegram.js +24 -10
- package/dist/channels/{feishu/text.d.ts → text.d.ts} +15 -1
- package/dist/channels/{feishu/text.js → text.js} +15 -1
- package/dist/channels/thread-participants.d.ts +21 -0
- package/dist/channels/thread-participants.js +132 -0
- package/dist/channels/turn-queue.js +8 -1
- package/dist/channels/turn-store.d.ts +1 -1
- package/dist/channels/turn-store.js +2 -3
- package/dist/cli/add-feishu.d.ts +30 -0
- package/dist/cli/add-feishu.js +366 -0
- package/dist/cli/add-slack.d.ts +11 -0
- package/dist/cli/add-slack.js +199 -0
- package/dist/{cli-auth.d.ts → cli/auth-view.d.ts} +1 -1
- package/dist/{cli-auth.js → cli/auth-view.js} +1 -1
- package/dist/cli/commands/add.d.ts +4 -0
- package/dist/cli/commands/add.js +191 -60
- package/dist/cli/commands/attach.d.ts +82 -0
- package/dist/cli/commands/attach.js +563 -0
- package/dist/cli/commands/chat.d.ts +1 -0
- package/dist/cli/commands/chat.js +12 -7
- package/dist/cli/commands/deploy.d.ts +3 -1
- package/dist/cli/commands/deploy.js +384 -82
- package/dist/cli/commands/dev.js +34 -26
- package/dist/cli/commands/fire.js +17 -18
- package/dist/cli/commands/info.js +36 -29
- package/dist/cli/commands/init.d.ts +1 -1
- package/dist/cli/commands/init.js +65 -53
- package/dist/cli/commands/invoke.js +11 -9
- package/dist/cli/commands/login.js +48 -45
- package/dist/cli/commands/models.js +1 -1
- package/dist/cli/commands/schedule.js +6 -8
- package/dist/cli/commands/start.js +102 -39
- package/dist/cli/commands/tool.js +23 -18
- package/dist/cli/fail.d.ts +17 -2
- package/dist/cli/fail.js +25 -1
- package/dist/{invoke-stream.d.ts → cli/invoke-stream.d.ts} +1 -1
- package/dist/{invoke-stream.js → cli/invoke-stream.js} +4 -0
- package/dist/cli/kernel.d.ts +3 -2
- package/dist/cli/models-view.d.ts +21 -0
- package/dist/cli/models-view.js +66 -0
- package/dist/cli/program.js +135 -56
- package/dist/cli/serve.d.ts +60 -20
- package/dist/cli/serve.js +252 -47
- package/dist/cli/shared.d.ts +40 -7
- package/dist/cli/shared.js +180 -38
- package/dist/collect.d.ts +14 -3
- package/dist/collect.js +24 -0
- package/dist/core.d.ts +3 -1
- package/dist/core.js +2 -0
- package/dist/deploy/agentcore/plan.d.ts +117 -0
- package/dist/deploy/agentcore/plan.js +721 -0
- package/dist/deploy/agentcore/run.d.ts +73 -0
- package/dist/deploy/agentcore/run.js +412 -0
- package/dist/deploy/agentcore/zip.d.ts +17 -0
- package/dist/deploy/agentcore/zip.js +68 -0
- package/dist/deploy/container.d.ts +26 -25
- package/dist/deploy/container.js +93 -89
- package/dist/deploy/docker/plan.d.ts +3 -3
- package/dist/deploy/docker/plan.js +20 -23
- package/dist/deploy/fly/plan.d.ts +5 -1
- package/dist/deploy/fly/plan.js +39 -26
- package/dist/deploy/fly/run.d.ts +17 -5
- package/dist/deploy/fly/run.js +52 -7
- package/dist/deploy/preflight.d.ts +20 -8
- package/dist/deploy/preflight.js +267 -71
- package/dist/deploy/railway/plan.d.ts +10 -1
- package/dist/deploy/railway/plan.js +53 -20
- package/dist/deploy/railway/run.d.ts +13 -5
- package/dist/deploy/railway/run.js +22 -7
- package/dist/deploy/runner.d.ts +7 -3
- package/dist/deploy/runner.js +9 -3
- package/dist/deploy/secrets.d.ts +2 -1
- package/dist/deploy/secrets.js +23 -3
- package/dist/dev-supervisor.d.ts +11 -10
- package/dist/dev-supervisor.js +56 -50
- package/dist/engines/pi/auth.d.ts +8 -7
- package/dist/engines/pi/auth.js +12 -10
- package/dist/engines/pi/channel.d.ts +22 -16
- package/dist/engines/pi/channel.js +92 -62
- package/dist/engines/pi/chat.d.ts +4 -16
- package/dist/engines/pi/chat.js +8 -261
- package/dist/engines/pi/config.d.ts +20 -55
- package/dist/engines/pi/config.js +49 -121
- package/dist/engines/pi/create.d.ts +30 -10
- package/dist/engines/pi/create.js +33 -26
- package/dist/engines/pi/definition.d.ts +7 -26
- package/dist/engines/pi/definition.js +8 -54
- package/dist/engines/pi/harness.d.ts +49 -0
- package/dist/engines/pi/harness.js +90 -2
- package/dist/engines/pi/invoke.d.ts +55 -2
- package/dist/engines/pi/invoke.js +304 -19
- package/dist/engines/pi/login.d.ts +12 -1
- package/dist/engines/pi/login.js +16 -4
- package/dist/engines/pi/models.d.ts +59 -13
- package/dist/engines/pi/models.js +62 -24
- package/dist/engines/pi/open.d.ts +114 -0
- package/dist/engines/pi/open.js +127 -0
- package/dist/engines/pi/session-builder.d.ts +16 -0
- package/dist/engines/pi/session-builder.js +308 -0
- package/dist/engines/pi/session-control.d.ts +50 -0
- package/dist/engines/pi/session-control.js +604 -0
- package/dist/engines/pi/sessions.d.ts +17 -2
- package/dist/engines/pi/sessions.js +9 -0
- package/dist/engines/pi/tool-context.d.ts +18 -11
- package/dist/engines/pi/tool-context.js +3 -9
- package/dist/engines/pi/tool.d.ts +6 -8
- package/dist/engines/pi/tool.js +10 -1
- package/dist/engines/pi/wake-tool.d.ts +0 -3
- package/dist/engines/pi/wake-tool.js +9 -7
- package/dist/env.d.ts +16 -4
- package/dist/env.js +43 -5
- package/dist/feishu.d.ts +1 -1
- package/dist/feishu.js +1 -1
- package/dist/host/node.d.ts +25 -8
- package/dist/host/node.js +5 -4
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/lark.d.ts +1 -1
- package/dist/lark.js +1 -1
- package/dist/loader.d.ts +2 -2
- package/dist/loader.js +3 -3
- package/dist/log.d.ts +1 -1
- package/dist/log.js +1 -1
- package/dist/observe.js +3 -0
- package/dist/paths.d.ts +138 -0
- package/dist/paths.js +326 -0
- package/dist/pi.d.ts +6 -5
- package/dist/pi.js +4 -3
- package/dist/runtime.d.ts +7 -5
- package/dist/runtime.js +2 -2
- package/dist/scaffold/add-channel.d.ts +26 -9
- package/dist/scaffold/add-channel.js +175 -40
- package/dist/scaffold/init.d.ts +32 -41
- package/dist/scaffold/init.js +161 -185
- package/dist/scaffold/templates/env.example +15 -6
- package/dist/scaffold/templates/fastagent.config.mjs +5 -4
- package/dist/scaffold/templates/gitignore +14 -6
- package/dist/scaffold/templates/persona.md +4 -2
- package/dist/scaffold/templates/secrets.gitignore +5 -0
- package/dist/scaffold/templates.d.ts +1 -7
- package/dist/scaffold/templates.js +3 -25
- package/dist/scaffold/vendor-skill.d.ts +2 -2
- package/dist/scaffold/vendor-skill.js +13 -13
- package/dist/schedule/discover.js +4 -4
- package/dist/schedule/scheduler.d.ts +40 -1
- package/dist/schedule/scheduler.js +89 -56
- package/dist/schedule/state.js +1 -1
- package/dist/schedule/wake-alarm.d.ts +47 -0
- package/dist/schedule/wake-alarm.js +136 -0
- package/dist/schedule/wakeups.d.ts +1 -3
- package/dist/schedule/wakeups.js +19 -1
- package/dist/session-remote.d.ts +53 -0
- package/dist/session-remote.js +336 -0
- package/dist/session.d.ts +265 -0
- package/dist/session.js +37 -0
- package/dist/slack.d.ts +2 -0
- package/dist/slack.js +2 -0
- package/dist/tunnel.d.ts +7 -4
- package/dist/tunnel.js +23 -12
- package/package.json +22 -7
- package/dist/channels/feishu/owned-threads.d.ts +0 -11
- package/dist/channels/feishu/owned-threads.js +0 -47
- package/dist/channels/feishu/seen.d.ts +0 -5
- package/dist/channels/feishu/seen.js +0 -47
- package/dist/cli-add-feishu.d.ts +0 -8
- package/dist/cli-add-feishu.js +0 -223
- package/dist/cli-models.d.ts +0 -11
- package/dist/cli-models.js +0 -20
- package/dist/engines/pi/workspace.d.ts +0 -56
- package/dist/engines/pi/workspace.js +0 -72
- package/dist/scaffold/templates/gitignore.agentdir-root +0 -5
- package/dist/scaffold/templates/gitignore.kit +0 -2
- package/dist/workspace.d.ts +0 -9
- package/dist/workspace.js +0 -45
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AWS AgentCore Runtime adapter: serve fastagent's whole HTTP surface through the Runtime's service
|
|
3
|
+
* contract. AgentCore gives a container exactly TWO paths — `POST /invocations` (the only ingress,
|
|
4
|
+
* reached via the SigV4 `InvokeAgentRuntime` API) and `GET /ping` (health) — and no public URL, so
|
|
5
|
+
* the deployment fronts webhooks with a thin forwarder Lambda and delivers cron slots from
|
|
6
|
+
* EventBridge Scheduler; both arrive here as an ENVELOPE in the /invocations payload:
|
|
7
|
+
*
|
|
8
|
+
* - `{ kind: "webhook", method, path, headers?, bodyB64? }` — a verbatim webhook request captured
|
|
9
|
+
* by the forwarder. Reconstructed into a real `Request` and dispatched to the SAME channel routes
|
|
10
|
+
* a direct deployment serves — signature verification (Telegram secret token, Feishu signatures)
|
|
11
|
+
* runs unchanged inside the channel. The channel's HTTP response travels back INSIDE the
|
|
12
|
+
* transport reply (`{ status, headers, bodyB64 }`, transport always 200): AgentCore folds a
|
|
13
|
+
* container non-2xx into its own 424 RuntimeClientError, so riding the real status inside the
|
|
14
|
+
* envelope is the only way the forwarder can re-emit it verbatim (a Feishu URL-verification
|
|
15
|
+
* challenge needs the exact body + content-type back).
|
|
16
|
+
* - `{ kind: "schedule-fire", name, slot }` — one cron instant from the external clock. Dispatched
|
|
17
|
+
* to {@link fireScheduleOnce}-shaped `fire` with the slot as the idempotency key (EventBridge
|
|
18
|
+
* delivery is at-least-once; a duplicate slot must not double-fire).
|
|
19
|
+
* - `{ kind: "invoke", session, text }` — the programmatic data plane; streams the invoke back as
|
|
20
|
+
* SSE (AgentCore's streaming response form), reusing the HTTP channel's handler wholesale.
|
|
21
|
+
*
|
|
22
|
+
* `/ping` reports `HealthyBusy` while process-wide background work is in flight (busy.ts) — webhook
|
|
23
|
+
* channels ACK fast and run turns fire-and-forget, and AgentCore ends an idle session, so without
|
|
24
|
+
* this signal a long turn would be killed mid-flight right after its ACK. `Healthy` when idle lets
|
|
25
|
+
* the platform reclaim the microVM (that idle-to-zero IS the point of this deployment).
|
|
26
|
+
*/
|
|
27
|
+
import { Buffer } from "node:buffer";
|
|
28
|
+
import { timingSafeEqual } from "node:crypto";
|
|
29
|
+
import { beginWork, onIdle } from "./busy.js";
|
|
30
|
+
import { router } from "../host/node.js";
|
|
31
|
+
import { log } from "../log.js";
|
|
32
|
+
import { rememberWakeAlarmUrl } from "../schedule/wake-alarm.js";
|
|
33
|
+
import { readBodyCapped } from "./body.js";
|
|
34
|
+
import { createInvokeHandler } from "./http.js";
|
|
35
|
+
import { text } from "./respond.js";
|
|
36
|
+
import { MAX_ENVELOPE_BYTES, MAX_WEBHOOK_BODY_BYTES } from "./agentcore-limits.js";
|
|
37
|
+
const unsnapshottedWarning = "[agentcore] this envelope carried no state-snapshot URLs — the state root is LOCAL ONLY and the " +
|
|
38
|
+
"platform erases it on the next deploy (redeploy with a current fastagent to restore durability)";
|
|
39
|
+
const jsonHeaders = { "content-type": "application/json" };
|
|
40
|
+
const json = (body, status) => new Response(`${JSON.stringify(body)}\n`, { status, headers: jsonHeaders });
|
|
41
|
+
/** Compare an untrusted envelope secret without leaking a matching-prefix timing signal. */
|
|
42
|
+
function secretMatches(actual, expected) {
|
|
43
|
+
if (typeof actual !== "string" || expected === undefined)
|
|
44
|
+
return false;
|
|
45
|
+
const actualBytes = Buffer.from(actual);
|
|
46
|
+
const expectedBytes = Buffer.from(expected);
|
|
47
|
+
return actualBytes.length === expectedBytes.length && timingSafeEqual(actualBytes, expectedBytes);
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Build the AgentCore serving surface: `{ "POST /invocations", "GET /ping" }`. The caller merges it
|
|
51
|
+
* over its routes (collision-checked at the mount site, serve.ts) — the inner routes stay mounted
|
|
52
|
+
* too, which is harmless (AgentCore routes only /invocations and /ping into the container) and keeps
|
|
53
|
+
* a local `curl` debug surface.
|
|
54
|
+
*/
|
|
55
|
+
export function agentcoreRoutes(options) {
|
|
56
|
+
const { routes, agent, stateRoot, isBusy, fire, stateSync, ingressSecret, onStateReady } = options;
|
|
57
|
+
const dispatch = router(routes);
|
|
58
|
+
const invokeHandler = createInvokeHandler(agent);
|
|
59
|
+
// Snapshot on the 0-in-flight edge: webhook channels ACK fast and finish the turn in the
|
|
60
|
+
// background, so "the request returned" is NOT when the state root settles.
|
|
61
|
+
if (stateSync)
|
|
62
|
+
onIdle(() => stateSync.save());
|
|
63
|
+
let warnedUnsnapshotted = false;
|
|
64
|
+
let stateReadyFired = false;
|
|
65
|
+
const handleInvocation = async (req) => {
|
|
66
|
+
const body = await readBodyCapped(req, MAX_ENVELOPE_BYTES);
|
|
67
|
+
if ("tooLarge" in body)
|
|
68
|
+
return text("envelope too large\n", 413);
|
|
69
|
+
let envelope;
|
|
70
|
+
try {
|
|
71
|
+
envelope = JSON.parse(body.text);
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
return text("invalid json\n", 400);
|
|
75
|
+
}
|
|
76
|
+
if (envelope === null || typeof envelope !== "object" || typeof envelope.kind !== "string") {
|
|
77
|
+
return text('need { "kind": "webhook" | "schedule-fire" | "invoke" | "wake-poke" | "checkpoint", ... }\n', 400);
|
|
78
|
+
}
|
|
79
|
+
// AUTHENTICATION BOUNDARY. `InvokeAgentRuntime` is an ordinary IAM action, so "reached this
|
|
80
|
+
// handler" proves nothing about the sender. Only an envelope carrying the shared secret is the
|
|
81
|
+
// forwarder's; anything else is the PUBLIC data plane, which may run exactly one kind (`invoke`)
|
|
82
|
+
// and may NOT carry internal fields — riding a `state` or `wake` URL on a public invoke would
|
|
83
|
+
// redirect the state snapshot (auth.json) or the alarm callback (the wake secret) to the caller.
|
|
84
|
+
// Internal fields are DROPPED rather than rejected: a public caller has no business knowing them.
|
|
85
|
+
const trusted = secretMatches(envelope.auth, ingressSecret);
|
|
86
|
+
if (!trusted) {
|
|
87
|
+
if (envelope.kind !== "invoke") {
|
|
88
|
+
log.warn(`[agentcore] rejected an unauthenticated "${envelope.kind}" envelope`);
|
|
89
|
+
return text("forbidden\n", 403);
|
|
90
|
+
}
|
|
91
|
+
envelope.wake = undefined;
|
|
92
|
+
envelope.state = undefined;
|
|
93
|
+
}
|
|
94
|
+
// The forwarder rides its public URL along on every envelope — persist it (write-if-changed) so
|
|
95
|
+
// the wake-alarm sink can call back. Written AFTER the restore below: a stale snapshot copy must
|
|
96
|
+
// not win over the URL this deployment is actually reachable at. A bad persist must not fail the turn.
|
|
97
|
+
const rememberUrl = () => {
|
|
98
|
+
if (typeof envelope.wake?.url !== "string")
|
|
99
|
+
return;
|
|
100
|
+
try {
|
|
101
|
+
rememberWakeAlarmUrl(stateRoot, envelope.wake.url);
|
|
102
|
+
}
|
|
103
|
+
catch (e) {
|
|
104
|
+
log.error(`[agentcore] could not persist the wake-alarm URL: ${String(e)}`);
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
// Cross-deploy state: the platform wipes /mnt/state on every version update, so the durable copy
|
|
108
|
+
// must be pulled back BEFORE anything reads it. A failed restore fails the request — serving an
|
|
109
|
+
// empty agent (and then snapshotting that emptiness over the good copy) is the worse outcome.
|
|
110
|
+
if (stateSync) {
|
|
111
|
+
if (envelope.state && typeof envelope.state.getUrl === "string" && typeof envelope.state.putUrl === "string") {
|
|
112
|
+
stateSync.use(envelope.state);
|
|
113
|
+
}
|
|
114
|
+
else if (envelope.kind !== "invoke" && !stateSync.configured() && !warnedUnsnapshotted) {
|
|
115
|
+
// webhook/schedule-fire/wake-poke reach us ONLY through the forwarder, which always mints the
|
|
116
|
+
// pair. Missing = a broken/stale topology whose state dies at the next deploy: say so, loudly,
|
|
117
|
+
// once per process (a direct `invoke` legitimately has none — its session storage is its own).
|
|
118
|
+
warnedUnsnapshotted = true;
|
|
119
|
+
log.warn(unsnapshottedWarning);
|
|
120
|
+
}
|
|
121
|
+
try {
|
|
122
|
+
await stateSync.ready();
|
|
123
|
+
}
|
|
124
|
+
catch (e) {
|
|
125
|
+
log.error(`[agentcore] state restore failed: ${String(e)}`);
|
|
126
|
+
return text(`state restore failed: ${String(e)}\n`, 503);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
rememberUrl();
|
|
130
|
+
// The state root is authoritative only now — anything that must READ it at startup (the wake-alarm
|
|
131
|
+
// reconcile) runs here, once, rather than at boot against a mount the platform just wiped.
|
|
132
|
+
if (onStateReady && !stateReadyFired) {
|
|
133
|
+
stateReadyFired = true;
|
|
134
|
+
onStateReady();
|
|
135
|
+
}
|
|
136
|
+
switch (envelope.kind) {
|
|
137
|
+
case "webhook": {
|
|
138
|
+
const { method, path, query, headers, bodyB64 } = envelope;
|
|
139
|
+
if (typeof method !== "string" || typeof path !== "string" || !path.startsWith("/")) {
|
|
140
|
+
return text('webhook envelope needs { "method": string, "path": "/..." }\n', 400);
|
|
141
|
+
}
|
|
142
|
+
if (typeof bodyB64 === "string" && Buffer.byteLength(bodyB64, "base64") > MAX_WEBHOOK_BODY_BYTES) {
|
|
143
|
+
return json({
|
|
144
|
+
status: 413,
|
|
145
|
+
headers: { "content-type": "text/plain" },
|
|
146
|
+
bodyB64: Buffer.from("payload too large\n").toString("base64"),
|
|
147
|
+
}, 200);
|
|
148
|
+
}
|
|
149
|
+
const inner = new Request(`http://agentcore.local${path}${typeof query === "string" && query !== "" ? `?${query}` : ""}`, {
|
|
150
|
+
method,
|
|
151
|
+
headers: headers ?? {},
|
|
152
|
+
body: typeof bodyB64 === "string" && method !== "GET" && method !== "HEAD"
|
|
153
|
+
? Buffer.from(bodyB64, "base64")
|
|
154
|
+
: undefined,
|
|
155
|
+
});
|
|
156
|
+
const response = await dispatch(inner);
|
|
157
|
+
// Buffer the channel's ACK (webhook ACKs are small by design — the turn itself runs
|
|
158
|
+
// fire-and-forget) and ride it inside the transport reply, byte-exact.
|
|
159
|
+
const replyBody = Buffer.from(await response.arrayBuffer());
|
|
160
|
+
const replyHeaders = {};
|
|
161
|
+
response.headers.forEach((value, key) => {
|
|
162
|
+
replyHeaders[key] = value;
|
|
163
|
+
});
|
|
164
|
+
const reply = {
|
|
165
|
+
status: response.status,
|
|
166
|
+
headers: replyHeaders,
|
|
167
|
+
bodyB64: replyBody.toString("base64"),
|
|
168
|
+
};
|
|
169
|
+
return json(reply, 200);
|
|
170
|
+
}
|
|
171
|
+
case "schedule-fire": {
|
|
172
|
+
const { name, slot } = envelope;
|
|
173
|
+
if (typeof name !== "string" || typeof slot !== "string" || Number.isNaN(Date.parse(slot))) {
|
|
174
|
+
return text('schedule-fire envelope needs { "name": string, "slot": ISO-date }\n', 400);
|
|
175
|
+
}
|
|
176
|
+
// No fire capability (no schedules in this definition) or an unknown name is deploy drift —
|
|
177
|
+
// an external clock rule outliving the schedule it fired for. 404 keeps it VISIBLE in the
|
|
178
|
+
// clock's logs (a 200 would silently absorb every future fire).
|
|
179
|
+
if (!fire)
|
|
180
|
+
return text(`no schedules in this deployment (schedule-fire "${name}")\n`, 404);
|
|
181
|
+
// The whole agent turn runs inside this request — but the CALLER (the forwarder Lambda) may
|
|
182
|
+
// time out and drop the connection while the turn keeps running server-side. Count it as
|
|
183
|
+
// in-flight work so /ping holds the session (HealthyBusy) for the remainder.
|
|
184
|
+
const workDone = beginWork();
|
|
185
|
+
try {
|
|
186
|
+
const outcome = await fire(name, new Date(slot));
|
|
187
|
+
return json(outcome, 200);
|
|
188
|
+
}
|
|
189
|
+
catch (e) {
|
|
190
|
+
if (e instanceof UnknownScheduleError)
|
|
191
|
+
return text(`${e.message}\n`, 404);
|
|
192
|
+
// A claim-state fault (unreadable/unwritable fires.json) — surface it as the request's
|
|
193
|
+
// failure so the external clock's logs carry it (fail visibly, never a silent absorb).
|
|
194
|
+
log.error(`[agentcore] schedule-fire ${name} failed: ${String(e)}`);
|
|
195
|
+
return text(`schedule-fire failed: ${String(e)}\n`, 500);
|
|
196
|
+
}
|
|
197
|
+
finally {
|
|
198
|
+
workDone();
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
case "checkpoint": {
|
|
202
|
+
// Deliberately does NOT wait for idle: the durable turn intent is persisted BEFORE the
|
|
203
|
+
// webhook ACK (turn-store.ts), so a flush right now already carries the interrupted turn —
|
|
204
|
+
// and blocking a deploy on a turn that may run for minutes would be the worse trade.
|
|
205
|
+
if (!stateSync)
|
|
206
|
+
return json({ written: false, reason: "no state sync in this deployment" }, 200);
|
|
207
|
+
try {
|
|
208
|
+
// The reply is the ONLY thing telling the operator whether an in-flight turn was
|
|
209
|
+
// protected, so it reports what actually happened — `written: false` (nothing to write)
|
|
210
|
+
// reads differently from `written: true`, and a failure is a failure.
|
|
211
|
+
return json(await stateSync.checkpoint(), 200);
|
|
212
|
+
}
|
|
213
|
+
catch (e) {
|
|
214
|
+
log.error(`[agentcore] checkpoint failed: ${String(e)}`);
|
|
215
|
+
return text(`checkpoint failed: ${String(e)}\n`, 500);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
case "wake-poke": {
|
|
219
|
+
// The poke's job is DONE by arriving: the invocation woke (or kept awake) the container, and
|
|
220
|
+
// the wake pump (boot drain + 30s poll) fires whatever is due. Nothing to dispatch.
|
|
221
|
+
return json({ ok: true }, 200);
|
|
222
|
+
}
|
|
223
|
+
case "invoke": {
|
|
224
|
+
// Reuse the HTTP channel's handler wholesale (SSE, cancellation, backpressure) by handing it
|
|
225
|
+
// the shape it already validates — one protocol, one implementation.
|
|
226
|
+
const inner = new Request("http://agentcore.local/invoke", {
|
|
227
|
+
method: "POST",
|
|
228
|
+
headers: jsonHeaders,
|
|
229
|
+
body: JSON.stringify({ session: envelope.session, text: envelope.text }),
|
|
230
|
+
});
|
|
231
|
+
return invokeHandler(inner);
|
|
232
|
+
}
|
|
233
|
+
default:
|
|
234
|
+
return text(`unknown envelope kind "${envelope.kind}"\n`, 400);
|
|
235
|
+
}
|
|
236
|
+
};
|
|
237
|
+
// Settle-then-snapshot: when the envelope leaves nothing in flight, its writes are final now (a
|
|
238
|
+
// background turn instead reports through the idle edge above).
|
|
239
|
+
const invocations = async (req) => {
|
|
240
|
+
const response = await handleInvocation(req);
|
|
241
|
+
if (stateSync && !isBusy())
|
|
242
|
+
stateSync.save();
|
|
243
|
+
return response;
|
|
244
|
+
};
|
|
245
|
+
return {
|
|
246
|
+
"POST /invocations": invocations,
|
|
247
|
+
// The Runtime ping contract: Healthy = reclaimable, HealthyBusy = keep the session alive
|
|
248
|
+
// (background turns in flight). No time_of_last_update — the platform tracks status changes
|
|
249
|
+
// itself, and a timestamp advancing every ping would defeat the idle timeout (their docs warn).
|
|
250
|
+
"GET /ping": () => json({ status: isBusy() ? "HealthyBusy" : "Healthy" }, 200),
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
/** Thrown by the mount-site `fire` binding when the envelope names a schedule this workspace does
|
|
254
|
+
* not have — the adapter maps it to 404 (deploy drift stays visible in the external clock's logs). */
|
|
255
|
+
export class UnknownScheduleError extends Error {
|
|
256
|
+
constructor(name) {
|
|
257
|
+
super(`unknown schedule "${name}"`);
|
|
258
|
+
this.name = "UnknownScheduleError";
|
|
259
|
+
}
|
|
260
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SHARED process-wide in-flight work signal. Channels ACK a webhook fast and run the turn
|
|
3
|
+
* fire-and-forget on this process's event loop (host/node.ts) — so "is this process busy?" is not
|
|
4
|
+
* derivable from open HTTP requests. The two shared execution primitives (turn-queue chains,
|
|
5
|
+
* task-tracker side tasks) report here; a serving surface that must stay alive while background
|
|
6
|
+
* work runs (the AgentCore adapter's /ping → HealthyBusy) reads it.
|
|
7
|
+
*
|
|
8
|
+
* Deliberately a counter, not a registry: consumers need only "any work in flight?"; keeping the
|
|
9
|
+
* module dependency-free lets both channel primitives import it without cycles.
|
|
10
|
+
*/
|
|
11
|
+
/**
|
|
12
|
+
* Mark one unit of background work as started. Returns its completion callback — idempotent, so a
|
|
13
|
+
* caller may safely settle it from multiple cleanup paths (finally + catch) without double-counting.
|
|
14
|
+
*/
|
|
15
|
+
export declare function beginWork(): () => void;
|
|
16
|
+
/**
|
|
17
|
+
* Subscribe to the 0-in-flight edge: the moment the process finishes its last background turn.
|
|
18
|
+
* Returns an unsubscribe. Used by the AgentCore adapter to push its state snapshot exactly when the
|
|
19
|
+
* state root has settled and before the platform may reclaim the idle microVM.
|
|
20
|
+
*/
|
|
21
|
+
export declare function onIdle(listener: () => void): () => void;
|
|
22
|
+
/** How many units of background work are currently in flight (0 = idle). */
|
|
23
|
+
export declare function activeWork(): number;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SHARED process-wide in-flight work signal. Channels ACK a webhook fast and run the turn
|
|
3
|
+
* fire-and-forget on this process's event loop (host/node.ts) — so "is this process busy?" is not
|
|
4
|
+
* derivable from open HTTP requests. The two shared execution primitives (turn-queue chains,
|
|
5
|
+
* task-tracker side tasks) report here; a serving surface that must stay alive while background
|
|
6
|
+
* work runs (the AgentCore adapter's /ping → HealthyBusy) reads it.
|
|
7
|
+
*
|
|
8
|
+
* Deliberately a counter, not a registry: consumers need only "any work in flight?"; keeping the
|
|
9
|
+
* module dependency-free lets both channel primitives import it without cycles.
|
|
10
|
+
*/
|
|
11
|
+
let inFlight = 0;
|
|
12
|
+
const idleListeners = new Set();
|
|
13
|
+
/**
|
|
14
|
+
* Mark one unit of background work as started. Returns its completion callback — idempotent, so a
|
|
15
|
+
* caller may safely settle it from multiple cleanup paths (finally + catch) without double-counting.
|
|
16
|
+
*/
|
|
17
|
+
export function beginWork() {
|
|
18
|
+
inFlight += 1;
|
|
19
|
+
let done = false;
|
|
20
|
+
return () => {
|
|
21
|
+
if (done)
|
|
22
|
+
return;
|
|
23
|
+
done = true;
|
|
24
|
+
inFlight -= 1;
|
|
25
|
+
if (inFlight > 0)
|
|
26
|
+
return;
|
|
27
|
+
for (const listener of idleListeners) {
|
|
28
|
+
// A listener fault must not corrupt the counter or starve the others (this runs inside a
|
|
29
|
+
// channel's `finally`): report and carry on.
|
|
30
|
+
try {
|
|
31
|
+
listener();
|
|
32
|
+
}
|
|
33
|
+
catch (e) {
|
|
34
|
+
console.error(`[fastagent] idle listener failed: ${String(e)}`);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Subscribe to the 0-in-flight edge: the moment the process finishes its last background turn.
|
|
41
|
+
* Returns an unsubscribe. Used by the AgentCore adapter to push its state snapshot exactly when the
|
|
42
|
+
* state root has settled and before the platform may reclaim the idle microVM.
|
|
43
|
+
*/
|
|
44
|
+
export function onIdle(listener) {
|
|
45
|
+
idleListeners.add(listener);
|
|
46
|
+
return () => {
|
|
47
|
+
idleListeners.delete(listener);
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
/** How many units of background work are currently in flight (0 = idle). */
|
|
51
|
+
export function activeWork() {
|
|
52
|
+
return inFlight;
|
|
53
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/** Per-message bound INSIDE that budget. The fold is a digest of many messages competing for one
|
|
2
|
+
* allowance, so the job here is fairness, not fidelity: one rambler must not price out everyone who
|
|
3
|
+
* spoke after them. 280 against 4000 keeps at least ~14 messages in a full buffer. A referent — the
|
|
4
|
+
* single message the asker points at — is the opposite job and takes REFERENT_MAX_CODE_POINTS. */
|
|
5
|
+
export declare const BUFFER_LINE_MAX_CHARS = 280;
|
|
6
|
+
/** How many buffered files and images (each, most recent first) a summon pulls in with the folded
|
|
7
|
+
* discussion — bounds the latency/token cost of "summarize the file from earlier" against a chatty
|
|
8
|
+
* group posting many attachments between summons. Skipped ones must be counted into the prompt
|
|
9
|
+
* note, so the model never sees an attachment reference it silently cannot open. Shared policy:
|
|
10
|
+
* each channel's attachment collector caps against this. */
|
|
11
|
+
export declare const BUFFER_ATTACH_MAX = 3;
|
|
12
|
+
export interface ContextBuffer<E> {
|
|
13
|
+
/** Record an un-summoned message. Persists BEFORE returning (pre-ACK; see the module header). */
|
|
14
|
+
push(placeKey: string, entry: E): void;
|
|
15
|
+
/** Render the fold text and snapshot the consumed entries (see the module header's consume protocol). */
|
|
16
|
+
peek(placeKey: string): {
|
|
17
|
+
text: string;
|
|
18
|
+
consumed: E[];
|
|
19
|
+
};
|
|
20
|
+
/** Remove exactly `consumed` (by identity) — call on the turn's `completed` event, when the folded
|
|
21
|
+
* discussion provably lives in the durable session. Consumes entries WHOLE, including ones whose
|
|
22
|
+
* attachments failed to load or were cap-skipped: their text is in the session (keeping them would
|
|
23
|
+
* re-fold duplicate text), and the prompt note said what is missing; re-post an attachment to use
|
|
24
|
+
* it. Post-ACK: a failed write is logged, never thrown (it must not abort the turn's delivery). */
|
|
25
|
+
commit(placeKey: string, consumed: E[]): void;
|
|
26
|
+
}
|
|
27
|
+
export declare function createContextBuffer<E>(options: {
|
|
28
|
+
path: string;
|
|
29
|
+
/** Log label, e.g. "[telegram]". */
|
|
30
|
+
label: string;
|
|
31
|
+
/** Shape validator for one persisted entry (the IO boundary — see the module header). */
|
|
32
|
+
isEntry: (value: unknown) => value is E;
|
|
33
|
+
/** One fold line for an entry — ALSO the eviction cost basis. */
|
|
34
|
+
line: (entry: E) => string;
|
|
35
|
+
}): ContextBuffer<E>;
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generic durable context buffer — the SHARED mechanics behind each stateful channel's
|
|
3
|
+
* "un-summoned group discussion" module (telegram/feishu/slack `context-buffer.ts`): recent
|
|
4
|
+
* un-summoned messages per conversation "place", kept under a char budget and folded into the next
|
|
5
|
+
* answered turn in that place, so a summoned agent has the discussion it didn't see turn-by-turn.
|
|
6
|
+
*
|
|
7
|
+
* Channel-neutral and generic over the entry shape (like ../turn-store.ts): the channel supplies its
|
|
8
|
+
* entry type, the shape validator (state files are an IO boundary — valid JSON of the WRONG shape
|
|
9
|
+
* must degrade exactly like a corrupt file: warn + empty, never flow in as trusted data), the
|
|
10
|
+
* fold-line renderer, and its log label. What stays per channel: the entry type itself, place-key
|
|
11
|
+
* derivation, and buffered-attachment selection (platform resource shapes are real differences).
|
|
12
|
+
*
|
|
13
|
+
* DURABLE, with the consume protocol every channel inherits:
|
|
14
|
+
* - `push` persists synchronously BEFORE the transport ACK (an ACKed delivery is not redelivered,
|
|
15
|
+
* so ACK-then-persist would be a silent-loss window): a throw becomes the webhook's 500 and the
|
|
16
|
+
* platform redelivers once the disk recovers — staged on a copy and rolled back on a failed
|
|
17
|
+
* write, so the redelivery does not double-append the entry already in memory.
|
|
18
|
+
* - `peek` renders WITHOUT clearing and snapshots exactly which entries it consumed.
|
|
19
|
+
* - `commit` removes only that snapshot, by object identity, on the turn's `completed` — so a
|
|
20
|
+
* failure or crash before `completed` leaves the discussion intact for the next summon, and a
|
|
21
|
+
* message that arrives while the turn runs survives for the next answered turn (a whole-bucket
|
|
22
|
+
* delete would lose it).
|
|
23
|
+
*/
|
|
24
|
+
import { log } from "../log.js";
|
|
25
|
+
import { loadStateFile, saveStateFile } from "./state.js";
|
|
26
|
+
/** Char budget for the per-place buffer — bounds the cost of folding it into a prompt; when exceeded
|
|
27
|
+
* the OLDEST un-summoned messages are dropped (not a time window: a quiet group keeps its
|
|
28
|
+
* sparse-but-relevant lines, a busy burst is capped). The `line` renderer is the eviction cost
|
|
29
|
+
* basis: the budget must price what the fold actually renders, or it would systematically overrun. */
|
|
30
|
+
const BUFFER_MAX_CHARS = 4000;
|
|
31
|
+
/** Per-message bound INSIDE that budget. The fold is a digest of many messages competing for one
|
|
32
|
+
* allowance, so the job here is fairness, not fidelity: one rambler must not price out everyone who
|
|
33
|
+
* spoke after them. 280 against 4000 keeps at least ~14 messages in a full buffer. A referent — the
|
|
34
|
+
* single message the asker points at — is the opposite job and takes REFERENT_MAX_CODE_POINTS. */
|
|
35
|
+
export const BUFFER_LINE_MAX_CHARS = 280;
|
|
36
|
+
/** How many buffered files and images (each, most recent first) a summon pulls in with the folded
|
|
37
|
+
* discussion — bounds the latency/token cost of "summarize the file from earlier" against a chatty
|
|
38
|
+
* group posting many attachments between summons. Skipped ones must be counted into the prompt
|
|
39
|
+
* note, so the model never sees an attachment reference it silently cannot open. Shared policy:
|
|
40
|
+
* each channel's attachment collector caps against this. */
|
|
41
|
+
export const BUFFER_ATTACH_MAX = 3;
|
|
42
|
+
export function createContextBuffer(options) {
|
|
43
|
+
const { path, label, isEntry, line } = options;
|
|
44
|
+
const load = () => {
|
|
45
|
+
const raw = loadStateFile(path);
|
|
46
|
+
if (raw === undefined)
|
|
47
|
+
return new Map();
|
|
48
|
+
if (typeof raw === "object" &&
|
|
49
|
+
raw !== null &&
|
|
50
|
+
!Array.isArray(raw) &&
|
|
51
|
+
Object.values(raw).every((entries) => Array.isArray(entries) && entries.every(isEntry))) {
|
|
52
|
+
return new Map(Object.entries(raw));
|
|
53
|
+
}
|
|
54
|
+
log.warn(`${label} unexpected shape in ${path} — starting with an empty context buffer`);
|
|
55
|
+
return new Map();
|
|
56
|
+
};
|
|
57
|
+
const buffers = load();
|
|
58
|
+
const persist = () => saveStateFile(path, Object.fromEntries(buffers));
|
|
59
|
+
return {
|
|
60
|
+
push(placeKey, entry) {
|
|
61
|
+
const previous = buffers.get(placeKey);
|
|
62
|
+
const entries = previous ? [...previous] : [];
|
|
63
|
+
entries.push(entry);
|
|
64
|
+
let total = entries.reduce((sum, candidate) => sum + line(candidate).length + 1, 0);
|
|
65
|
+
while (entries.length > 1 && total > BUFFER_MAX_CHARS) {
|
|
66
|
+
const dropped = entries.shift();
|
|
67
|
+
if (dropped)
|
|
68
|
+
total -= line(dropped).length + 1;
|
|
69
|
+
}
|
|
70
|
+
buffers.set(placeKey, entries);
|
|
71
|
+
try {
|
|
72
|
+
persist();
|
|
73
|
+
}
|
|
74
|
+
catch (error) {
|
|
75
|
+
if (previous)
|
|
76
|
+
buffers.set(placeKey, previous);
|
|
77
|
+
else
|
|
78
|
+
buffers.delete(placeKey);
|
|
79
|
+
throw error;
|
|
80
|
+
}
|
|
81
|
+
},
|
|
82
|
+
peek(placeKey) {
|
|
83
|
+
const entries = buffers.get(placeKey) ?? [];
|
|
84
|
+
return { text: entries.map(line).join("\n"), consumed: [...entries] };
|
|
85
|
+
},
|
|
86
|
+
commit(placeKey, consumed) {
|
|
87
|
+
const entries = buffers.get(placeKey);
|
|
88
|
+
if (!entries)
|
|
89
|
+
return;
|
|
90
|
+
const remaining = entries.filter((entry) => !consumed.includes(entry));
|
|
91
|
+
if (remaining.length === 0)
|
|
92
|
+
buffers.delete(placeKey);
|
|
93
|
+
else
|
|
94
|
+
buffers.set(placeKey, remaining);
|
|
95
|
+
try {
|
|
96
|
+
persist();
|
|
97
|
+
}
|
|
98
|
+
catch (error) {
|
|
99
|
+
log.error(`${label} context-buffer write failed post-ACK (a restart may re-fold answered discussion): ${String(error)}`);
|
|
100
|
+
}
|
|
101
|
+
},
|
|
102
|
+
};
|
|
103
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { type SessionControl, type SessionEvent } from "../session.ts";
|
|
2
|
+
import type { Agent } from "../agent.ts";
|
|
3
|
+
import type { Routes } from "../host/node.ts";
|
|
4
|
+
/** The SSE payload: one control-plane event in its transport envelope. */
|
|
5
|
+
export interface WireEvent {
|
|
6
|
+
sessionId: string;
|
|
7
|
+
/** Serving-process incarnation (per `controlRoutes` call). A change means the server restarted:
|
|
8
|
+
* live continuity is gone — run the reconnect steps (entries cursor + state). */
|
|
9
|
+
epoch: string;
|
|
10
|
+
/** Per-connection monotonic counter. A gap means events were lost in transit on THIS connection. */
|
|
11
|
+
seq: number;
|
|
12
|
+
event: SessionEvent;
|
|
13
|
+
}
|
|
14
|
+
export interface ControlRoutesOptions {
|
|
15
|
+
/** Shared bearer secret, required on every route. Never optional: an unauthenticated
|
|
16
|
+
* remote-control endpoint must not be constructible by omission. */
|
|
17
|
+
token: string;
|
|
18
|
+
/** The DATA plane over the wire: when provided, `POST /control/invoke` mounts the standard
|
|
19
|
+
* invoke handler behind the same bearer token — a remote client (Web panel, desktop app,
|
|
20
|
+
* `attach`) can START runs regardless of which channels occupy `/invoke`. Same contract, same
|
|
21
|
+
* SSE event stream; disconnecting the response cancels the run (SPEC cancellation). */
|
|
22
|
+
agent?: Agent;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Mount the control plane: `GET /control/capabilities|state|entries|events` + `POST
|
|
26
|
+
* /control/dispatch`, all bearer-authenticated. `events` streams SSE (`data: <WireEvent>` lines).
|
|
27
|
+
*/
|
|
28
|
+
export declare function controlRoutes(control: SessionControl, options: ControlRoutesOptions): Routes;
|