@fastagent-sh/fastagent 0.21.1 → 0.22.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -12
- package/dist/agent.d.ts +24 -37
- package/dist/agent.js +7 -15
- package/dist/atomic-write.d.ts +13 -17
- package/dist/atomic-write.js +18 -29
- package/dist/bind.d.ts +12 -22
- package/dist/bind.js +18 -27
- package/dist/channel.d.ts +11 -33
- package/dist/channels/agentcore-limits.d.ts +2 -13
- package/dist/channels/agentcore-limits.js +2 -13
- package/dist/channels/agentcore-protocol.d.ts +67 -58
- package/dist/channels/agentcore-protocol.js +23 -15
- package/dist/channels/agentcore-service.d.ts +19 -33
- package/dist/channels/agentcore-service.js +176 -59
- package/dist/channels/agentcore.d.ts +39 -33
- package/dist/channels/agentcore.js +90 -224
- package/dist/channels/body.d.ts +15 -2
- package/dist/channels/body.js +24 -2
- package/dist/channels/busy.d.ts +1 -21
- package/dist/channels/busy.js +2 -39
- package/dist/channels/control.d.ts +17 -62
- package/dist/channels/control.js +74 -182
- package/dist/channels/define-channel.d.ts +39 -0
- package/dist/channels/define-channel.js +9 -0
- package/dist/channels/discover.d.ts +10 -31
- package/dist/channels/discover.js +37 -31
- package/dist/channels/feishu/bootstrap-token.d.ts +1 -8
- package/dist/channels/feishu/bootstrap-token.js +7 -23
- package/dist/channels/feishu/card.d.ts +14 -28
- package/dist/channels/feishu/card.js +16 -30
- package/dist/channels/feishu/cloud.d.ts +1 -5
- package/dist/channels/feishu/context-buffer.d.ts +4 -23
- package/dist/channels/feishu/context-buffer.js +8 -31
- package/dist/channels/feishu/crypto.d.ts +10 -5
- package/dist/channels/feishu/crypto.js +12 -19
- package/dist/channels/feishu/feishu-api.d.ts +2 -1
- package/dist/channels/feishu/feishu-api.js +5 -4
- package/dist/channels/feishu/feishu.d.ts +4 -10
- package/dist/channels/feishu/feishu.js +113 -227
- package/dist/channels/feishu/invoke-turn.d.ts +5 -4
- package/dist/channels/feishu/invoke-turn.js +36 -54
- package/dist/channels/feishu/model.d.ts +4 -21
- package/dist/channels/feishu/model.js +1 -8
- package/dist/channels/feishu/normalize.d.ts +2 -10
- package/dist/channels/feishu/normalize.js +7 -42
- package/dist/channels/feishu/parse.d.ts +9 -29
- package/dist/channels/feishu/parse.js +8 -26
- package/dist/channels/feishu/preview.d.ts +12 -14
- package/dist/channels/feishu/preview.js +108 -207
- package/dist/channels/feishu/register-app.d.ts +8 -23
- package/dist/channels/feishu/register-app.js +7 -20
- package/dist/channels/feishu/register-webhook.d.ts +2 -9
- package/dist/channels/feishu/register-webhook.js +9 -33
- package/dist/channels/feishu/scaffold/channel.ts +27 -19
- package/dist/channels/feishu/scaffold/channel.websocket.ts +31 -0
- package/dist/channels/feishu/setup-mode.d.ts +11 -36
- package/dist/channels/feishu/setup-mode.js +7 -26
- package/dist/channels/feishu/ws-ingress.d.ts +1 -2
- package/dist/channels/feishu/ws-ingress.js +6 -8
- package/dist/channels/http.d.ts +15 -15
- package/dist/channels/http.js +19 -9
- package/dist/channels/kit/attachment-path.d.ts +4 -5
- package/dist/channels/kit/attachment-path.js +10 -34
- package/dist/channels/kit/context-buffer.d.ts +14 -21
- package/dist/channels/kit/context-buffer.js +25 -38
- package/dist/channels/kit/delivery.d.ts +35 -0
- package/dist/channels/kit/delivery.js +129 -0
- package/dist/channels/kit/event-stream.d.ts +5 -0
- package/dist/channels/kit/event-stream.js +29 -0
- package/dist/channels/kit/invoke-turn-kit.d.ts +53 -26
- package/dist/channels/kit/invoke-turn-kit.js +89 -32
- package/dist/channels/kit/preview-kit.d.ts +18 -72
- package/dist/channels/kit/preview-kit.js +22 -88
- package/dist/channels/kit/signature.d.ts +3 -9
- package/dist/channels/kit/signature.js +3 -9
- package/dist/channels/kit/state.d.ts +1 -2
- package/dist/channels/kit/state.js +5 -14
- package/dist/channels/kit/stop-command.d.ts +1 -2
- package/dist/channels/kit/stop-command.js +6 -9
- package/dist/channels/kit/tasks.d.ts +1 -1
- package/dist/channels/kit/tasks.js +8 -21
- package/dist/channels/kit/text.d.ts +2 -16
- package/dist/channels/kit/text.js +23 -31
- package/dist/channels/kit/thread-participants.d.ts +4 -17
- package/dist/channels/kit/thread-participants.js +8 -55
- package/dist/channels/kit/transport.d.ts +26 -0
- package/dist/channels/kit/transport.js +20 -0
- package/dist/channels/kit/turn-queue.d.ts +7 -11
- package/dist/channels/kit/turn-queue.js +21 -39
- package/dist/channels/kit/turn-runner.d.ts +32 -18
- package/dist/channels/kit/turn-runner.js +98 -53
- package/dist/channels/kit/turn-store.d.ts +32 -32
- package/dist/channels/kit/turn-store.js +45 -88
- package/dist/channels/lark/lark.d.ts +1 -5
- package/dist/channels/lark/onboard.d.ts +3 -12
- package/dist/channels/lark/onboard.js +3 -13
- package/dist/channels/lark/scaffold/channel.ts +27 -19
- package/dist/channels/lark/scaffold/channel.websocket.ts +31 -0
- package/dist/channels/registration.d.ts +9 -35
- package/dist/channels/registration.js +6 -37
- package/dist/channels/secret.js +2 -5
- package/dist/channels/serve.d.ts +47 -44
- package/dist/channels/serve.js +165 -101
- package/dist/channels/slack/config-api.d.ts +4 -27
- package/dist/channels/slack/config-api.js +5 -34
- package/dist/channels/slack/context-buffer.d.ts +2 -3
- package/dist/channels/slack/context-buffer.js +5 -5
- package/dist/channels/slack/invoke-turn.d.ts +3 -1
- package/dist/channels/slack/invoke-turn.js +25 -34
- package/dist/channels/slack/manifest.js +1 -3
- package/dist/channels/slack/model.d.ts +1 -2
- package/dist/channels/slack/model.js +1 -1
- package/dist/channels/slack/onboard.d.ts +4 -3
- package/dist/channels/slack/onboard.js +8 -11
- package/dist/channels/slack/onboarding-state.d.ts +3 -5
- package/dist/channels/slack/onboarding-state.js +9 -6
- package/dist/channels/slack/parse.d.ts +4 -13
- package/dist/channels/slack/parse.js +7 -28
- package/dist/channels/slack/preview.d.ts +12 -2
- package/dist/channels/slack/preview.js +292 -269
- package/dist/channels/slack/reaction.d.ts +11 -3
- package/dist/channels/slack/reaction.js +19 -8
- package/dist/channels/slack/register-webhook.js +4 -7
- package/dist/channels/slack/scaffold/channel.ts +20 -13
- package/dist/channels/slack/scaffold/slack-send.ts +2 -2
- package/dist/channels/slack/setup-server.d.ts +1 -5
- package/dist/channels/slack/setup-server.js +1 -5
- package/dist/channels/slack/shared-api.d.ts +1 -4
- package/dist/channels/slack/shared-api.js +2 -13
- package/dist/channels/slack/slack-api.d.ts +2 -1
- package/dist/channels/slack/slack-api.js +6 -4
- package/dist/channels/slack/slack.d.ts +11 -14
- package/dist/channels/slack/slack.js +121 -122
- package/dist/channels/sse.d.ts +1 -1
- package/dist/channels/sse.js +1 -1
- package/dist/channels/telegram/context-buffer.d.ts +7 -22
- package/dist/channels/telegram/context-buffer.js +6 -23
- package/dist/channels/telegram/invoke-turn.d.ts +7 -7
- package/dist/channels/telegram/invoke-turn.js +24 -43
- package/dist/channels/telegram/parse.d.ts +24 -51
- package/dist/channels/telegram/parse.js +32 -84
- package/dist/channels/telegram/preview.d.ts +10 -1
- package/dist/channels/telegram/preview.js +79 -100
- package/dist/channels/telegram/register-webhook.js +5 -23
- package/dist/channels/telegram/scaffold/channel.ts +25 -18
- package/dist/channels/telegram/scaffold/telegram-send.ts +6 -4
- package/dist/channels/telegram/telegram-api.d.ts +4 -3
- package/dist/channels/telegram/telegram-api.js +9 -7
- package/dist/channels/telegram/telegram.d.ts +4 -18
- package/dist/channels/telegram/telegram.js +38 -94
- package/dist/channels/telegram/turn-store.d.ts +11 -14
- package/dist/channels/telegram/turn-store.js +8 -6
- package/dist/channels/wait-health.d.ts +4 -6
- package/dist/channels/wait-health.js +10 -14
- package/dist/cli/add-feishu.d.ts +2 -12
- package/dist/cli/add-feishu.js +24 -71
- package/dist/cli/add-slack.d.ts +5 -3
- package/dist/cli/add-slack.js +53 -76
- package/dist/cli/auth-view.d.ts +2 -16
- package/dist/cli/auth-view.js +2 -16
- package/dist/cli/commands/add.js +25 -40
- package/dist/cli/commands/chat.d.ts +0 -1
- package/dist/cli/commands/chat.js +5 -6
- package/dist/cli/commands/deploy/agentcore.js +60 -65
- package/dist/cli/commands/deploy/docker.js +50 -23
- package/dist/cli/commands/deploy/fly.js +23 -61
- package/dist/cli/commands/deploy/railway.js +23 -18
- package/dist/cli/commands/deploy/shared.d.ts +38 -57
- package/dist/cli/commands/deploy/shared.js +40 -59
- package/dist/cli/commands/deploy.d.ts +10 -27
- package/dist/cli/commands/deploy.js +38 -60
- package/dist/cli/commands/destroy.d.ts +4 -0
- package/dist/cli/commands/destroy.js +56 -0
- package/dist/cli/commands/dev.d.ts +2 -1
- package/dist/cli/commands/dev.js +20 -23
- package/dist/cli/commands/info.d.ts +0 -2
- package/dist/cli/commands/info.js +83 -49
- package/dist/cli/commands/init.d.ts +1 -2
- package/dist/cli/commands/init.js +16 -55
- package/dist/cli/commands/invoke.d.ts +0 -1
- package/dist/cli/commands/invoke.js +5 -7
- package/dist/cli/commands/login.d.ts +2 -1
- package/dist/cli/commands/login.js +33 -38
- package/dist/cli/commands/logs.js +3 -4
- package/dist/cli/commands/models.js +1 -5
- package/dist/cli/commands/routine-run.d.ts +6 -0
- package/dist/cli/commands/routine-run.js +47 -0
- package/dist/cli/commands/routine.d.ts +25 -0
- package/dist/cli/commands/routine.js +122 -0
- package/dist/cli/commands/start.d.ts +31 -3
- package/dist/cli/commands/start.js +131 -81
- package/dist/cli/commands/tool.js +26 -12
- package/dist/cli/fail.d.ts +13 -23
- package/dist/cli/fail.js +20 -23
- package/dist/cli/invoke-stream.d.ts +1 -8
- package/dist/cli/kernel.d.ts +6 -38
- package/dist/cli/kernel.js +14 -77
- package/dist/cli/models-view.d.ts +1 -9
- package/dist/cli/models-view.js +11 -22
- package/dist/cli/program.d.ts +5 -6
- package/dist/cli/program.js +120 -157
- package/dist/cli/serve.d.ts +57 -41
- package/dist/cli/serve.js +94 -121
- package/dist/cli/shared.d.ts +15 -61
- package/dist/cli/shared.js +55 -181
- package/dist/cli.js +1 -8
- package/dist/collect.d.ts +6 -0
- package/dist/collect.js +6 -0
- package/dist/core.d.ts +4 -1
- package/dist/core.js +9 -7
- package/dist/declared-secrets.d.ts +55 -0
- package/dist/declared-secrets.js +88 -0
- package/dist/deploy/agentcore/aws-cli.d.ts +91 -0
- package/dist/deploy/agentcore/aws-cli.js +64 -0
- package/dist/deploy/agentcore/destroy.d.ts +51 -0
- package/dist/deploy/agentcore/destroy.js +273 -0
- package/dist/deploy/agentcore/forwarder.js +28 -79
- package/dist/deploy/agentcore/logs.d.ts +2 -18
- package/dist/deploy/agentcore/logs.js +27 -51
- package/dist/deploy/agentcore/plan.d.ts +85 -105
- package/dist/deploy/agentcore/plan.js +152 -314
- package/dist/deploy/agentcore/run.d.ts +28 -28
- package/dist/deploy/agentcore/run.js +227 -306
- package/dist/deploy/agentcore/zip.d.ts +2 -15
- package/dist/deploy/agentcore/zip.js +3 -17
- package/dist/deploy/build-context.d.ts +21 -0
- package/dist/deploy/build-context.js +137 -0
- package/dist/deploy/channel-ingress.d.ts +26 -39
- package/dist/deploy/channel-ingress.js +57 -28
- package/dist/deploy/container.d.ts +20 -34
- package/dist/deploy/container.js +49 -100
- package/dist/deploy/docker/plan.d.ts +12 -11
- package/dist/deploy/docker/plan.js +43 -24
- package/dist/deploy/docker/run.d.ts +22 -25
- package/dist/deploy/docker/run.js +39 -37
- package/dist/deploy/fly/plan.d.ts +15 -44
- package/dist/deploy/fly/plan.js +41 -73
- package/dist/deploy/fly/run.d.ts +16 -51
- package/dist/deploy/fly/run.js +42 -95
- package/dist/deploy/hosts.d.ts +24 -3
- package/dist/deploy/hosts.js +24 -3
- package/dist/deploy/preflight.d.ts +47 -41
- package/dist/deploy/preflight.js +282 -383
- package/dist/deploy/railway/plan.d.ts +16 -42
- package/dist/deploy/railway/plan.js +42 -60
- package/dist/deploy/railway/run.d.ts +30 -68
- package/dist/deploy/railway/run.js +86 -100
- package/dist/deploy/registration-gate.d.ts +6 -13
- package/dist/deploy/residency.d.ts +37 -0
- package/dist/deploy/residency.js +24 -0
- package/dist/deploy/runner.d.ts +10 -9
- package/dist/deploy/runner.js +7 -18
- package/dist/deploy/secrets.d.ts +40 -41
- package/dist/deploy/secrets.js +117 -103
- package/dist/deploy/workspace.d.ts +50 -0
- package/dist/deploy/workspace.js +197 -0
- package/dist/dev-supervisor.d.ts +1 -9
- package/dist/dev-supervisor.js +21 -64
- package/dist/effect-port.d.ts +50 -0
- package/dist/effect-port.js +77 -0
- package/dist/engines/pi/agent-session-factory.d.ts +50 -69
- package/dist/engines/pi/agent-session-factory.js +249 -146
- package/dist/engines/pi/auth.d.ts +15 -11
- package/dist/engines/pi/auth.js +96 -99
- package/dist/engines/pi/chat.d.ts +1 -5
- package/dist/engines/pi/chat.js +2 -14
- package/dist/engines/pi/config.d.ts +53 -51
- package/dist/engines/pi/config.js +83 -92
- package/dist/engines/pi/create.d.ts +60 -75
- package/dist/engines/pi/create.js +108 -141
- package/dist/engines/pi/definition.d.ts +14 -58
- package/dist/engines/pi/definition.js +21 -85
- package/dist/engines/pi/invoke-session.d.ts +5 -25
- package/dist/engines/pi/invoke-session.js +233 -244
- package/dist/engines/pi/login.d.ts +95 -35
- package/dist/engines/pi/login.js +259 -107
- package/dist/engines/pi/machine.d.ts +31 -0
- package/dist/engines/pi/machine.js +106 -0
- package/dist/engines/pi/models.d.ts +70 -89
- package/dist/engines/pi/models.js +188 -81
- package/dist/engines/pi/open.d.ts +65 -53
- package/dist/engines/pi/open.js +129 -69
- package/dist/engines/pi/report.d.ts +3 -12
- package/dist/engines/pi/report.js +2 -16
- package/dist/engines/pi/search-tools.d.ts +1 -9
- package/dist/engines/pi/search-tools.js +26 -57
- package/dist/engines/pi/service.d.ts +2 -16
- package/dist/engines/pi/service.js +2 -16
- package/dist/engines/pi/session-builder.d.ts +5 -9
- package/dist/engines/pi/session-builder.js +44 -82
- package/dist/engines/pi/session-control.js +286 -269
- package/dist/engines/pi/session-effects.d.ts +14 -0
- package/dist/engines/pi/session-effects.js +30 -0
- package/dist/engines/pi/session-inheritance.d.ts +9 -18
- package/dist/engines/pi/session-inheritance.js +103 -160
- package/dist/engines/pi/session-markers.d.ts +31 -30
- package/dist/engines/pi/session-markers.js +26 -25
- package/dist/engines/pi/session-settings.d.ts +6 -22
- package/dist/engines/pi/session-settings.js +10 -21
- package/dist/engines/pi/session-store.d.ts +26 -92
- package/dist/engines/pi/session-store.js +96 -310
- package/dist/engines/pi/tool-context.d.ts +17 -45
- package/dist/engines/pi/tool-context.js +7 -26
- package/dist/engines/pi/tool.d.ts +45 -36
- package/dist/engines/pi/tool.js +43 -57
- package/dist/engines/pi/turn-kit.d.ts +14 -52
- package/dist/engines/pi/turn-kit.js +18 -67
- package/dist/engines/pi/wake-tool.d.ts +2 -10
- package/dist/engines/pi/wake-tool.js +7 -27
- package/dist/env.d.ts +27 -28
- package/dist/env.js +45 -50
- package/dist/index.js +0 -2
- package/dist/lark.d.ts +1 -2
- package/dist/lark.js +1 -2
- package/dist/loader.d.ts +14 -55
- package/dist/loader.js +24 -43
- package/dist/log.d.ts +2 -12
- package/dist/log.js +3 -24
- package/dist/node.d.ts +2 -15
- package/dist/node.js +2 -18
- package/dist/observe.d.ts +2 -8
- package/dist/open-url.d.ts +1 -1
- package/dist/open-url.js +1 -1
- package/dist/paths.d.ts +70 -134
- package/dist/paths.js +165 -232
- package/dist/pi.d.ts +4 -6
- package/dist/pi.js +3 -3
- package/dist/proxy.d.ts +16 -6
- package/dist/proxy.js +52 -16
- package/dist/runtime.d.ts +18 -12
- package/dist/runtime.js +18 -11
- package/dist/scaffold/add-channel.d.ts +14 -31
- package/dist/scaffold/add-channel.js +48 -112
- package/dist/scaffold/init.d.ts +9 -31
- package/dist/scaffold/init.js +45 -138
- package/dist/scaffold/templates/env.example +6 -4
- package/dist/scaffold/templates/fastagent.config.ts +36 -0
- package/dist/scaffold/templates.d.ts +3 -3
- package/dist/scaffold/templates.js +5 -8
- package/dist/scaffold/vendor-skill.d.ts +2 -5
- package/dist/scaffold/vendor-skill.js +19 -29
- package/dist/schedule/cron.d.ts +9 -5
- package/dist/schedule/cron.js +9 -11
- package/dist/schedule/discover.d.ts +12 -12
- package/dist/schedule/discover.js +43 -34
- package/dist/schedule/routine.d.ts +43 -0
- package/dist/schedule/routine.js +32 -0
- package/dist/schedule/run.d.ts +55 -0
- package/dist/schedule/run.js +146 -0
- package/dist/schedule/scheduler.d.ts +44 -55
- package/dist/schedule/scheduler.js +291 -265
- package/dist/schedule/state.d.ts +105 -10
- package/dist/schedule/state.js +293 -19
- package/dist/schedule/wake-alarm.d.ts +10 -23
- package/dist/schedule/wake-alarm.js +91 -121
- package/dist/schedule/wakeups.d.ts +23 -39
- package/dist/schedule/wakeups.js +33 -75
- package/dist/secrets-gate.d.ts +32 -0
- package/dist/secrets-gate.js +33 -0
- package/dist/service.d.ts +137 -98
- package/dist/service.js +250 -252
- package/dist/session-remote.d.ts +33 -44
- package/dist/session-remote.js +203 -192
- package/dist/session.d.ts +166 -229
- package/dist/session.js +33 -55
- package/dist/tunnel.d.ts +8 -36
- package/dist/tunnel.js +24 -65
- package/dist/version.d.ts +2 -2
- package/dist/version.js +2 -2
- package/package.json +10 -16
- package/dist/channels/agentcore-state.d.ts +0 -77
- package/dist/channels/agentcore-state.js +0 -276
- package/dist/channels/github/github.d.ts +0 -31
- package/dist/channels/github/github.js +0 -89
- package/dist/channels/github/scaffold/channel.ts +0 -25
- package/dist/cli/commands/attach.d.ts +0 -108
- package/dist/cli/commands/attach.js +0 -637
- package/dist/cli/commands/fire.d.ts +0 -7
- package/dist/cli/commands/fire.js +0 -38
- package/dist/cli/commands/schedule.d.ts +0 -12
- package/dist/cli/commands/schedule.js +0 -87
- package/dist/github.d.ts +0 -3
- package/dist/github.js +0 -2
- package/dist/scaffold/templates/fastagent.config.mjs +0 -23
- package/dist/schedule/audit.d.ts +0 -18
- package/dist/schedule/audit.js +0 -57
- package/dist/schedule/schedule.d.ts +0 -35
- package/dist/schedule/schedule.js +0 -23
package/dist/service.js
CHANGED
|
@@ -1,50 +1,83 @@
|
|
|
1
|
-
|
|
1
|
+
/** The product, as one call: an agent directory becomes a live service. */
|
|
2
|
+
import * as Cause from "effect/Cause";
|
|
3
|
+
import * as Effect from "effect/Effect";
|
|
4
|
+
import * as Exit from "effect/Exit";
|
|
5
|
+
import * as Fiber from "effect/Fiber";
|
|
6
|
+
import * as Scope from "effect/Scope";
|
|
7
|
+
import { createControlPlane } from "./channels/control.js";
|
|
2
8
|
import { createInvokeHandler } from "./channels/http.js";
|
|
3
9
|
import { text } from "./channels/respond.js";
|
|
4
|
-
import { parseRouteKey, pathUnderPrefix, router } from "./channels/serve.js";
|
|
10
|
+
import { assertCorsOrigins, parseRouteKey, pathUnderPrefix, router } from "./channels/serve.js";
|
|
5
11
|
import { loadChannels } from "./channels/discover.js";
|
|
6
|
-
import {
|
|
12
|
+
import { loadRoutines } from "./schedule/discover.js";
|
|
13
|
+
import { createRoutineListHandler, createRunHandler } from "./schedule/run.js";
|
|
7
14
|
import { createScheduler } from "./schedule/scheduler.js";
|
|
8
15
|
import { log } from "./log.js";
|
|
9
|
-
import {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
* error from arriving. The CLI passes a shorter one: its own forced exit must come AFTER this, or
|
|
13
|
-
* the process leaves at 0 before the failure is known. */
|
|
16
|
+
import { refuseBrokenDeclarations } from "./loader.js";
|
|
17
|
+
import { gateSecrets } from "./secrets-gate.js";
|
|
18
|
+
/** Default wait for a channel's `closed` before reporting it stuck. */
|
|
14
19
|
const CLOSE_DEADLINE_MS = 5_000;
|
|
15
|
-
/**
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
20
|
+
/** One deadline for all connections; a failed close must not hide a sibling that is still running. */
|
|
21
|
+
function closeWithin(runs, names, deadlineMs) {
|
|
22
|
+
return Effect.gen(function* () {
|
|
23
|
+
const pending = new Set(runs.map((_, i) => i));
|
|
24
|
+
const failures = [];
|
|
25
|
+
yield* Effect.forEach(runs, (run, i) => Effect.tryPromise({ try: () => run.closed, catch: (error) => error }).pipe(Effect.match({
|
|
26
|
+
onSuccess: () => {
|
|
27
|
+
pending.delete(i);
|
|
28
|
+
},
|
|
29
|
+
onFailure: (error) => {
|
|
30
|
+
pending.delete(i);
|
|
31
|
+
failures.push(error);
|
|
32
|
+
},
|
|
33
|
+
})), { concurrency: "unbounded", discard: true }).pipe(Effect.timeoutOrElse({ duration: deadlineMs, orElse: () => Effect.void }));
|
|
34
|
+
if (pending.size > 0) {
|
|
35
|
+
const stuck = [...pending].map((i) => names[i] ?? "channel");
|
|
36
|
+
return yield* Effect.fail(new Error(`long connection(s) did not stop within ${deadlineMs}ms: ${stuck.join(", ")}`));
|
|
37
|
+
}
|
|
38
|
+
if (failures.length > 0) {
|
|
39
|
+
return yield* Effect.fail(failures.length === 1 ? failures[0] : new AggregateError(failures, "long connections failed to close"));
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
/** The port a serve binds when neither a flag, `PORT` (start only) nor `http.port` names one. */
|
|
44
|
+
export const DEFAULT_HTTP_PORT = 8787;
|
|
45
|
+
/**
|
|
46
|
+
* Does this surface serve `POST /run`, given a routine to run? It FOLLOWS `invoke` unless `run` says otherwise (see
|
|
47
|
+
* {@link HttpSurface.run}). ONE function because the assembly mounts the route and `preflightDeploy` names it; a
|
|
48
|
+
* default spelled twice is a default that moves once.
|
|
49
|
+
*/
|
|
50
|
+
export function shouldServeRun(http) {
|
|
51
|
+
return http?.run ?? http?.invoke !== false;
|
|
42
52
|
}
|
|
43
53
|
/**
|
|
44
|
-
*
|
|
45
|
-
*
|
|
54
|
+
* What an anonymous caller of a port can do, worst first — the ONE wording the startup report and deploy's pre-flight
|
|
55
|
+
* both print, each from its own facts (what mounted vs what will mount).
|
|
46
56
|
*/
|
|
47
|
-
export
|
|
57
|
+
export function describeAnonymousSurface(open) {
|
|
58
|
+
return [
|
|
59
|
+
...(open.invoke ? ["POST /invoke (run a turn with this agent's tools)"] : []),
|
|
60
|
+
...(open.run ? ["POST /run (run any routine this agent declares; GET /routines lists them)"] : []),
|
|
61
|
+
...(open.controlPrefix ? [`${open.controlPrefix}/* (read, steer or delete any session)`] : []),
|
|
62
|
+
];
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* The surface this deployment serves: the DATA plane (`POST /invoke`), `GET /health`, and the discovered channels.
|
|
66
|
+
*
|
|
67
|
+
* `/invoke` is the framework's interface, so it is served whatever channels a definition declares (unless
|
|
68
|
+
* `http.invoke: false`). It is RESERVED for the same reason `/control/*` is: a channel taking that path would silently replace the one route every client,
|
|
69
|
+
* every doc and the startup line all name. `/health` stays overridable — a probe is the deployment's to shape.
|
|
70
|
+
*/
|
|
71
|
+
export async function routesFor(agentDir, agent, stateRoot, control,
|
|
72
|
+
/**
|
|
73
|
+
* `http.invoke: false` withholds the data plane. Two callers, two reasons: the AgentCore adapter serves the
|
|
74
|
+
* Runtime's `/invocations` contract instead, and an author sets it to leave the channels' signature checks as the
|
|
75
|
+
* only way into a public port.
|
|
76
|
+
*
|
|
77
|
+
* `routines` mounts `POST /run` and `GET /routines` (per {@link shouldServeRun}). Passed in rather than loaded here
|
|
78
|
+
* so the routes and the resident clock cannot disagree about which routines exist (`loadServingRoutines`).
|
|
79
|
+
*/
|
|
80
|
+
options = {}) {
|
|
48
81
|
const { routes, longConnections, routeChannels, collisions, failures } = await loadChannels(agentDir, {
|
|
49
82
|
agent,
|
|
50
83
|
stateRoot,
|
|
@@ -53,40 +86,62 @@ export async function routesFor(agentDir, agent, stateRoot, control, options = {
|
|
|
53
86
|
for (const c of collisions) {
|
|
54
87
|
log.warn(`[fastagent] channel route "${c.route}" (${c.source}) collides with an earlier channel — not mounted`);
|
|
55
88
|
}
|
|
56
|
-
|
|
57
|
-
if (
|
|
58
|
-
throw new Error(
|
|
59
|
-
`fix it, or rename an intentionally disabled file to *.disabled`);
|
|
89
|
+
refuseBrokenDeclarations(failures);
|
|
90
|
+
if (collisions.length > 0) {
|
|
91
|
+
throw new Error(`${collisions.length} channel route collision(s) — two channels cannot serve one route`);
|
|
60
92
|
}
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
93
|
+
/**
|
|
94
|
+
* Which channel key already serves this path for the method a built-in would answer — the CHANNEL's spelling, not
|
|
95
|
+
* ours. A channel may write the method-less `"/invoke"`, and an error naming `"POST /invoke"` sends its author
|
|
96
|
+
* grepping their own file for a string that is not in it.
|
|
97
|
+
*/
|
|
98
|
+
const covered = (path, method) => Object.keys(routes).find((key) => {
|
|
64
99
|
const entry = parseRouteKey(key);
|
|
65
|
-
return entry.path ===
|
|
100
|
+
return entry.path === path && (entry.method === undefined || entry.method === method);
|
|
66
101
|
});
|
|
67
102
|
let ready = longConnections.length === 0;
|
|
68
103
|
const health = () => (ready ? text("ok\n", 200) : text("starting\n", 503));
|
|
104
|
+
// AgentCore serves the data plane through the Runtime's own `/invocations` contract, so the adapter opts out — and
|
|
105
|
+
// with no `/invoke` of ours on that surface there is nothing to reserve, which is why the refusal is in here.
|
|
106
|
+
const unverified = { ...(covered("/health", "GET") ? {} : { "GET /health": health }) };
|
|
107
|
+
if (options.http?.invoke !== false)
|
|
108
|
+
unverified["POST /invoke"] = createInvokeHandler(agent);
|
|
109
|
+
// `POST /run` exists only where there is something to run, and `GET /routines` exactly where that route does:
|
|
110
|
+
// it is the catalogue OF that route, so listing names nobody can use would be a catalogue of nothing. Both ride
|
|
111
|
+
// this table for the reason the table exists: they authenticate nobody, so they inherit the JSON body gate, the
|
|
112
|
+
// cross-origin policy, the reserved path and the startup report's account of what is open.
|
|
113
|
+
if (shouldServeRun(options.http)) {
|
|
114
|
+
const routines = options.routines ?? [];
|
|
115
|
+
const run = createRunHandler({ agent, routines });
|
|
116
|
+
if (run)
|
|
117
|
+
unverified["POST /run"] = run;
|
|
118
|
+
const list = createRoutineListHandler({ routines });
|
|
119
|
+
if (list)
|
|
120
|
+
unverified["GET /routines"] = list;
|
|
121
|
+
}
|
|
122
|
+
// ONE rule over the whole table, so the next route we add is reserved by existing here rather than by someone
|
|
123
|
+
// remembering to write a second check for it. `/health` is exempt by construction: it is only in `ours` when no
|
|
124
|
+
// channel already serves it, because a probe is the deployment's to shape.
|
|
125
|
+
const taken = Object.keys(unverified).flatMap((key) => {
|
|
126
|
+
const entry = parseRouteKey(key);
|
|
127
|
+
const channelKey = covered(entry.path, entry.method ?? "");
|
|
128
|
+
return channelKey === undefined ? [] : [{ channelKey, key }];
|
|
129
|
+
});
|
|
130
|
+
if (taken.length > 0) {
|
|
131
|
+
throw new Error(`channel route(s) ${taken.map((t) => `"${t.channelKey}"`).join(", ")} take a path this serve answers on ` +
|
|
132
|
+
`itself (${taken.map((t) => `"${t.key}"`).join(", ")}) — rename the channel route`);
|
|
133
|
+
}
|
|
69
134
|
return {
|
|
70
|
-
|
|
135
|
+
unverified,
|
|
136
|
+
selfVerifying: routes,
|
|
71
137
|
longConnections,
|
|
72
138
|
routeChannels,
|
|
73
|
-
builtinInvoke,
|
|
74
139
|
setReady(value) {
|
|
75
140
|
ready = value;
|
|
76
141
|
},
|
|
77
142
|
};
|
|
78
143
|
}
|
|
79
|
-
/**
|
|
80
|
-
* Refuse channel routes the control plane would swallow.
|
|
81
|
-
*
|
|
82
|
-
* `router` refuses this too, and would catch it a moment later; this exists for the sentence, not
|
|
83
|
-
* the check — an author who lands a route under `/control` needs to hear about `sessionControl`,
|
|
84
|
-
* which the host has no way to mention. Both ask {@link pathUnderPrefix}, so there is one rule with
|
|
85
|
-
* two wordings, not two rules.
|
|
86
|
-
*
|
|
87
|
-
* Called from BOTH mount points: here at boot, and again on agentcore's lazy path, whose channels
|
|
88
|
-
* load after this ran against an empty base.
|
|
89
|
-
*/
|
|
144
|
+
/** Refuse channel routes the control plane would swallow. */
|
|
90
145
|
export function assertNoControlPlaneCollision(channelRoutes, plane) {
|
|
91
146
|
const collisions = Object.keys(channelRoutes).filter((key) => pathUnderPrefix(parseRouteKey(key).path, plane.prefix));
|
|
92
147
|
if (collisions.length > 0) {
|
|
@@ -94,217 +149,160 @@ export function assertNoControlPlaneCollision(channelRoutes, plane) {
|
|
|
94
149
|
`rename the channel route or disable sessionControl in fastagent.config`);
|
|
95
150
|
}
|
|
96
151
|
}
|
|
97
|
-
export function mountSessionControl(routes, control
|
|
152
|
+
export function mountSessionControl(routes, control) {
|
|
98
153
|
if (!control)
|
|
99
154
|
return { routes, mounts: [] };
|
|
100
|
-
|
|
101
|
-
// permissions, which works because both holders share a filesystem. A deployment removes that
|
|
102
|
-
// premise — a token minted in the container is replaced every restart and reachable only by shelling
|
|
103
|
-
// in — so there the deployer mints it and injects it here, like the wake/ingress secrets.
|
|
104
|
-
// Trimmed on read, like `.env` values already are: a token pasted from a dashboard with a trailing
|
|
105
|
-
// newline would otherwise become the box's token verbatim, and every caller holding the clean value
|
|
106
|
-
// gets a bare 401 — the undiagnosable symptom, one character wide.
|
|
107
|
-
const injected = process.env[CONTROL_TOKEN_ENV]?.trim();
|
|
108
|
-
// SET BUT EMPTY is the deployed default, not an edge case: the generated Compose topology writes
|
|
109
|
-
// every secret as `NAME: "${NAME:-}"`, so an operator who skipped this one lands here. Falling back
|
|
110
|
-
// silently would leave exactly the symptom the injection exists to remove — a token the caller does
|
|
111
|
-
// not have — with nothing in the log to tell it apart from a deployment that never asked.
|
|
112
|
-
if (injected === "") {
|
|
113
|
-
log.warn(`[fastagent] ${CONTROL_TOKEN_ENV} is set but empty — minting a per-boot token instead; callers holding ` +
|
|
114
|
-
"the deploy-time value will get 401 (set it, or read the minted one from control.json on the box)");
|
|
115
|
-
}
|
|
116
|
-
else if (injected !== undefined && injected.length < 16) {
|
|
117
|
-
// Length is a crude proxy for entropy — sixteen `a`s pass. It is aimed at `changeme`, which this
|
|
118
|
-
// change makes newly dangerous: the plane went from unusable-in-a-deployment to usable by whoever
|
|
119
|
-
// holds this string, and the empty case is the only other thing that says anything.
|
|
120
|
-
log.warn(`[fastagent] ${CONTROL_TOKEN_ENV} is ${injected.length} characters — it is the ONLY thing between ` +
|
|
121
|
-
"/control/* (steer, stop, rewrite a session) and anyone who can reach the port; use a random value (uuidgen)");
|
|
122
|
-
}
|
|
123
|
-
const token = injected || crypto.randomUUID();
|
|
124
|
-
const plane = createControlPlane(control, { token, agent: options.agent });
|
|
155
|
+
const plane = createControlPlane(control);
|
|
125
156
|
assertNoControlPlaneCollision(routes, plane);
|
|
126
|
-
return { routes, mounts: [plane],
|
|
157
|
+
return { routes, mounts: [plane], controlPrefix: plane.prefix };
|
|
127
158
|
}
|
|
128
159
|
/**
|
|
129
|
-
* Load
|
|
130
|
-
*
|
|
131
|
-
*
|
|
132
|
-
*
|
|
160
|
+
* Load the agent's `routines/`, gated.
|
|
161
|
+
*
|
|
162
|
+
* Separate from {@link startSchedules} because two things need the SAME list and one of them runs first: the
|
|
163
|
+
* `POST /run` route is built with the routes (so a channel cannot take that path unnoticed), and the resident
|
|
164
|
+
* clock starts later, inside the service's scope. Loading twice would let them disagree about which routines exist.
|
|
165
|
+
*/
|
|
166
|
+
export async function loadServingRoutines(agentDir) {
|
|
167
|
+
// Thrown, not exited on: this runs inside an embedder's app as well as the CLI, and a library that calls
|
|
168
|
+
// process.exit takes a decision (degrade? retry? stop?) that belongs to its host.
|
|
169
|
+
const { routines, secrets, failures } = await loadRoutines(agentDir);
|
|
170
|
+
// The routines' half of the serving-path gate (the opener does the tools', loadChannels the
|
|
171
|
+
// channels'): a routine reads its env at IMPORT time, so an unset declared value has already
|
|
172
|
+
// produced a broken prompt — refusing here is the last point where that is a startup failure.
|
|
173
|
+
gateSecrets({ declared: secrets, failures });
|
|
174
|
+
refuseBrokenDeclarations(failures);
|
|
175
|
+
return routines;
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Start the clock over routines {@link loadServingRoutines} already gated — on every serve, routines or not: the
|
|
179
|
+
* agent's own wake-ups are a default capability, and this is what fires them.
|
|
133
180
|
*/
|
|
134
|
-
export
|
|
135
|
-
|
|
136
|
-
// that calls process.exit takes a decision (degrade? retry? stop?) that belongs to its host. The
|
|
137
|
-
// CLI catches at its own boundary.
|
|
138
|
-
const { schedules, failures } = await loadSchedules(agentDir);
|
|
139
|
-
reportModuleLoadFailures(failures);
|
|
140
|
-
if (schedules.length === 0 && !selfSchedule)
|
|
141
|
-
return { schedules, stop: () => { } };
|
|
142
|
-
const scheduler = createScheduler({ agent, stateRoot, schedules, externalClock: options.externalClock });
|
|
181
|
+
export function startSchedules(agent, stateRoot, routines, options = {}) {
|
|
182
|
+
const scheduler = Effect.runSync(createScheduler({ agent, stateRoot, routines, externalClock: options.externalClock }));
|
|
143
183
|
scheduler.start();
|
|
144
|
-
if (
|
|
145
|
-
log.info(`[fastagent]
|
|
184
|
+
if (routines.length > 0) {
|
|
185
|
+
log.info(`[fastagent] routines: ${routines.map((r) => r.name).join(", ")}${options.externalClock ? " (external clock — no resident cron timers)" : ""}`);
|
|
146
186
|
}
|
|
147
|
-
|
|
148
|
-
// as the CLI, and a library that installs SIGINT handlers is deciding something that is not its
|
|
149
|
-
// to decide. `runStart`/`runDev` wire it to their own shutdown.
|
|
150
|
-
return { schedules, stop: () => scheduler.stop() };
|
|
187
|
+
return { routines, stop: () => scheduler.stop() };
|
|
151
188
|
}
|
|
152
189
|
/**
|
|
153
|
-
* The assembly itself, over an already-opened directory: channels, the control plane,
|
|
154
|
-
*
|
|
155
|
-
*
|
|
156
|
-
* {@link createAgentService} is this plus opening the directory. `dev`/`start` open separately — their
|
|
157
|
-
* startup report needs the opened values before anything mounts — and then arrive here, so there is
|
|
158
|
-
* one assembly rather than one per caller.
|
|
190
|
+
* The assembly itself, over an already-opened directory: channels, the control plane, routines and long connections,
|
|
191
|
+
* composed into one handler.
|
|
159
192
|
*/
|
|
160
193
|
export async function mountAgentService(opened, options = {}) {
|
|
161
194
|
const { agentDir, workspace, stateRoot, sessionControl } = opened;
|
|
162
|
-
// Wrapped BEFORE anything consumes it: routes, the control plane and
|
|
163
|
-
//
|
|
195
|
+
// Wrapped BEFORE anything consumes it: routes, the control plane and routines must all drive the same agent, so
|
|
196
|
+
// this is a hook rather than something a caller applies afterwards.
|
|
164
197
|
const agent = options.wrapAgent?.(opened.agent) ?? opened.agent;
|
|
165
198
|
const closeTimeoutMs = options.closeTimeoutMs ?? CLOSE_DEADLINE_MS;
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
//
|
|
172
|
-
|
|
173
|
-
const
|
|
174
|
-
const
|
|
175
|
-
|
|
176
|
-
//
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
const close = () => {
|
|
194
|
-
// Awaits the connections rather than only signalling them: `close()` promises they are stopped,
|
|
195
|
-
// and a caller tearing down a test or a request-scoped service needs that to be true on return.
|
|
196
|
-
closing ??= (async () => {
|
|
197
|
-
abort.abort();
|
|
198
|
-
scheduled.stop();
|
|
199
|
-
options.signal?.removeEventListener("abort", onAbort);
|
|
200
|
-
// A failure to stop is the caller's to know about — swallowing it would let `close()` report
|
|
201
|
-
// success over a channel still holding on. Bounded, because a channel that ignores its abort
|
|
202
|
-
// signal must not hang the teardown either.
|
|
203
|
-
const { stuck, failures } = await closeWithin(runs, routed.longConnections.map((c) => c.name), closeTimeoutMs);
|
|
204
|
-
if (stuck.length > 0) {
|
|
205
|
-
throw new Error(`long connection(s) did not stop within ${closeTimeoutMs}ms: ${stuck.join(", ")}`);
|
|
206
|
-
}
|
|
207
|
-
if (failures.length > 0) {
|
|
208
|
-
throw failures.length === 1 ? failures[0] : new AggregateError(failures, "long connections failed to close");
|
|
209
|
-
}
|
|
210
|
-
})();
|
|
211
|
-
return closing;
|
|
212
|
-
};
|
|
213
|
-
// Rollback IS close(), plus keeping the original error: a failure to clean up is the aftermath,
|
|
214
|
-
// and replacing the reason the caller needs with it hides the actual cause.
|
|
215
|
-
const rollback = async (error) => {
|
|
216
|
-
await close().catch((closeError) => log.error(`[fastagent] cleanup after a failed start also failed: ${String(closeError)}`));
|
|
217
|
-
throw error;
|
|
218
|
-
};
|
|
219
|
-
// Rolled back on failure: a connection that throws while the ones before it are open, and the
|
|
220
|
-
// scheduler already ticking, would otherwise leave both running behind a rejected open().
|
|
221
|
-
for (const connection of routed.longConnections) {
|
|
222
|
-
let run;
|
|
223
|
-
try {
|
|
224
|
-
run = connection.connect(abort.signal);
|
|
225
|
-
}
|
|
226
|
-
catch (error) {
|
|
227
|
-
return rollback(error);
|
|
228
|
-
}
|
|
229
|
-
if (typeof run?.ready?.then !== "function" || typeof run?.closed?.then !== "function") {
|
|
230
|
-
return rollback(new Error(`${connection.name} connect(signal) must return { ready: Promise, closed: Promise }`));
|
|
231
|
-
}
|
|
232
|
-
void run.closed.then(() => {
|
|
233
|
-
if (abort.signal.aborted)
|
|
234
|
-
return;
|
|
235
|
-
// A channel that dies leaves the service serving something it no longer has.
|
|
236
|
-
dropped = true;
|
|
237
|
-
routed.setReady(false);
|
|
238
|
-
onClosed(connection.name);
|
|
239
|
-
}, (error) => {
|
|
199
|
+
// The embedder's way in, checked like the config file's: `allowedOrigin` compares exact strings, so a stray
|
|
200
|
+
// trailing slash would refuse the front end with nothing naming the rule.
|
|
201
|
+
const corsOrigins = opened.http?.cors;
|
|
202
|
+
if (corsOrigins)
|
|
203
|
+
assertCorsOrigins(corsOrigins, "mountAgentService: http.cors");
|
|
204
|
+
// Loaded BEFORE the routes, because `POST /run` is one of them and the resident clock below must run over the
|
|
205
|
+
// same list — two loads would be two answers to "which routines exist".
|
|
206
|
+
const routines = await loadServingRoutines(agentDir);
|
|
207
|
+
const routed = await routesFor(agentDir, agent, stateRoot, sessionControl, { http: opened.http, routines });
|
|
208
|
+
const withControl = mountSessionControl(routed.selfVerifying, opened.publishControl ? sessionControl : undefined);
|
|
209
|
+
// Composed BEFORE anything starts.
|
|
210
|
+
const handler = router({
|
|
211
|
+
unverified: routed.unverified,
|
|
212
|
+
selfVerifying: withControl.routes,
|
|
213
|
+
mounts: withControl.mounts,
|
|
214
|
+
...(corsOrigins ? { corsOrigins } : {}),
|
|
215
|
+
});
|
|
216
|
+
return Effect.runPromise(Effect.gen(function* () {
|
|
217
|
+
const lifetime = yield* Scope.make();
|
|
218
|
+
const abort = new AbortController();
|
|
219
|
+
const runs = [];
|
|
220
|
+
const names = routed.longConnections.map((c) => c.name);
|
|
221
|
+
// A connection lost during startup must not be hidden by a sibling becoming ready later.
|
|
222
|
+
let dropped = false;
|
|
223
|
+
const onClosed = options.onChannelClosed ??
|
|
224
|
+
((name, error) => log.error(`[fastagent] long connection ${name} ${error === undefined ? "closed" : `failed: ${String(error)}`}`));
|
|
225
|
+
const notifyClosed = (name, error) => {
|
|
240
226
|
if (abort.signal.aborted)
|
|
241
227
|
return;
|
|
242
228
|
dropped = true;
|
|
243
229
|
routed.setReady(false);
|
|
244
|
-
onClosed(
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
230
|
+
onClosed(name, error);
|
|
231
|
+
};
|
|
232
|
+
// Scope.close alone does not join concurrent closes or retain their failure.
|
|
233
|
+
const shutdown = yield* Effect.cached(Effect.gen(function* () {
|
|
234
|
+
abort.abort();
|
|
235
|
+
yield* Scope.close(lifetime, Exit.void);
|
|
236
|
+
}));
|
|
237
|
+
const close = () => Effect.runPromise(shutdown);
|
|
238
|
+
const rollback = shutdown.pipe(Effect.catchCause((cause) => Effect.sync(() => log.error(`[fastagent] cleanup after a failed start also failed: ${String(Cause.squash(cause))}`))));
|
|
239
|
+
const onAbort = () => {
|
|
240
|
+
void close().catch((error) => log.error(`[fastagent] service close failed: ${String(error)}`));
|
|
241
|
+
};
|
|
242
|
+
return yield* Effect.gen(function* () {
|
|
243
|
+
// Registered first, so subscriptions and schedule timers stop before waiting for transports.
|
|
244
|
+
yield* Effect.addFinalizer(() => closeWithin(runs, names, closeTimeoutMs).pipe(Effect.orDie));
|
|
245
|
+
const scheduled = yield* Effect.acquireRelease(Effect.try({
|
|
246
|
+
try: () => startSchedules(agent, stateRoot, routines),
|
|
247
|
+
catch: (error) => error,
|
|
248
|
+
}), (scheduled) => Effect.sync(scheduled.stop));
|
|
249
|
+
const readiness = yield* Effect.forEach(routed.longConnections, (connection) => Effect.gen(function* () {
|
|
250
|
+
const run = yield* Effect.try({
|
|
251
|
+
try: () => connection.connect(abort.signal),
|
|
252
|
+
catch: (error) => error,
|
|
253
|
+
});
|
|
254
|
+
if (typeof run?.ready?.then !== "function" || typeof run?.closed?.then !== "function") {
|
|
255
|
+
return yield* Effect.fail(new Error(`${connection.name} connect(signal) must return { ready: Promise, closed: Promise }`));
|
|
256
|
+
}
|
|
257
|
+
runs.push(run);
|
|
258
|
+
const closed = Effect.tryPromise({ try: () => run.closed, catch: (error) => error });
|
|
259
|
+
yield* closed.pipe(Effect.match({
|
|
260
|
+
onSuccess: () => notifyClosed(connection.name),
|
|
261
|
+
onFailure: (error) => notifyClosed(connection.name, error),
|
|
262
|
+
}), Effect.catchCause((cause) => Effect.sync(() => log.error(`[fastagent] channel closure callback failed: ${String(Cause.squash(cause))}`))), Effect.forkScoped({ startImmediately: true }));
|
|
263
|
+
// Observe ready immediately, including when a later connect() throws and mount rolls back.
|
|
264
|
+
return yield* Effect.raceFirst(Effect.tryPromise({ try: () => run.ready, catch: (error) => error }), closed.pipe(Effect.matchEffect({
|
|
265
|
+
onSuccess: () => Effect.fail(new Error(`${connection.name} closed before it was ready`)),
|
|
266
|
+
onFailure: (error) => Effect.fail(new Error(`${connection.name} failed before it was ready: ${String(error)}`)),
|
|
267
|
+
}))).pipe(Effect.tap(() => Effect.sync(() => {
|
|
268
|
+
if (!abort.signal.aborted)
|
|
269
|
+
log.info(`[fastagent] long connection ready: ${connection.name}`);
|
|
270
|
+
})), Effect.forkScoped({ startImmediately: true }));
|
|
268
271
|
}));
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
//
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
schedules: scheduled.schedules,
|
|
306
|
-
ready,
|
|
307
|
-
...(withControl.control ? { control: withControl.control } : {}),
|
|
308
|
-
close,
|
|
309
|
-
};
|
|
272
|
+
if (options.signal?.aborted)
|
|
273
|
+
yield* shutdown;
|
|
274
|
+
else
|
|
275
|
+
yield* Effect.acquireRelease(Effect.sync(() => options.signal?.addEventListener("abort", onAbort, { once: true })), () => Effect.sync(() => options.signal?.removeEventListener("abort", onAbort)));
|
|
276
|
+
// The public readiness waiter lives outside the scope it may roll back.
|
|
277
|
+
const ready = Effect.runPromise(Effect.gen(function* () {
|
|
278
|
+
yield* Effect.forEach(readiness, Fiber.join, { concurrency: "unbounded", discard: true });
|
|
279
|
+
if (abort.signal.aborted)
|
|
280
|
+
return yield* Effect.fail(new Error("service closed before it became ready"));
|
|
281
|
+
if (dropped)
|
|
282
|
+
return yield* Effect.fail(new Error("a long connection closed before startup completed"));
|
|
283
|
+
routed.setReady(true);
|
|
284
|
+
}).pipe(Effect.catchCause((cause) => abort.signal.aborted
|
|
285
|
+
? Effect.fail(new Error("service closed before it became ready"))
|
|
286
|
+
: Effect.failCause(cause)), Effect.onError(() => rollback)));
|
|
287
|
+
// The rejection is still delivered to callers that await ready.
|
|
288
|
+
ready.catch(() => { });
|
|
289
|
+
return {
|
|
290
|
+
handler,
|
|
291
|
+
agent,
|
|
292
|
+
routes: { ...routed.unverified, ...withControl.routes },
|
|
293
|
+
agentDir,
|
|
294
|
+
workspace,
|
|
295
|
+
channels: {
|
|
296
|
+
routes: routed.routeChannels,
|
|
297
|
+
longConnections: names,
|
|
298
|
+
},
|
|
299
|
+
unverifiedRoutes: Object.keys(routed.unverified),
|
|
300
|
+
...(corsOrigins ? { corsOrigins } : {}),
|
|
301
|
+
routines: scheduled.routines,
|
|
302
|
+
ready,
|
|
303
|
+
...(withControl.controlPrefix ? { controlPrefix: withControl.controlPrefix } : {}),
|
|
304
|
+
close,
|
|
305
|
+
};
|
|
306
|
+
}).pipe(Scope.provide(lifetime), Effect.onError(() => rollback));
|
|
307
|
+
}));
|
|
310
308
|
}
|