@fastagent-sh/fastagent 0.18.0 → 0.20.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 +25 -7
- package/dist/agent.d.ts +1 -1
- package/dist/agent.js +1 -1
- package/dist/atomic-write.d.ts +23 -0
- package/dist/atomic-write.js +53 -0
- package/dist/{host/node.d.ts → channel.d.ts} +24 -28
- package/dist/channel.js +1 -0
- package/dist/channels/agentcore-limits.d.ts +9 -0
- package/dist/channels/agentcore-limits.js +9 -0
- package/dist/channels/agentcore-service.d.ts +55 -0
- package/dist/channels/agentcore-service.js +107 -0
- package/dist/channels/agentcore.d.ts +22 -20
- package/dist/channels/agentcore.js +20 -11
- package/dist/channels/busy.d.ts +1 -1
- package/dist/channels/busy.js +1 -1
- package/dist/channels/control.d.ts +55 -7
- package/dist/channels/control.js +312 -67
- package/dist/channels/discover.d.ts +59 -0
- package/dist/{engines/pi/channel.js → channels/discover.js} +19 -34
- package/dist/channels/feishu/card.js +1 -1
- package/dist/channels/feishu/context-buffer.d.ts +1 -1
- package/dist/channels/feishu/context-buffer.js +4 -4
- package/dist/channels/feishu/feishu-api.js +6 -9
- package/dist/channels/feishu/feishu.d.ts +1 -1
- package/dist/channels/feishu/feishu.js +41 -24
- package/dist/channels/feishu/invoke-turn.d.ts +1 -1
- package/dist/channels/feishu/invoke-turn.js +3 -4
- package/dist/channels/feishu/preview.d.ts +1 -1
- package/dist/channels/feishu/preview.js +7 -3
- package/dist/channels/feishu/ws-ingress.d.ts +1 -1
- package/dist/channels/github/github.d.ts +1 -1
- package/dist/channels/http.d.ts +11 -7
- package/dist/channels/http.js +1 -109
- package/dist/channels/kit/attachment-path.d.ts +12 -0
- package/dist/channels/kit/attachment-path.js +43 -0
- package/dist/channels/{context-buffer.d.ts → kit/context-buffer.d.ts} +9 -0
- package/dist/channels/{context-buffer.js → kit/context-buffer.js} +13 -2
- package/dist/channels/{invoke-turn-kit.d.ts → kit/invoke-turn-kit.d.ts} +14 -3
- package/dist/channels/{invoke-turn-kit.js → kit/invoke-turn-kit.js} +17 -6
- package/dist/channels/{preview-kit.d.ts → kit/preview-kit.d.ts} +3 -1
- package/dist/channels/{seen.js → kit/seen.js} +1 -1
- package/dist/channels/kit/signature.d.ts +12 -0
- package/dist/channels/kit/signature.js +17 -0
- package/dist/channels/{state.js → kit/state.js} +4 -7
- package/dist/channels/{stop-command.d.ts → kit/stop-command.d.ts} +1 -1
- package/dist/channels/{stop-command.js → kit/stop-command.js} +5 -5
- package/dist/channels/{tasks.d.ts → kit/tasks.d.ts} +1 -1
- package/dist/channels/{tasks.js → kit/tasks.js} +11 -5
- package/dist/channels/{thread-participants.js → kit/thread-participants.js} +1 -1
- package/dist/channels/{turn-queue.js → kit/turn-queue.js} +2 -2
- package/dist/channels/{turn-store.d.ts → kit/turn-store.d.ts} +19 -3
- package/dist/channels/{turn-store.js → kit/turn-store.js} +30 -3
- package/dist/channels/lark/lark.d.ts +1 -1
- package/dist/channels/serve.d.ts +73 -0
- package/dist/channels/serve.js +243 -0
- package/dist/channels/slack/bot-auth.js +3 -14
- package/dist/channels/slack/context-buffer.d.ts +2 -2
- package/dist/channels/slack/context-buffer.js +2 -2
- package/dist/channels/slack/invoke-turn.d.ts +2 -2
- package/dist/channels/slack/invoke-turn.js +13 -6
- package/dist/channels/slack/onboard.js +5 -5
- package/dist/channels/slack/onboarding-state.d.ts +7 -3
- package/dist/channels/slack/onboarding-state.js +13 -20
- package/dist/channels/slack/parse.js +2 -2
- package/dist/channels/slack/preview.d.ts +1 -1
- package/dist/channels/slack/preview.js +11 -3
- package/dist/channels/slack/register-webhook.js +1 -1
- package/dist/channels/slack/scaffold/slack-send.ts +2 -17
- package/dist/channels/slack/slack-api.js +23 -55
- package/dist/channels/slack/slack.d.ts +1 -1
- package/dist/channels/slack/slack.js +20 -20
- package/dist/channels/slack/welcomed.js +1 -1
- package/dist/channels/telegram/context-buffer.d.ts +2 -2
- package/dist/channels/telegram/context-buffer.js +2 -2
- package/dist/channels/telegram/invoke-turn.d.ts +1 -1
- package/dist/channels/telegram/invoke-turn.js +1 -1
- package/dist/channels/telegram/parse.d.ts +21 -1
- package/dist/channels/telegram/parse.js +67 -13
- package/dist/channels/telegram/preview.d.ts +1 -1
- package/dist/channels/telegram/preview.js +6 -2
- package/dist/channels/telegram/scaffold/channel.ts +7 -3
- package/dist/channels/telegram/telegram-api.js +5 -6
- package/dist/channels/telegram/telegram.d.ts +3 -3
- package/dist/channels/telegram/telegram.js +60 -72
- package/dist/channels/telegram/turn-store.d.ts +2 -2
- package/dist/channels/telegram/turn-store.js +2 -2
- package/dist/cli/add-slack.js +6 -5
- package/dist/cli/commands/add.js +1 -5
- package/dist/cli/commands/attach.d.ts +2 -4
- package/dist/cli/commands/attach.js +17 -15
- package/dist/cli/commands/deploy.d.ts +50 -5
- package/dist/cli/commands/deploy.js +183 -134
- package/dist/cli/commands/dev.js +25 -40
- package/dist/cli/commands/fire.js +1 -1
- package/dist/cli/commands/info.js +7 -5
- package/dist/cli/commands/init.js +1 -2
- package/dist/cli/commands/schedule.js +1 -1
- package/dist/cli/commands/start.js +80 -121
- package/dist/cli/commands/tool.js +5 -7
- package/dist/cli/program.js +1 -1
- package/dist/cli/serve.d.ts +22 -70
- package/dist/cli/serve.js +66 -257
- package/dist/cli/shared.d.ts +40 -10
- package/dist/cli/shared.js +53 -6
- package/dist/core.d.ts +3 -5
- package/dist/core.js +7 -5
- package/dist/deploy/agentcore/logs.d.ts +2 -2
- package/dist/deploy/agentcore/logs.js +2 -2
- package/dist/deploy/agentcore/plan.d.ts +12 -10
- package/dist/deploy/agentcore/plan.js +39 -31
- package/dist/deploy/agentcore/run.d.ts +5 -4
- package/dist/deploy/agentcore/run.js +13 -36
- package/dist/deploy/channel-ingress.d.ts +73 -0
- package/dist/deploy/channel-ingress.js +101 -0
- package/dist/deploy/docker/plan.d.ts +3 -7
- package/dist/deploy/docker/plan.js +4 -15
- package/dist/deploy/docker/run.d.ts +18 -1
- package/dist/deploy/docker/run.js +10 -0
- package/dist/deploy/fly/plan.d.ts +4 -5
- package/dist/deploy/fly/plan.js +7 -22
- package/dist/deploy/fly/run.d.ts +9 -23
- package/dist/deploy/fly/run.js +32 -68
- package/dist/deploy/preflight.d.ts +6 -7
- package/dist/deploy/preflight.js +27 -23
- package/dist/deploy/railway/plan.d.ts +4 -5
- package/dist/deploy/railway/plan.js +11 -26
- package/dist/deploy/railway/run.d.ts +9 -9
- package/dist/deploy/railway/run.js +40 -40
- package/dist/deploy/runner.js +22 -2
- package/dist/deploy/secrets.d.ts +18 -9
- package/dist/deploy/secrets.js +64 -12
- package/dist/dev-supervisor.js +11 -6
- package/dist/engines/pi/agent-session-factory.d.ts +104 -0
- package/dist/engines/pi/agent-session-factory.js +295 -0
- package/dist/engines/pi/auth.js +43 -43
- package/dist/engines/pi/config.d.ts +5 -5
- package/dist/engines/pi/config.js +1 -1
- package/dist/engines/pi/create.d.ts +67 -42
- package/dist/engines/pi/create.js +142 -72
- package/dist/engines/pi/definition.d.ts +42 -8
- package/dist/engines/pi/definition.js +114 -11
- package/dist/engines/pi/invoke-session.d.ts +22 -19
- package/dist/engines/pi/invoke-session.js +286 -100
- package/dist/engines/pi/login.js +39 -20
- package/dist/engines/pi/models.d.ts +14 -0
- package/dist/engines/pi/models.js +9 -1
- package/dist/engines/pi/open.d.ts +10 -8
- package/dist/engines/pi/open.js +21 -11
- package/dist/engines/pi/report.d.ts +0 -4
- package/dist/engines/pi/report.js +0 -7
- package/dist/engines/pi/search-tools.js +1 -1
- package/dist/engines/pi/service.d.ts +23 -0
- package/dist/engines/pi/service.js +27 -0
- package/dist/engines/pi/session-builder.js +87 -118
- package/dist/engines/pi/session-control.d.ts +28 -14
- package/dist/engines/pi/session-control.js +570 -438
- package/dist/engines/pi/session-inheritance.d.ts +30 -0
- package/dist/engines/pi/session-inheritance.js +261 -0
- package/dist/engines/pi/session-markers.d.ts +48 -0
- package/dist/engines/pi/session-markers.js +59 -0
- package/dist/engines/pi/session-settings.d.ts +15 -3
- package/dist/engines/pi/session-settings.js +22 -1
- package/dist/engines/pi/session-store.d.ts +140 -0
- package/dist/engines/pi/session-store.js +666 -0
- package/dist/engines/pi/tool-context.d.ts +36 -13
- package/dist/engines/pi/tool-context.js +51 -4
- package/dist/engines/pi/tool.d.ts +5 -5
- package/dist/engines/pi/tool.js +3 -3
- package/dist/engines/pi/turn-kit.d.ts +32 -6
- package/dist/engines/pi/turn-kit.js +28 -1
- package/dist/engines/pi/wake-tool.js +1 -1
- package/dist/env.js +1 -2
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/loader.d.ts +49 -7
- package/dist/loader.js +79 -18
- package/dist/log.d.ts +3 -0
- package/dist/log.js +5 -1
- package/dist/node.d.ts +16 -0
- package/dist/node.js +20 -0
- package/dist/paths.d.ts +26 -0
- package/dist/paths.js +37 -2
- package/dist/pi.d.ts +14 -8
- package/dist/pi.js +6 -5
- package/dist/scaffold/add-channel.js +2 -2
- package/dist/scaffold/init.js +7 -2
- package/dist/scaffold/templates/persona.md +2 -2
- package/dist/scaffold/templates/tools/fetch-url.ts +0 -2
- package/dist/schedule/discover.js +3 -15
- package/dist/schedule/state.js +4 -9
- package/dist/schedule/wake-alarm.d.ts +13 -7
- package/dist/schedule/wake-alarm.js +88 -46
- package/dist/schedule/wakeups.d.ts +2 -2
- package/dist/schedule/wakeups.js +11 -8
- package/dist/service.d.ts +157 -0
- package/dist/service.js +376 -0
- package/dist/session-remote.d.ts +8 -3
- package/dist/session-remote.js +162 -92
- package/dist/session.d.ts +229 -93
- package/dist/session.js +61 -23
- package/dist/telegram.d.ts +1 -1
- package/dist/telegram.js +1 -1
- package/dist/tunnel.d.ts +18 -3
- package/dist/tunnel.js +32 -40
- package/package.json +14 -5
- package/dist/engines/pi/channel.d.ts +0 -37
- package/dist/engines/pi/harness.d.ts +0 -115
- package/dist/engines/pi/harness.js +0 -154
- package/dist/engines/pi/invoke.d.ts +0 -60
- package/dist/engines/pi/invoke.js +0 -459
- package/dist/engines/pi/read-image.d.ts +0 -4
- package/dist/engines/pi/read-image.js +0 -62
- package/dist/engines/pi/sessions.d.ts +0 -60
- package/dist/engines/pi/sessions.js +0 -464
- package/dist/host/node.js +0 -52
- /package/dist/channels/{preview-kit.js → kit/preview-kit.js} +0 -0
- /package/dist/channels/{seen.d.ts → kit/seen.d.ts} +0 -0
- /package/dist/channels/{state.d.ts → kit/state.d.ts} +0 -0
- /package/dist/channels/{text.d.ts → kit/text.d.ts} +0 -0
- /package/dist/channels/{text.js → kit/text.js} +0 -0
- /package/dist/channels/{thread-participants.d.ts → kit/thread-participants.d.ts} +0 -0
- /package/dist/channels/{turn-queue.d.ts → kit/turn-queue.d.ts} +0 -0
package/README.md
CHANGED
|
@@ -157,21 +157,39 @@ const agent = createPiAgent({
|
|
|
157
157
|
|
|
158
158
|
The root export intentionally contains the supported surface only.
|
|
159
159
|
|
|
160
|
+
Engine-neutral and runtime-neutral are different properties, and the entries are layered by them —
|
|
161
|
+
each layer drops one, and every layer's dependency list is asserted in CI:
|
|
162
|
+
|
|
163
|
+
| | engine-neutral | runtime-neutral | costs |
|
|
164
|
+
|---|---|---|---|
|
|
165
|
+
| `/core`, `/session` | yes | yes | nothing |
|
|
166
|
+
| `/node` | yes | no (filesystem, clock, environment) | `@hono/node-server`, `croner` |
|
|
167
|
+
| `/pi` | no | no | the pi runtime |
|
|
168
|
+
|
|
169
|
+
`Provider` and `ProviderAuth` are exported as types because our options name them; the factory that
|
|
170
|
+
builds one, `createProvider`, comes from `@earendil-works/pi-ai` — add it as a direct dependency when
|
|
171
|
+
you register a custom provider.
|
|
172
|
+
|
|
160
173
|
| Area | Examples | Stability |
|
|
161
174
|
|---|---|---|
|
|
162
175
|
| Contract | `Agent`, `AgentEvent`, `collect` | Stable within SPEC v0.1 |
|
|
163
|
-
|
|
|
176
|
+
| Directory → service | `createAgentService` (pi opener), `mountAgentService` + `MountableAgent` (neutral assembly), `AgentService` | The supported way to mount an agent directory in an app |
|
|
177
|
+
| Mounting | `createInvokeHandler`, `Routes`, `ChannelHandler` | Reference implementation, pre-1.0 |
|
|
178
|
+
| Node binding | `nodeListener`, `serveNode` (from `/node`) | The one runtime-specific piece; see below |
|
|
164
179
|
| pi assembly | `createPiAgentFromDir`, `createPiAgentFromDefinition`, `createPiAgent` | Usable now, may tighten before 1.0 |
|
|
165
|
-
| Tool/channel authoring | `defineTool`, `z`, `
|
|
166
|
-
| Injection ports | `
|
|
167
|
-
| Not exported |
|
|
180
|
+
| Tool/channel authoring | `defineTool`, `z`, `defineSchedule`, `ChannelModule` | Usable now, may tighten before 1.0 |
|
|
181
|
+
| Injection ports | `PiSessionRecordStore`, `piSessionRecordStore`, `piInMemorySessionRecordStore`, `Lease`, `Provider` | Public because options reference them |
|
|
182
|
+
| Not exported | The assembly's parts — `router`, `createControlPlane`, `loadTools`/`loadChannels`/`loadSchedules`, `createScheduler` — and prompt/config internals | `createAgentService` does this; no compatibility promise |
|
|
168
183
|
|
|
169
|
-
Subpath
|
|
184
|
+
Subpath entry points (`./package.json` is also exported, for tools that read the version):
|
|
170
185
|
|
|
171
|
-
- `@fastagent-sh/fastagent/core` — engine-neutral contract, consumption helpers, channel
|
|
186
|
+
- `@fastagent-sh/fastagent/core` — engine-neutral contract, consumption helpers, channel kit, schedules, and the session-control clients (`connectSessionControl`, `connectAgent`). **Zero third-party dependencies**, enforced by test;
|
|
187
|
+
- `@fastagent-sh/fastagent/node` — the engine-neutral pieces that need a Node runtime: `mountAgentService` (the assembly), `serveNode` / `nodeListener` (the `node:http` ↔ Fetch binding);
|
|
188
|
+
- `@fastagent-sh/fastagent/session` — the engine-neutral session-control contract (types and error codes);
|
|
172
189
|
- `@fastagent-sh/fastagent/pi` — the pi reference implementation;
|
|
173
190
|
- `@fastagent-sh/fastagent/github` — GitHub webhook channel;
|
|
174
191
|
- `@fastagent-sh/fastagent/telegram` — Telegram bot channel;
|
|
192
|
+
- `@fastagent-sh/fastagent/slack` — Slack Events API bot channel;
|
|
175
193
|
- `@fastagent-sh/fastagent/feishu` — canonical Feishu bot channel (飞书, open.feishu.cn);
|
|
176
194
|
- `@fastagent-sh/fastagent/lark` — Lark-international compatibility profile over the Feishu engine.
|
|
177
195
|
|
|
@@ -196,7 +214,7 @@ FastAgent is pre-1.0. The stable design center is the Agent Handler contract in
|
|
|
196
214
|
The neutral contract leaves room for capabilities that are not complete product features yet:
|
|
197
215
|
|
|
198
216
|
- **Durable execution**: Telegram, Slack, and Feishu/Lark accepted turns replay at least once today; general durability and exactly-once execution remain future backend work.
|
|
199
|
-
- **Sandboxed execution** —
|
|
217
|
+
- **Sandboxed execution** — all seven coding tools, ② project context, channels, and author-written `tools/` reach the local process directly; a sandbox around the complete agent process is future work.
|
|
200
218
|
- **Observability export** — leveled logs and per-turn traces exist today; an OpenTelemetry exporter does not.
|
|
201
219
|
- **More harness bindings and channels** — pi is the built-in harness; another harness can implement the Agent contract, and community channels can use the channel kit.
|
|
202
220
|
- **More deploy targets** — local Docker, Fly, Railway, and AWS Bedrock AgentCore ship today; the generated container is the portable path for other hosts.
|
package/dist/agent.d.ts
CHANGED
|
@@ -86,7 +86,7 @@ export type AgentEvent = {
|
|
|
86
86
|
export declare const SESSION_BUSY_CODE = "session_busy";
|
|
87
87
|
/**
|
|
88
88
|
* The `failed.code` set when a run was DELIBERATELY stopped — a control-plane abort, or any
|
|
89
|
-
*
|
|
89
|
+
* engine-level abort it attributes (`stopReason: "aborted"`) — rather than failing on its
|
|
90
90
|
* own. Channels can render cancellation distinctly from an error, and MUST treat it as a settled
|
|
91
91
|
* outcome — durable turn-intent cleanup included — so a deliberate stop is never replayed as a
|
|
92
92
|
* fresh turn on restart. Exported as a constant for the same reason as {@link SESSION_BUSY_CODE}:
|
package/dist/agent.js
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
export const SESSION_BUSY_CODE = "session_busy";
|
|
15
15
|
/**
|
|
16
16
|
* The `failed.code` set when a run was DELIBERATELY stopped — a control-plane abort, or any
|
|
17
|
-
*
|
|
17
|
+
* engine-level abort it attributes (`stopReason: "aborted"`) — rather than failing on its
|
|
18
18
|
* own. Channels can render cancellation distinctly from an error, and MUST treat it as a settled
|
|
19
19
|
* outcome — durable turn-intent cleanup included — so a deliberate stop is never replayed as a
|
|
20
20
|
* fresh turn on restart. Exported as a constant for the same reason as {@link SESSION_BUSY_CODE}:
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Write a file so a reader sees the whole thing or nothing: same-directory temp, then rename.
|
|
3
|
+
*
|
|
4
|
+
* The temp name is fixed (`<path>.tmp`). It holds wherever one process writes one state root: a
|
|
5
|
+
* deployment runs one container, `dev`'s supervisor respawns its worker only after the old one has
|
|
6
|
+
* EXITED (dev-supervisor.ts), and these writes are synchronous, so an exited process has none in
|
|
7
|
+
* flight. Slack's onboarding state is the one file with two writers — `add slack`, and the
|
|
8
|
+
* config-token rotation inside `--tunnel` webhook registration — so a second terminal running
|
|
9
|
+
* `add slack --replace-config` can overlap a live `dev --tunnel`. Kept fixed anyway: that window is
|
|
10
|
+
* a single write at tunnel startup, its repair is the `add slack --replace-config` the registration
|
|
11
|
+
* failure already prints, and the fixed name is the seam several channel tests use to inject a write
|
|
12
|
+
* failure by occupying that path with a directory. Revisit for a writer that is neither rare nor
|
|
13
|
+
* self-repairing.
|
|
14
|
+
*
|
|
15
|
+
* `mode` is applied to the temp first, so the content is never briefly world-readable.
|
|
16
|
+
*
|
|
17
|
+
* The `chmod` is NOT redundant with the `mode` option: `writeFileSync` applies `mode` only when it
|
|
18
|
+
* CREATES the file, so a temp left behind by a crashed writer keeps its old, possibly loose
|
|
19
|
+
* permissions and the rename publishes them (verified: 0644 survives a `{ mode: 0o600 }` write).
|
|
20
|
+
* It runs on the temp, before the rename — the final path is then never observable with the wrong
|
|
21
|
+
* permissions, which a chmod after the rename cannot promise.
|
|
22
|
+
*/
|
|
23
|
+
export declare function writeFileAtomic(path: string, data: string | Buffer, mode?: number): void;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One spelling of "a reader sees the whole file or none of it", after five copies of it drifted
|
|
3
|
+
* apart: two identical, three with different temp names and different permission handling.
|
|
4
|
+
*
|
|
5
|
+
* Synchronous, and there is no async sibling: every caller either sits on a path where a KB-sized
|
|
6
|
+
* write must complete BEFORE a transport ACK (channel state — an ACKed delivery is not redelivered)
|
|
7
|
+
* or on a CLI/startup path where the cost is not observable. An async spelling would buy one of them
|
|
8
|
+
* nothing and cost this module a second set of rules to keep true.
|
|
9
|
+
*/
|
|
10
|
+
import { chmodSync, mkdirSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
11
|
+
import { dirname } from "node:path";
|
|
12
|
+
/**
|
|
13
|
+
* Write a file so a reader sees the whole thing or nothing: same-directory temp, then rename.
|
|
14
|
+
*
|
|
15
|
+
* The temp name is fixed (`<path>.tmp`). It holds wherever one process writes one state root: a
|
|
16
|
+
* deployment runs one container, `dev`'s supervisor respawns its worker only after the old one has
|
|
17
|
+
* EXITED (dev-supervisor.ts), and these writes are synchronous, so an exited process has none in
|
|
18
|
+
* flight. Slack's onboarding state is the one file with two writers — `add slack`, and the
|
|
19
|
+
* config-token rotation inside `--tunnel` webhook registration — so a second terminal running
|
|
20
|
+
* `add slack --replace-config` can overlap a live `dev --tunnel`. Kept fixed anyway: that window is
|
|
21
|
+
* a single write at tunnel startup, its repair is the `add slack --replace-config` the registration
|
|
22
|
+
* failure already prints, and the fixed name is the seam several channel tests use to inject a write
|
|
23
|
+
* failure by occupying that path with a directory. Revisit for a writer that is neither rare nor
|
|
24
|
+
* self-repairing.
|
|
25
|
+
*
|
|
26
|
+
* `mode` is applied to the temp first, so the content is never briefly world-readable.
|
|
27
|
+
*
|
|
28
|
+
* The `chmod` is NOT redundant with the `mode` option: `writeFileSync` applies `mode` only when it
|
|
29
|
+
* CREATES the file, so a temp left behind by a crashed writer keeps its old, possibly loose
|
|
30
|
+
* permissions and the rename publishes them (verified: 0644 survives a `{ mode: 0o600 }` write).
|
|
31
|
+
* It runs on the temp, before the rename — the final path is then never observable with the wrong
|
|
32
|
+
* permissions, which a chmod after the rename cannot promise.
|
|
33
|
+
*/
|
|
34
|
+
export function writeFileAtomic(path, data, mode) {
|
|
35
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
36
|
+
const tmp = `${path}.tmp`;
|
|
37
|
+
try {
|
|
38
|
+
writeFileSync(tmp, data, mode === undefined ? undefined : { mode });
|
|
39
|
+
if (mode !== undefined)
|
|
40
|
+
chmodSync(tmp, mode);
|
|
41
|
+
renameSync(tmp, path);
|
|
42
|
+
}
|
|
43
|
+
catch (error) {
|
|
44
|
+
try {
|
|
45
|
+
rmSync(tmp, { force: true });
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
// `force` only forgives ENOENT: if the temp is a directory, rmSync throws its own error and
|
|
49
|
+
// would replace the write failure that actually explains what went wrong.
|
|
50
|
+
}
|
|
51
|
+
throw error;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
@@ -1,8 +1,28 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
/**
|
|
2
|
+
* The Channel contract — the trigger side of the product boundary (core.md §1), beside `agent.ts`
|
|
3
|
+
* (what an engine implements) and `session.ts` (the serving control plane). Pure types, no runtime
|
|
4
|
+
* dependency; importing a host, a framework, or an engine here is forbidden, as in those two.
|
|
5
|
+
*
|
|
6
|
+
* §7 fixes two module forms: a function is a route channel, an object with `connect` is a long
|
|
7
|
+
* connection. An agent directory ships hand-written `channels/*.ts` against them, which is why they
|
|
8
|
+
* live away from the code that serves them — a WebSocket ingress needs `LongConnection` and has no
|
|
9
|
+
* HTTP in it, and a `ChannelModule` import must not drag `node:http` in behind it.
|
|
10
|
+
*
|
|
11
|
+
* How a route table becomes a running server: `channels/serve.ts`.
|
|
12
|
+
*/
|
|
13
|
+
import type { Agent } from "./agent.ts";
|
|
14
|
+
import type { SessionControl } from "./session.ts";
|
|
15
|
+
/** A mounted request handler (a channel's fetch, or a plain route like health). Fetch-shaped by
|
|
16
|
+
* contract (SPEC §11): the one signature every runtime and embedding app already speaks. */
|
|
4
17
|
export type ChannelHandler = (req: Request) => Response | Promise<Response>;
|
|
5
|
-
/**
|
|
18
|
+
/**
|
|
19
|
+
* This deployment's HTTP surface: route key → handler.
|
|
20
|
+
*
|
|
21
|
+
* A key is `"/path"` (any method) or `"METHOD /path"`, with a LITERAL path — small on purpose, so
|
|
22
|
+
* that "would these two fight over a request?" is string equality rather than a prediction about a
|
|
23
|
+
* matcher, and no channel silently shadows another. `assertRouteKey` in `channels/serve.ts`
|
|
24
|
+
* enforces it; a handler owning a prefix is a `PrefixMount`, never a key.
|
|
25
|
+
*/
|
|
6
26
|
export type Routes = Record<string, ChannelHandler>;
|
|
7
27
|
/**
|
|
8
28
|
* What the framework hands a channel at mount time: the assembled agent plus the resolved state ROOT
|
|
@@ -34,27 +54,3 @@ export interface LongConnectionChannelModule {
|
|
|
34
54
|
name: string;
|
|
35
55
|
connect(ctx: ChannelContext, signal: AbortSignal): LongConnection;
|
|
36
56
|
}
|
|
37
|
-
/** Parse a route key: `"METHOD /path"` → `{ method, path }`, or `"/path"` → `{ path }` (any method). */
|
|
38
|
-
export declare function parseRouteKey(key: string): {
|
|
39
|
-
method?: string;
|
|
40
|
-
path: string;
|
|
41
|
-
};
|
|
42
|
-
/**
|
|
43
|
-
* Compose a {@link Routes} table into one handler: exact pathname match (optionally method-qualified),
|
|
44
|
-
* 405 when the path exists under another method, 404 otherwise. No params/wildcards.
|
|
45
|
-
*/
|
|
46
|
-
export declare function router(routes: Routes): ChannelHandler;
|
|
47
|
-
/**
|
|
48
|
-
* Serve `handler` on a Node HTTP server. Thin mechanism: bind, report the port, let the caller stop
|
|
49
|
-
* accepting or force-close active connections — no logging/signals/exit (the CLI owns those).
|
|
50
|
-
* `listening` resolves with the bound port (useful for port 0) or rejects on a bind error.
|
|
51
|
-
* `host` is the bind address; unset means all interfaces (what containers need).
|
|
52
|
-
*/
|
|
53
|
-
export declare function serveNode(handler: ChannelHandler, options: {
|
|
54
|
-
port: number;
|
|
55
|
-
host?: string;
|
|
56
|
-
}): {
|
|
57
|
-
listening: Promise<number>;
|
|
58
|
-
close: () => Promise<void>;
|
|
59
|
-
closeAllConnections: () => void;
|
|
60
|
-
};
|
package/dist/channel.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -1,3 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The HOST's body ceilings, and the one place they are computed.
|
|
3
|
+
*
|
|
4
|
+
* Lambda Function URLs cap a request at 6 MB, so the forwarder cannot deliver more than that no
|
|
5
|
+
* matter what the adapter accepts; the body arrives base64-encoded (×4/3) inside a JSON envelope, so
|
|
6
|
+
* the ORIGINAL body ceiling is smaller still. This is a real capability difference from a resident
|
|
7
|
+
* host — the GitHub channel's own contract is 25 MiB — which is why `deploy agentcore` states it at
|
|
8
|
+
* plan time rather than letting an oversized payload surface as an opaque 502.
|
|
9
|
+
*/
|
|
1
10
|
/**
|
|
2
11
|
* Maximum original webhook body after reserving JSON-envelope overhead and base64 expansion.
|
|
3
12
|
* Enforced by both the public forwarder and the runtime adapter.
|
|
@@ -1,3 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The HOST's body ceilings, and the one place they are computed.
|
|
3
|
+
*
|
|
4
|
+
* Lambda Function URLs cap a request at 6 MB, so the forwarder cannot deliver more than that no
|
|
5
|
+
* matter what the adapter accepts; the body arrives base64-encoded (×4/3) inside a JSON envelope, so
|
|
6
|
+
* the ORIGINAL body ceiling is smaller still. This is a real capability difference from a resident
|
|
7
|
+
* host — the GitHub channel's own contract is 25 MiB — which is why `deploy agentcore` states it at
|
|
8
|
+
* plan time rather than letting an oversized payload surface as an opaque 502.
|
|
9
|
+
*/
|
|
1
10
|
/** AWS Lambda Function URLs accept request payloads up to 6 MB. */
|
|
2
11
|
const FUNCTION_URL_REQUEST_LIMIT = 6 * 1000 * 1000;
|
|
3
12
|
/**
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The AgentCore serving assembly — the same product as `mountAgentService`, built differently
|
|
3
|
+
* because the host is.
|
|
4
|
+
*
|
|
5
|
+
* Two facts drive every difference. There is no public URL (the adapter's `POST /invocations` is the
|
|
6
|
+
* only ingress, and cron slots arrive through it from an external clock, so no resident timers), and
|
|
7
|
+
* **the state mount at boot is PRE-RESTORE** — empty after every version update. Discovering channels
|
|
8
|
+
* eagerly would therefore cache that emptiness (thread participation, delivery dedup, pending turns)
|
|
9
|
+
* and then clobber the restored files with it, so channels are constructed lazily on the first
|
|
10
|
+
* envelope. Everywhere else the state root is durable at boot and a broken channel fails startup.
|
|
11
|
+
*
|
|
12
|
+
* That is a different assembly, not a flag on the shared one: handler shape, discovery timing, clock
|
|
13
|
+
* source, long-connection support and shutdown all differ. What it is NOT is a different product —
|
|
14
|
+
* it returns the same {@link AgentService}, so `start` picks an assembly once and everything after
|
|
15
|
+
* that point is common.
|
|
16
|
+
*/
|
|
17
|
+
import type { Agent } from "../agent.ts";
|
|
18
|
+
import type { Routes } from "../channel.ts";
|
|
19
|
+
import type { LoadedSchedule } from "../schedule/schedule.ts";
|
|
20
|
+
import { type AgentService, type MountableAgent } from "../service.ts";
|
|
21
|
+
import { type AgentcoreAdapterOptions } from "./agentcore.ts";
|
|
22
|
+
export interface MountAgentcoreServiceOptions {
|
|
23
|
+
/** Wrap the opened agent before anything binds to it (the CLI's turn trace). */
|
|
24
|
+
wrapAgent?: (agent: Agent) => Agent;
|
|
25
|
+
/** Runs once the state snapshot is restored. The wake-alarm reconcile passes through here because
|
|
26
|
+
* its sink is a PROCESS-global: the process entry owns that, not a service that can be closed. */
|
|
27
|
+
onStateReady?: () => void;
|
|
28
|
+
control?: {
|
|
29
|
+
tunnel?: boolean;
|
|
30
|
+
host?: string;
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
/** Is this process running inside the AgentCore Runtime? Set by the generated deploy artifacts. */
|
|
34
|
+
export declare function isAgentcoreRuntime(): boolean;
|
|
35
|
+
export declare function mountAgentcoreService(opened: MountableAgent, options?: MountAgentcoreServiceOptions): Promise<AgentService>;
|
|
36
|
+
/**
|
|
37
|
+
* Mount the AgentCore Runtime adapter (`POST /invocations` + `GET /ping`) — the deployed container's
|
|
38
|
+
* ONLY reachable surface (channels/agentcore.ts). Wired by `start` when `FASTAGENT_AGENTCORE=1` (set
|
|
39
|
+
* by the generated deploy artifacts, never by hand).
|
|
40
|
+
*
|
|
41
|
+
* The adapter IS the surface: the agent's channels live in a table INSIDE the envelope dispatch, a
|
|
42
|
+
* separate namespace from these two paths, so a channel route named `/invocations` is reached
|
|
43
|
+
* through the Function URL as itself and cannot shadow anything.
|
|
44
|
+
*/
|
|
45
|
+
export declare function mountAgentcore(options: {
|
|
46
|
+
agent: Agent;
|
|
47
|
+
stateRoot: string;
|
|
48
|
+
schedules: readonly LoadedSchedule[];
|
|
49
|
+
onStateReady?: () => void;
|
|
50
|
+
/** Cancels the adapter's process-global registrations on close. */
|
|
51
|
+
signal?: AbortSignal;
|
|
52
|
+
/** The channel surface, constructed on the first envelope AFTER the state-snapshot restore — never
|
|
53
|
+
* at boot, where the mount is pre-restore (channels/agentcore.ts). */
|
|
54
|
+
channels: AgentcoreAdapterOptions["channels"];
|
|
55
|
+
}): Routes;
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { log } from "../log.js";
|
|
2
|
+
import { fireScheduleOnce } from "../schedule/scheduler.js";
|
|
3
|
+
import { assertNoControlPlaneCollision, mountSessionControl, routesFor, startSchedules, } from "../service.js";
|
|
4
|
+
import { UnknownScheduleError, agentcoreRoutes } from "./agentcore.js";
|
|
5
|
+
import { createStateSync } from "./agentcore-state.js";
|
|
6
|
+
import { activeWork } from "./busy.js";
|
|
7
|
+
import { router } from "./serve.js";
|
|
8
|
+
/** Is this process running inside the AgentCore Runtime? Set by the generated deploy artifacts. */
|
|
9
|
+
export function isAgentcoreRuntime() {
|
|
10
|
+
return process.env.FASTAGENT_AGENTCORE === "1";
|
|
11
|
+
}
|
|
12
|
+
export async function mountAgentcoreService(opened, options = {}) {
|
|
13
|
+
const { agentDir, workspace, stateRoot, sessionControl } = opened;
|
|
14
|
+
const agent = options.wrapAgent?.(opened.agent) ?? opened.agent;
|
|
15
|
+
// The control plane mounts over an EMPTY route surface: the lazy channels join it later, and the
|
|
16
|
+
// collision rule runs again then (below) against what they actually brought.
|
|
17
|
+
const withControl = mountSessionControl({}, sessionControl, stateRoot, { ...options.control, agent });
|
|
18
|
+
const scheduled = await startSchedules(agentDir, agent, stateRoot, opened.selfSchedule, {
|
|
19
|
+
externalClock: true,
|
|
20
|
+
});
|
|
21
|
+
const lazyChannels = async () => {
|
|
22
|
+
const lazy = await routesFor(agentDir, agent, stateRoot, sessionControl, { builtinInvoke: false });
|
|
23
|
+
if (lazy.longConnections.length > 0) {
|
|
24
|
+
throw new Error(`long-connection channel(s) ${lazy.longConnections.map((c) => c.name).join(", ")} cannot serve on ` +
|
|
25
|
+
`AgentCore (scale-to-zero severs resident connections) — use the channel's webhook form`);
|
|
26
|
+
}
|
|
27
|
+
// The SAME rule mountSessionControl applies, through the same function: its check ran against an
|
|
28
|
+
// empty base at boot, so it has to run again once the channels are real.
|
|
29
|
+
for (const plane of withControl.mounts)
|
|
30
|
+
assertNoControlPlaneCollision(lazy.routes, plane);
|
|
31
|
+
return { routes: lazy.routes, mounts: withControl.mounts };
|
|
32
|
+
};
|
|
33
|
+
// The adapter registers process-global listeners; this is what takes them down on close.
|
|
34
|
+
const closed = new AbortController();
|
|
35
|
+
const adapterRoutes = mountAgentcore({
|
|
36
|
+
signal: closed.signal,
|
|
37
|
+
agent,
|
|
38
|
+
stateRoot,
|
|
39
|
+
schedules: scheduled.schedules,
|
|
40
|
+
onStateReady: options.onStateReady,
|
|
41
|
+
channels: lazyChannels,
|
|
42
|
+
});
|
|
43
|
+
const handler = router(adapterRoutes, withControl.mounts);
|
|
44
|
+
log.info(`[fastagent] agentcore: serving POST /invocations + GET /ping (FASTAGENT_AGENTCORE=1)`);
|
|
45
|
+
let unannounce;
|
|
46
|
+
return {
|
|
47
|
+
handler,
|
|
48
|
+
agent,
|
|
49
|
+
// The adapter IS the surface here; the channel routes arrive lazily BEHIND it. Reporting `{}`
|
|
50
|
+
// would make the startup line claim nothing is served.
|
|
51
|
+
routes: adapterRoutes,
|
|
52
|
+
agentDir,
|
|
53
|
+
workspace,
|
|
54
|
+
// Unknown at boot by design — a channel list here would be the pre-restore emptiness.
|
|
55
|
+
channels: { routes: [], longConnections: [], builtinInvoke: false },
|
|
56
|
+
schedules: scheduled.schedules,
|
|
57
|
+
ready: Promise.resolve(), // nothing to open: no port of our own, no resident connections
|
|
58
|
+
...(withControl.control ? { control: withControl.control } : {}),
|
|
59
|
+
announce(boundPort) {
|
|
60
|
+
unannounce = withControl.announce(boundPort);
|
|
61
|
+
},
|
|
62
|
+
async close() {
|
|
63
|
+
// UNTESTED, deliberately noted: no test observes these timers being cleared. Installing fake
|
|
64
|
+
// timers early enough to count them deadlocks the assembly's own IO. What IS tested is that
|
|
65
|
+
// close() runs and is idempotent; the stop itself rides on scheduler.stop()'s own tests.
|
|
66
|
+
scheduled.stop();
|
|
67
|
+
closed.abort();
|
|
68
|
+
unannounce?.(); // a stale discovery file would point `attach` at a stopped service
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Mount the AgentCore Runtime adapter (`POST /invocations` + `GET /ping`) — the deployed container's
|
|
74
|
+
* ONLY reachable surface (channels/agentcore.ts). Wired by `start` when `FASTAGENT_AGENTCORE=1` (set
|
|
75
|
+
* by the generated deploy artifacts, never by hand).
|
|
76
|
+
*
|
|
77
|
+
* The adapter IS the surface: the agent's channels live in a table INSIDE the envelope dispatch, a
|
|
78
|
+
* separate namespace from these two paths, so a channel route named `/invocations` is reached
|
|
79
|
+
* through the Function URL as itself and cannot shadow anything.
|
|
80
|
+
*/
|
|
81
|
+
export function mountAgentcore(options) {
|
|
82
|
+
const { agent, stateRoot, schedules, onStateReady, channels, signal } = options;
|
|
83
|
+
return agentcoreRoutes({
|
|
84
|
+
channels,
|
|
85
|
+
agent,
|
|
86
|
+
stateRoot,
|
|
87
|
+
isBusy: () => activeWork() > 0,
|
|
88
|
+
// Cross-deploy durability: AgentCore wipes the state mount on every runtime version update, so
|
|
89
|
+
// the state root is restored from (and pushed to) an S3 snapshot through presigned URLs the
|
|
90
|
+
// forwarder mints per envelope. Always wired on this path — the platform gives no other way to
|
|
91
|
+
// keep an agent's memory across a deploy.
|
|
92
|
+
stateSync: createStateSync({ stateRoot }),
|
|
93
|
+
// What separates a forwarder envelope from any IAM principal's InvokeAgentRuntime call. Absent =
|
|
94
|
+
// no forwarder in this topology, so only the public `invoke` kind is servable.
|
|
95
|
+
ingressSecret: process.env.FASTAGENT_INGRESS_SECRET,
|
|
96
|
+
onStateReady,
|
|
97
|
+
...(signal ? { signal } : {}),
|
|
98
|
+
fire: schedules.length === 0
|
|
99
|
+
? undefined
|
|
100
|
+
: (name, slot) => {
|
|
101
|
+
const schedule = schedules.find((s) => s.name === name);
|
|
102
|
+
if (!schedule)
|
|
103
|
+
throw new UnknownScheduleError(name);
|
|
104
|
+
return fireScheduleOnce({ agent, stateRoot, schedule, slot });
|
|
105
|
+
},
|
|
106
|
+
});
|
|
107
|
+
}
|
|
@@ -1,15 +1,8 @@
|
|
|
1
1
|
import type { Agent } from "../agent.ts";
|
|
2
2
|
import type { StateSync, StateUrls } from "./agentcore-state.ts";
|
|
3
|
-
import type { Routes } from "../
|
|
3
|
+
import type { Routes } from "../channel.ts";
|
|
4
|
+
import { type PrefixMount } from "../channels/serve.ts";
|
|
4
5
|
import type { ScheduleFireOutcome } from "../schedule/scheduler.ts";
|
|
5
|
-
/**
|
|
6
|
-
* The HOST's webhook body limit, and the one place it is computed. Lambda Function URLs cap a request
|
|
7
|
-
* at 6 MB, so the forwarder cannot deliver more than that no matter what the adapter accepts; the
|
|
8
|
-
* body arrives base64-encoded (×4/3) inside a JSON envelope, so the ORIGINAL body ceiling is smaller
|
|
9
|
-
* still. This is a real capability difference from a resident host — the GitHub channel's own
|
|
10
|
-
* contract is 25 MiB — so `deploy agentcore` says so at plan time rather than letting an oversized
|
|
11
|
-
* payload surface as an opaque 502.
|
|
12
|
-
*/
|
|
13
6
|
/** What the forwarder Lambda / EventBridge deliver in the `/invocations` payload. Every kind may
|
|
14
7
|
* carry `wake` — the forwarder's self-resolved public URL, which the adapter persists so the wake
|
|
15
8
|
* ALARM sink (schedule/wake-alarm.ts) can call back without the URL being baked anywhere. */
|
|
@@ -71,14 +64,21 @@ export interface WebhookReply {
|
|
|
71
64
|
headers: Record<string, string>;
|
|
72
65
|
bodyB64: string;
|
|
73
66
|
}
|
|
67
|
+
/** What the lazy factory hands back: literal routes plus any prefix-owning mounts (the control
|
|
68
|
+
* plane), so the adapter's INNER dispatch is assembled exactly like a direct host's. */
|
|
69
|
+
export interface RouteSurface {
|
|
70
|
+
routes: Routes;
|
|
71
|
+
mounts?: readonly PrefixMount[];
|
|
72
|
+
}
|
|
74
73
|
export interface AgentcoreAdapterOptions {
|
|
75
|
-
/** The serving routes a direct deployment would mount (channels or the builtin invoke + health)
|
|
76
|
-
*
|
|
77
|
-
*
|
|
78
|
-
*
|
|
79
|
-
*
|
|
80
|
-
*
|
|
81
|
-
|
|
74
|
+
/** The serving routes a direct deployment would mount (channels or the builtin invoke + health),
|
|
75
|
+
* built LAZILY: channel construction loads channel state and replays durable turn intent, so on
|
|
76
|
+
* AgentCore it must not run until the state root is authoritative — which happens at the first
|
|
77
|
+
* envelope's `stateSync.ready()` (the restore URLs only an envelope carries), never at boot, where
|
|
78
|
+
* the mount is pre-restore (empty after every version update). A factory, not a value: there is no
|
|
79
|
+
* moment during construction at which the right answer is knowable. May answer synchronously — the
|
|
80
|
+
* resolution chain normalizes it either way. */
|
|
81
|
+
channels: () => Promise<RouteSurface> | RouteSurface;
|
|
82
82
|
agent: Agent;
|
|
83
83
|
/** Where the forwarder URL from envelopes is persisted for the wake-alarm sink (the state root). */
|
|
84
84
|
stateRoot: string;
|
|
@@ -94,15 +94,17 @@ export interface AgentcoreAdapterOptions {
|
|
|
94
94
|
/** FASTAGENT_INGRESS_SECRET: what makes an envelope the FORWARDER's rather than any IAM principal's.
|
|
95
95
|
* Undefined = nothing can be trusted, so only the public `invoke` kind is served. */
|
|
96
96
|
ingressSecret?: string;
|
|
97
|
+
/** Cancels the adapter's process-global registrations. Without it a closed adapter keeps saving
|
|
98
|
+
* state on every later idle edge — including work belonging to whatever mounted after it. */
|
|
99
|
+
signal?: AbortSignal;
|
|
97
100
|
/** Runs ONCE, after the state root is authoritative (post-restore) — the wake-alarm reconcile, which
|
|
98
101
|
* at boot would see the mount the platform just wiped and conclude there is nothing pending. */
|
|
99
102
|
onStateReady?: () => void;
|
|
100
103
|
}
|
|
101
104
|
/**
|
|
102
|
-
* Build the AgentCore serving surface: `{ "POST /invocations", "GET /ping" }
|
|
103
|
-
*
|
|
104
|
-
*
|
|
105
|
-
* a local `curl` debug surface.
|
|
105
|
+
* Build the AgentCore serving surface: `{ "POST /invocations", "GET /ping" }` — the whole of what the
|
|
106
|
+
* platform routes into the container. The agent's own channels are not beside these: they are a table
|
|
107
|
+
* inside the envelope dispatch below, reached only by unwrapping a forwarder envelope.
|
|
106
108
|
*/
|
|
107
109
|
export declare function agentcoreRoutes(options: AgentcoreAdapterOptions): Routes;
|
|
108
110
|
/** Thrown by the mount-site `fire` binding when the envelope names a schedule this workspace does
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
import { Buffer } from "node:buffer";
|
|
29
29
|
import { timingSafeEqual } from "node:crypto";
|
|
30
30
|
import { beginWork, onIdle } from "./busy.js";
|
|
31
|
-
import { router } from "../
|
|
31
|
+
import { router } from "../channels/serve.js";
|
|
32
32
|
import { log } from "../log.js";
|
|
33
33
|
import { rememberWakeAlarmUrl } from "../schedule/wake-alarm.js";
|
|
34
34
|
import { readBodyCapped } from "./body.js";
|
|
@@ -48,14 +48,13 @@ function secretMatches(actual, expected) {
|
|
|
48
48
|
return actualBytes.length === expectedBytes.length && timingSafeEqual(actualBytes, expectedBytes);
|
|
49
49
|
}
|
|
50
50
|
/**
|
|
51
|
-
* Build the AgentCore serving surface: `{ "POST /invocations", "GET /ping" }
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
* a local `curl` debug surface.
|
|
51
|
+
* Build the AgentCore serving surface: `{ "POST /invocations", "GET /ping" }` — the whole of what the
|
|
52
|
+
* platform routes into the container. The agent's own channels are not beside these: they are a table
|
|
53
|
+
* inside the envelope dispatch below, reached only by unwrapping a forwarder envelope.
|
|
55
54
|
*/
|
|
56
55
|
export function agentcoreRoutes(options) {
|
|
57
|
-
const {
|
|
58
|
-
// Lazy channel construction (see AgentcoreAdapterOptions.
|
|
56
|
+
const { channels, agent, stateRoot, isBusy, fire, stateSync, ingressSecret, onStateReady } = options;
|
|
57
|
+
// Lazy channel construction (see AgentcoreAdapterOptions.channels) — resolved ONCE per process, on
|
|
59
58
|
// the first trusted envelope after the state root is authoritative, and the outcome is cached
|
|
60
59
|
// EITHER WAY. Success: the same resident channels a direct host keeps. Failure too: construction
|
|
61
60
|
// is an ACTIVATION with side effects — loadChannels builds every healthy channel (starting its
|
|
@@ -71,8 +70,8 @@ export function agentcoreRoutes(options) {
|
|
|
71
70
|
// The factory runs INSIDE the chain: a synchronous throw must land in the cached rejection,
|
|
72
71
|
// not escape before `dispatchP` is assigned (which would silently re-run the activation).
|
|
73
72
|
dispatchP = Promise.resolve()
|
|
74
|
-
.then(
|
|
75
|
-
.then(router);
|
|
73
|
+
.then(channels)
|
|
74
|
+
.then((surface) => router(surface.routes, surface.mounts));
|
|
76
75
|
dispatchP.catch(() => { }); // observed here so the CACHED rejection is never "unhandled"
|
|
77
76
|
}
|
|
78
77
|
return dispatchP;
|
|
@@ -80,8 +79,10 @@ export function agentcoreRoutes(options) {
|
|
|
80
79
|
const invokeHandler = createInvokeHandler(agent);
|
|
81
80
|
// Snapshot on the 0-in-flight edge: webhook channels ACK fast and finish the turn in the
|
|
82
81
|
// background, so "the request returned" is NOT when the state root settles.
|
|
83
|
-
if (stateSync)
|
|
84
|
-
onIdle(() => stateSync.save());
|
|
82
|
+
if (stateSync) {
|
|
83
|
+
const off = onIdle(() => stateSync.save());
|
|
84
|
+
options.signal?.addEventListener("abort", off, { once: true });
|
|
85
|
+
}
|
|
85
86
|
let warnedUnsnapshotted = false;
|
|
86
87
|
let stateReadyFired = false;
|
|
87
88
|
const handleInvocation = async (req) => {
|
|
@@ -299,6 +300,14 @@ export function agentcoreRoutes(options) {
|
|
|
299
300
|
};
|
|
300
301
|
// Settle-then-snapshot: when the envelope leaves nothing in flight, its writes are final now (a
|
|
301
302
|
// background turn instead reports through the idle edge above).
|
|
303
|
+
//
|
|
304
|
+
// A public `invoke` is the one kind this MISSES, measurably: it answers with an unconsumed SSE
|
|
305
|
+
// stream, so the turn runs after this line and its records wait for the next envelope's snapshot.
|
|
306
|
+
// Counting it as in-flight work would close that and cost more than it buys — the stream drains at
|
|
307
|
+
// the CLIENT's pace, so one parked reader pins /ping at HealthyBusy and defeats the idle reclaim,
|
|
308
|
+
// which is the quota-exhaustion failure the ping contract below warns about. The kinds that carry
|
|
309
|
+
// conversations (webhook, schedule-fire, wake-poke) all run their turn inside the request or
|
|
310
|
+
// through `beginWork`, so they land on one of the two edges; `invoke` is the direct/debug door.
|
|
302
311
|
const invocations = async (req) => {
|
|
303
312
|
const response = await handleInvocation(req);
|
|
304
313
|
if (stateSync && !isBusy())
|
package/dist/channels/busy.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
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 (
|
|
3
|
+
* fire-and-forget on this process's event loop (serve.ts) — so "is this process busy?" is not
|
|
4
4
|
* derivable from open HTTP requests. The two shared execution primitives (turn-queue chains,
|
|
5
5
|
* task-tracker side tasks) report here; a serving surface that must stay alive while background
|
|
6
6
|
* work runs (the AgentCore adapter's /ping → HealthyBusy) reads it.
|
package/dist/channels/busy.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
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 (
|
|
3
|
+
* fire-and-forget on this process's event loop (serve.ts) — so "is this process busy?" is not
|
|
4
4
|
* derivable from open HTTP requests. The two shared execution primitives (turn-queue chains,
|
|
5
5
|
* task-tracker side tasks) report here; a serving surface that must stay alive while background
|
|
6
6
|
* work runs (the AgentCore adapter's /ping → HealthyBusy) reads it.
|