@fastagent-sh/fastagent 0.21.0 → 0.22.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 +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 -43
- 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 +209 -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 +153 -228
- 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/session-remote.js
CHANGED
|
@@ -1,50 +1,94 @@
|
|
|
1
1
|
import { SSE_HEARTBEAT_MS } from "./channels/sse.js";
|
|
2
2
|
import { abortFirstIterator } from "./collect.js";
|
|
3
3
|
import { isAddressableSession, } from "./session.js";
|
|
4
|
-
/**
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
|
|
8
|
-
* tool call) keep heartbeating and never trip this. */
|
|
4
|
+
/**
|
|
5
|
+
* Dead-connection watchdog for SSE reads: the server heartbeats every SSE_HEARTBEAT_MS, so a PENDING READ seeing no
|
|
6
|
+
* bytes (of ANY kind — comments included) for this many missed beats means the connection is a black hole.
|
|
7
|
+
*/
|
|
9
8
|
const SSE_IDLE_LIMIT_MS = 3 * SSE_HEARTBEAT_MS;
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
9
|
+
/**
|
|
10
|
+
* Every request carries a TIMEOUT: a client's reliability model counts failed rounds against a budget ("unreachable
|
|
11
|
+
* for ~Ns"), which a black-hole endpoint (firewall drop, half-dead tunnel) would silently defeat — a hung
|
|
12
|
+
* state()/entries(), or a stream that never finishes connecting, ticks nothing.
|
|
13
|
+
*/
|
|
14
|
+
const REQUEST_TIMEOUT_MS = 10_000;
|
|
15
|
+
/** The PAYLOAD-bearing calls get a longer budget than the black-hole detector's 10s. */
|
|
16
|
+
const PAYLOAD_TIMEOUT_MS = 60_000;
|
|
17
|
+
class StreamEnded extends Error {
|
|
18
|
+
kind;
|
|
19
|
+
constructor(kind, message) {
|
|
20
|
+
super(message);
|
|
21
|
+
this.name = "StreamEnded";
|
|
22
|
+
this.kind = kind;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
/** The reason this connection was ended, when one was given — `undefined` for any other failure. */
|
|
26
|
+
function endedBecause(signal) {
|
|
27
|
+
return signal.reason instanceof StreamEnded ? signal.reason : undefined;
|
|
28
|
+
}
|
|
29
|
+
function readBudget(abort, what, connectMs) {
|
|
30
|
+
let timer;
|
|
31
|
+
let connected = false;
|
|
32
|
+
const expire = () => abort.abort(connected
|
|
33
|
+
? new StreamEnded("idle", `${what}: no bytes for ${SSE_IDLE_LIMIT_MS / 1000}s (heartbeats absent) — dead connection`)
|
|
34
|
+
: new StreamEnded("connect-timeout", `${what}: no usable response in ${connectMs / 1000}s — the endpoint accepted the connection and never completed one`));
|
|
35
|
+
const disarm = () => {
|
|
36
|
+
clearTimeout(timer);
|
|
37
|
+
timer = undefined;
|
|
38
|
+
};
|
|
19
39
|
return {
|
|
40
|
+
// `??=`: a nested arm does not restart the clock, so one long read cannot be extended by re-arming inside it.
|
|
20
41
|
arm: () => {
|
|
21
|
-
|
|
42
|
+
timer ??= setTimeout(expire, connected ? SSE_IDLE_LIMIT_MS : connectMs);
|
|
22
43
|
},
|
|
23
|
-
disarm
|
|
24
|
-
|
|
44
|
+
disarm,
|
|
45
|
+
connected: () => {
|
|
46
|
+
disarm();
|
|
47
|
+
connected = true;
|
|
25
48
|
},
|
|
26
|
-
stale: () => stale,
|
|
27
|
-
stop: () => clearInterval(timer),
|
|
28
49
|
};
|
|
29
50
|
}
|
|
30
|
-
/**
|
|
31
|
-
*
|
|
32
|
-
*
|
|
51
|
+
/**
|
|
52
|
+
* Open a streaming response — the one connect sequence both wire planes perform, under one budget: the headers, the
|
|
53
|
+
* error body of a non-2xx (a half-dead tunnel answering 4xx and then black-holing the body is the case that makes
|
|
54
|
+
* this one phase rather than two), and the check that a body exists at all. It returns only once the connection is
|
|
55
|
+
* something that can carry events, which is the moment the budget switches to the idle limit.
|
|
56
|
+
*/
|
|
57
|
+
async function openStreamBody(options) {
|
|
58
|
+
const { fetchFn, url, init, abort, budget, what } = options;
|
|
59
|
+
budget.arm(); // the connect await is a pending read
|
|
60
|
+
const res = await fetchFn(url, { ...init, signal: abort.signal });
|
|
61
|
+
if (!res.ok)
|
|
62
|
+
throw new ControlRequestError(res.status, await res.text());
|
|
63
|
+
if (!res.body)
|
|
64
|
+
throw new Error(`${what}: response has no body`);
|
|
65
|
+
budget.connected();
|
|
66
|
+
return res.body;
|
|
67
|
+
}
|
|
68
|
+
/** A control request the server answered with a non-2xx status. */
|
|
33
69
|
export class ControlRequestError extends Error {
|
|
34
70
|
status;
|
|
35
|
-
/**
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
|
|
71
|
+
/**
|
|
72
|
+
* The plane's own error code, when the reply carried one (`sessions()` is the only read that does today — design
|
|
73
|
+
* §13).
|
|
74
|
+
*/
|
|
39
75
|
code;
|
|
40
|
-
|
|
76
|
+
/**
|
|
77
|
+
* Whether re-sending is worth it, as the plane itself answered — the same question `SessionResult.retryable`
|
|
78
|
+
* answers for a write, carried here because a throwing read has no result to put it in. Absent when the reply
|
|
79
|
+
* declared no opinion; a caller then has only the status.
|
|
80
|
+
*/
|
|
81
|
+
retryable;
|
|
82
|
+
constructor(status, body, declared) {
|
|
41
83
|
super(`control request failed: ${status} ${body}`);
|
|
42
84
|
this.status = status;
|
|
43
|
-
if (code !== undefined)
|
|
44
|
-
this.code = code;
|
|
85
|
+
if (declared?.code !== undefined)
|
|
86
|
+
this.code = declared.code;
|
|
87
|
+
if (declared?.retryable !== undefined)
|
|
88
|
+
this.retryable = declared.retryable;
|
|
45
89
|
}
|
|
46
90
|
}
|
|
47
|
-
/** A non-2xx reply as an error, carrying the plane
|
|
91
|
+
/** A non-2xx reply as an error, carrying what the plane declared about it. */
|
|
48
92
|
async function controlError(res) {
|
|
49
93
|
const body = await res.text();
|
|
50
94
|
if (!res.headers.get("content-type")?.includes("application/json"))
|
|
@@ -54,77 +98,73 @@ async function controlError(res) {
|
|
|
54
98
|
parsed = JSON.parse(body);
|
|
55
99
|
}
|
|
56
100
|
catch {
|
|
57
|
-
// The reply declared JSON and is not — a protocol fault worth seeing, but not worth losing the
|
|
58
|
-
// status over: both travel in one error rather than a bare SyntaxError from a rejection path.
|
|
101
|
+
// The reply declared JSON and is not — a protocol fault worth seeing, but not worth losing the status over.
|
|
59
102
|
return new ControlRequestError(res.status, `${body} (declared application/json but did not parse)`);
|
|
60
103
|
}
|
|
61
|
-
return new ControlRequestError(res.status, body,
|
|
104
|
+
return new ControlRequestError(res.status, body, {
|
|
105
|
+
...(typeof parsed?.code === "string" ? { code: parsed.code } : {}),
|
|
106
|
+
...(typeof parsed?.retryable === "boolean" ? { retryable: parsed.retryable } : {}),
|
|
107
|
+
});
|
|
62
108
|
}
|
|
63
|
-
/**
|
|
64
|
-
* Connect and return a remote `SessionControl`. Async because `capabilities()` is synchronous in
|
|
65
|
-
* the contract: the static declaration is fetched ONCE here and served from memory — which also
|
|
66
|
-
* makes a wrong URL/token fail at connect time, not on first use.
|
|
67
|
-
*/
|
|
68
109
|
export async function connectSessionControl(options) {
|
|
69
|
-
const { url,
|
|
110
|
+
const { url, fetchFn = fetch } = options;
|
|
70
111
|
const base = url.replace(/\/$/, "");
|
|
71
|
-
const headers = { authorization: `Bearer ${token}` };
|
|
72
|
-
// Non-streaming requests carry a TIMEOUT: attach's whole reliability model counts failed rounds
|
|
73
|
-
// against a budget ("unreachable for ~Ns"), which a black-hole endpoint (firewall drop, half-dead
|
|
74
|
-
// tunnel) would silently defeat — a hung state()/entries() ticks nothing. The SSE stream stays
|
|
75
|
-
// timeout-free (quiet is normal there; heartbeats cover proxy idling).
|
|
76
|
-
const REQUEST_TIMEOUT_MS = 10_000;
|
|
77
|
-
// The PAYLOAD-bearing calls get a longer budget than the black-hole detector's 10s — in both
|
|
78
|
-
// directions: dispatch may UPLOAD up to the 1 MiB body cap (base64 images in steer/follow_up),
|
|
79
|
-
// and entries may DOWNLOAD a long session's full record (a cursor-less first backfill) — a slow
|
|
80
|
-
// link legitimately needs longer, and cutting a healthy transfer would be indistinguishable from
|
|
81
|
-
// a dead endpoint. capabilities/state stay on the short detector: they are small by contract.
|
|
82
|
-
const PAYLOAD_TIMEOUT_MS = 60_000;
|
|
83
112
|
const get = async (path, timeoutMs = REQUEST_TIMEOUT_MS) => {
|
|
84
|
-
const res = await fetchFn(`${base}${path}`, {
|
|
113
|
+
const res = await fetchFn(`${base}${path}`, { signal: AbortSignal.timeout(timeoutMs) });
|
|
85
114
|
if (!res.ok)
|
|
86
115
|
throw await controlError(res);
|
|
87
116
|
return (await res.json());
|
|
88
117
|
};
|
|
89
118
|
const capabilities = await get("/control/capabilities");
|
|
90
119
|
const eventsOf = (session) => {
|
|
91
|
-
//
|
|
92
|
-
//
|
|
93
|
-
//
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
120
|
+
// The server subscribes BEFORE it writes the response headers (channels/sse.ts pulls the source once first), so
|
|
121
|
+
// "headers arrived, 2xx" IS "this session's subscription exists" — the boundary a reconnecting client awaits
|
|
122
|
+
// before reading history (session.ts, SessionEventStream).
|
|
123
|
+
let subscribed;
|
|
124
|
+
let unreachable;
|
|
125
|
+
const ready = new Promise((resolve, reject) => {
|
|
126
|
+
subscribed = resolve;
|
|
127
|
+
unreachable = reject;
|
|
128
|
+
});
|
|
129
|
+
// A consumer that never awaits `ready` (it can iterate and let the stream report the same failure) would
|
|
130
|
+
// otherwise take an unhandled rejection for a connection that legitimately failed. Awaiters still get it.
|
|
131
|
+
ready.catch(() => { });
|
|
132
|
+
// ONE CALL IS ONE SUBSCRIPTION — one connection, one readiness. A second iteration would open a second
|
|
133
|
+
// connection and inherit this `ready`, reporting a boundary the first one crossed; it is refused instead, the
|
|
134
|
+
// same way the local hub refuses it.
|
|
97
135
|
const openStream = (abort) => (async function* iterate() {
|
|
98
|
-
|
|
99
|
-
// endpoint) is otherwise a window no timeout covers — the same watchdog terminates it,
|
|
100
|
-
// with headers-arrival counting as the first sign of life.
|
|
101
|
-
const watchdog = idleWatchdog(abort);
|
|
102
|
-
watchdog.arm(); // the connect await is a pending read
|
|
136
|
+
const budget = readBudget(abort, "control events", REQUEST_TIMEOUT_MS);
|
|
103
137
|
try {
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
throw new ControlRequestError(res.status, await res.text());
|
|
138
|
+
let body;
|
|
139
|
+
try {
|
|
140
|
+
body = await openStreamBody({
|
|
141
|
+
fetchFn,
|
|
142
|
+
url: `${base}/control/sessions/${encodeURIComponent(session)}/events`,
|
|
143
|
+
init: {},
|
|
144
|
+
abort,
|
|
145
|
+
budget,
|
|
146
|
+
what: "control events",
|
|
147
|
+
});
|
|
115
148
|
}
|
|
116
|
-
|
|
117
|
-
|
|
149
|
+
catch (error) {
|
|
150
|
+
// The subscription was never established: the endpoint is unreachable or never completed a response, a
|
|
151
|
+
// gateway in front refused it, the consumer cancelled first. Whoever ended it said why, and `fetch` rejected with
|
|
152
|
+
// that very object. A waiter on `ready` must learn it instead of waiting out a stream that will never
|
|
153
|
+
// carry anything — including the cancellation, which the ITERATION reports as a clean end (walking away
|
|
154
|
+
// is not an error) while `ready` still has a promise it cannot keep.
|
|
155
|
+
const failure = endedBecause(abort.signal) ?? error;
|
|
156
|
+
unreachable(failure);
|
|
157
|
+
throw failure;
|
|
158
|
+
}
|
|
159
|
+
subscribed();
|
|
118
160
|
let nextSeq = 0;
|
|
119
|
-
for await (const data of sseData(
|
|
120
|
-
// Parse discipline, same as the other two wire planes (dispatch parses, invoke
|
|
121
|
-
//
|
|
122
|
-
// thrown, so a consumer's failure budget applies — never misdiagnosed as an
|
|
123
|
-
// in-transit gap whose remedy (reconnect) can never fix it.
|
|
161
|
+
for await (const data of sseData(body, budget)) {
|
|
162
|
+
// Parse discipline, same as the other two wire planes (dispatch parses, invoke classifies drift): a
|
|
163
|
+
// non-JSON or non-envelope payload is PROTOCOL MISMATCH.
|
|
124
164
|
let wire;
|
|
125
165
|
try {
|
|
126
|
-
// The ONE envelope type (control.ts's WireEvent) — an inline shape would let the
|
|
127
|
-
//
|
|
166
|
+
// The ONE envelope type (control.ts's WireEvent) — an inline shape would let the envelope drift
|
|
167
|
+
// server-side while this cast silently kept the old fields.
|
|
128
168
|
wire = JSON.parse(data);
|
|
129
169
|
}
|
|
130
170
|
catch (parseError) {
|
|
@@ -138,11 +178,7 @@ export async function connectSessionControl(options) {
|
|
|
138
178
|
typeof wire.event.type !== "string") {
|
|
139
179
|
throw new Error("control events: malformed envelope — the endpoint does not speak this protocol version");
|
|
140
180
|
}
|
|
141
|
-
// Envelope checks — consumed HERE.
|
|
142
|
-
// one connection — see the header note.) A gap THROWS like a protocol mismatch: the
|
|
143
|
-
// consumer's failure path (budget, its own io) owns the diagnostic — a library-level
|
|
144
|
-
// log would bypass consumer output discipline, and a silent clean end would be
|
|
145
|
-
// indistinguishable from the server closing normally.
|
|
181
|
+
// Envelope checks — consumed HERE.
|
|
146
182
|
if (wire.seq !== nextSeq) {
|
|
147
183
|
throw new Error(`control events: sequence gap (expected ${nextSeq}, got ${wire.seq}) — events were lost in transit; resync via entries()`);
|
|
148
184
|
}
|
|
@@ -151,34 +187,48 @@ export async function connectSessionControl(options) {
|
|
|
151
187
|
}
|
|
152
188
|
}
|
|
153
189
|
catch (error) {
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
throw new Error(`control events: no bytes for ${SSE_IDLE_LIMIT_MS / 1000}s (heartbeats absent) — dead connection; resync via entries()`);
|
|
157
|
-
}
|
|
190
|
+
const ended = endedBecause(abort.signal);
|
|
191
|
+
if (ended?.kind === "cancelled")
|
|
158
192
|
return; // the consumer walked away — clean end, not an error
|
|
159
|
-
|
|
160
|
-
throw error;
|
|
193
|
+
throw ended ?? error;
|
|
161
194
|
}
|
|
162
195
|
finally {
|
|
163
|
-
|
|
196
|
+
budget.disarm();
|
|
164
197
|
abort.abort();
|
|
165
198
|
}
|
|
166
199
|
})();
|
|
200
|
+
let iterated = false;
|
|
167
201
|
return {
|
|
202
|
+
ready,
|
|
168
203
|
[Symbol.asyncIterator]() {
|
|
204
|
+
if (iterated) {
|
|
205
|
+
throw new Error("control events: this stream is one subscription — call events() again for another");
|
|
206
|
+
}
|
|
207
|
+
iterated = true;
|
|
169
208
|
const abort = new AbortController();
|
|
170
|
-
// Abort-first cancellation (see abortFirstIterator): aborting the connection unblocks a
|
|
171
|
-
//
|
|
172
|
-
return abortFirstIterator(openStream(abort), () =>
|
|
209
|
+
// Abort-first cancellation (see abortFirstIterator): aborting the connection unblocks a generator suspended
|
|
210
|
+
// on a quiet stream read.
|
|
211
|
+
return abortFirstIterator(openStream(abort), () => {
|
|
212
|
+
const cancelled = new StreamEnded("cancelled", "control events: cancelled by the consumer");
|
|
213
|
+
abort.abort(cancelled);
|
|
214
|
+
// Said HERE as well as in the generator's catch, because a generator that was never pulled does not run its
|
|
215
|
+
// body on `return()` — and then nothing else would ever settle `ready`. Rejecting an already-settled
|
|
216
|
+
// promise is a no-op, so the connected case still reports whatever ended it.
|
|
217
|
+
unreachable(cancelled);
|
|
218
|
+
});
|
|
173
219
|
},
|
|
174
220
|
};
|
|
175
221
|
};
|
|
176
|
-
/**
|
|
177
|
-
*
|
|
222
|
+
/**
|
|
223
|
+
* A write that answers a `SessionResult`: the result rides HTTP 200 either way (`ok: false` is a protocol answer,
|
|
224
|
+
* not a transport failure), so a non-2xx here is a REAL transport fault.
|
|
225
|
+
*/
|
|
178
226
|
const write = async (path, method, body) => {
|
|
227
|
+
// Always declared, body or not: the plane refuses a write it was not told is JSON (channels/body.ts), and an
|
|
228
|
+
// empty `PATCH` is a legal no-op that still has to get through.
|
|
179
229
|
const res = await fetchFn(`${base}${path}`, {
|
|
180
230
|
method,
|
|
181
|
-
headers:
|
|
231
|
+
headers: { "content-type": "application/json" },
|
|
182
232
|
...(body === undefined ? {} : { body: JSON.stringify(body) }),
|
|
183
233
|
signal: AbortSignal.timeout(PAYLOAD_TIMEOUT_MS),
|
|
184
234
|
});
|
|
@@ -189,12 +239,8 @@ export async function connectSessionControl(options) {
|
|
|
189
239
|
const id = (session) => encodeURIComponent(session);
|
|
190
240
|
return {
|
|
191
241
|
capabilities: () => capabilities,
|
|
192
|
-
// NOT prefetched like capabilities: a live definition can grow a skill between calls, so the
|
|
193
|
-
//
|
|
194
|
-
// skills/ per request), which is what keeps it honest about a directory that changes underneath
|
|
195
|
-
// it. A 404 is SKEW, not a fault in the definition: without this the two read identically
|
|
196
|
-
// (uncoded non-2xx), and a client would report "this agent's skills are unreadable" about a
|
|
197
|
-
// serve that simply predates the route.
|
|
242
|
+
// NOT prefetched like capabilities: a live definition can grow a skill between calls, so the list is fetched per
|
|
243
|
+
// call.
|
|
198
244
|
async commands() {
|
|
199
245
|
try {
|
|
200
246
|
return await get("/control/commands");
|
|
@@ -207,16 +253,9 @@ export async function connectSessionControl(options) {
|
|
|
207
253
|
}
|
|
208
254
|
},
|
|
209
255
|
sessions: {
|
|
210
|
-
// Rejects when the deployment cannot enumerate its store
|
|
211
|
-
// ControlRequestError carrying `sessions_unavailable`, so a client can tell it from an
|
|
212
|
-
// unreachable endpoint instead of retrying forever.
|
|
256
|
+
// Rejects when the deployment cannot enumerate its store.
|
|
213
257
|
list: () => get("/control/sessions", PAYLOAD_TIMEOUT_MS),
|
|
214
|
-
// PUT: the fork is idempotent, and so is the request that carries it.
|
|
215
|
-
// segment exactly like `get`'s id, so it is refused on the same rule — without this the local
|
|
216
|
-
// plane answers `invalid_command` while the wire answers 404 from a URL that normalised away.
|
|
217
|
-
// ASYNC, so the guard REJECTS rather than throwing out of a method typed `Promise`: a caller
|
|
218
|
-
// that wrote `.catch(…)` — or handed this to `Promise.all` — must not be surprised by a
|
|
219
|
-
// synchronous throw. (`get` may throw: it is synchronous by signature.)
|
|
258
|
+
// PUT: the fork is idempotent, and so is the request that carries it.
|
|
220
259
|
fork: async ({ from, at, into }) => {
|
|
221
260
|
if (!isAddressableSession(into)) {
|
|
222
261
|
throw new Error(`session id ${JSON.stringify(into)} cannot travel as a URL path segment — this transport cannot address it`);
|
|
@@ -224,12 +263,6 @@ export async function connectSessionControl(options) {
|
|
|
224
263
|
return write(`/control/sessions/${id(into)}`, "PUT", { from, at });
|
|
225
264
|
},
|
|
226
265
|
// The local hub's handle is a pure binding; so is this one — an id and the transport above it.
|
|
227
|
-
// Nothing is FETCHED here, which is what keeps the two isomorphic. What is checked is the one
|
|
228
|
-
// thing the wire cannot express: `.` and `..` survive `encodeURIComponent` and are then
|
|
229
|
-
// normalised away by URL parsing, so every call on such a handle would arrive at a DIFFERENT
|
|
230
|
-
// route — `.` reads as the collection (200 JSON, which the SSE reader ends as a silently empty
|
|
231
|
-
// stream) and `..` as a 404 the local plane answers normally. Refused at the binding, where a
|
|
232
|
-
// caller can see it, rather than once per call in a place it looks like a server answer.
|
|
233
266
|
get: (session) => {
|
|
234
267
|
if (!isAddressableSession(session)) {
|
|
235
268
|
throw new Error(`session id ${JSON.stringify(session)} cannot travel as a URL path segment — this transport cannot address it`);
|
|
@@ -254,18 +287,12 @@ export async function connectSessionControl(options) {
|
|
|
254
287
|
};
|
|
255
288
|
}
|
|
256
289
|
/**
|
|
257
|
-
* The remote DATA plane: an `Agent` whose `invoke` drives `POST /
|
|
258
|
-
*
|
|
259
|
-
*
|
|
260
|
-
* included: SPEC MUST 2 forbids iteration throws, so every failure — transport (401/refused/
|
|
261
|
-
* dropped mid-stream), protocol, and the images precheck — becomes a terminal `failed` event
|
|
262
|
-
* (`retryable` from the HTTP status where one exists; network trouble is retryable). Breaking out
|
|
263
|
-
* of iteration disconnects the request, which cancels the run (SPEC cancellation semantics travel
|
|
264
|
-
* the wire). The invoke wire is text-only for now: a prompt with images fails visibly instead of
|
|
265
|
-
* silently dropping them (steer/follow_up on the control plane carry full Prompts).
|
|
290
|
+
* The remote DATA plane: an `Agent` whose `invoke` drives `POST /invoke` on a serving process. A real Agent,
|
|
291
|
+
* failure discipline included — SPEC MUST 2 forbids iteration throws, so transport, protocol and precheck failures
|
|
292
|
+
* all become `failed` events.
|
|
266
293
|
*/
|
|
267
294
|
export function connectAgent(options) {
|
|
268
|
-
const { url,
|
|
295
|
+
const { url, fetchFn = fetch } = options;
|
|
269
296
|
const base = url.replace(/\/$/, "");
|
|
270
297
|
const toFailed = (error) => {
|
|
271
298
|
if (error instanceof ControlRequestError) {
|
|
@@ -273,13 +300,12 @@ export function connectAgent(options) {
|
|
|
273
300
|
}
|
|
274
301
|
return { type: "failed", details: String(error), retryable: true }; // network-class: worth re-sending
|
|
275
302
|
};
|
|
276
|
-
// COMPILE-TIME drift guard (dispatch-wire parity): the invoke body carries exactly text (and
|
|
277
|
-
//
|
|
278
|
-
// (carry it or reject it), never vanish on the wire while the client believes it was sent.
|
|
303
|
+
// COMPILE-TIME drift guard (dispatch-wire parity): the invoke body carries exactly text (and rejects images
|
|
304
|
+
// visibly).
|
|
279
305
|
const _invokeDriftGuard = {};
|
|
280
306
|
void _invokeDriftGuard;
|
|
281
|
-
// Same guard for Scope: the body carries session + the lineage extension — a new Scope field must
|
|
282
|
-
//
|
|
307
|
+
// Same guard for Scope: the body carries session + the lineage extension — a new Scope field must force a decision
|
|
308
|
+
// (carry it or reject it), never vanish on the wire.
|
|
283
309
|
const _scopeDriftGuard = {};
|
|
284
310
|
void _scopeDriftGuard;
|
|
285
311
|
return {
|
|
@@ -296,37 +322,33 @@ export function connectAgent(options) {
|
|
|
296
322
|
}
|
|
297
323
|
// A terminal closes the stream. Cleanup errors must not append a second terminal.
|
|
298
324
|
let terminalSeen = false;
|
|
299
|
-
//
|
|
300
|
-
//
|
|
301
|
-
|
|
302
|
-
|
|
325
|
+
// The run's driver rides the same mechanism as the events plane, with the connect limit its own callers
|
|
326
|
+
// need: a black-holed POST must not hang it, but a scale-to-zero host (fly `auto_start_machines`, a cold
|
|
327
|
+
// AgentCore container) can legitimately hold this open for tens of seconds before the first header, and
|
|
328
|
+
// that is a slow success, not a dead endpoint. A connect failure lands in the catch below, which is where
|
|
329
|
+
// every non-terminal failure becomes the one `failed` event this stream owes its caller.
|
|
330
|
+
const budget = readBudget(abort, "remote invoke", PAYLOAD_TIMEOUT_MS);
|
|
303
331
|
try {
|
|
304
|
-
const
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
332
|
+
const body = await openStreamBody({
|
|
333
|
+
fetchFn,
|
|
334
|
+
url: `${base}/invoke`,
|
|
335
|
+
init: {
|
|
336
|
+
method: "POST",
|
|
337
|
+
headers: { "content-type": "application/json" },
|
|
338
|
+
body: JSON.stringify({
|
|
339
|
+
session: scope.session,
|
|
340
|
+
text: prompt.text,
|
|
341
|
+
// Lineage rides the wire so a remote thread scope inherits server-side; the server reads it on the
|
|
342
|
+
// session-create path only, same as in-process.
|
|
343
|
+
...(scope.parentSession !== undefined ? { parentSession: scope.parentSession } : {}),
|
|
344
|
+
...(scope.branchHints !== undefined ? { branchHints: scope.branchHints } : {}),
|
|
345
|
+
}),
|
|
346
|
+
},
|
|
347
|
+
abort,
|
|
348
|
+
budget,
|
|
349
|
+
what: "remote invoke",
|
|
316
350
|
});
|
|
317
|
-
|
|
318
|
-
if (!res.ok) {
|
|
319
|
-
watchdog.arm(); // the error body is a pending read too — see the events() twin
|
|
320
|
-
const failure = toFailed(new ControlRequestError(res.status, await res.text()));
|
|
321
|
-
watchdog.disarm();
|
|
322
|
-
yield failure;
|
|
323
|
-
return;
|
|
324
|
-
}
|
|
325
|
-
if (!res.body) {
|
|
326
|
-
yield { type: "failed", details: "remote invoke: response has no body", retryable: true };
|
|
327
|
-
return;
|
|
328
|
-
}
|
|
329
|
-
for await (const data of sseData(res.body, watchdog)) {
|
|
351
|
+
for await (const data of sseData(body, budget)) {
|
|
330
352
|
let event;
|
|
331
353
|
try {
|
|
332
354
|
event = JSON.parse(data);
|
|
@@ -339,9 +361,8 @@ export function connectAgent(options) {
|
|
|
339
361
|
};
|
|
340
362
|
return;
|
|
341
363
|
}
|
|
342
|
-
// Shape check, same discipline as the events plane: `data: null` / `data: 42` is
|
|
343
|
-
//
|
|
344
|
-
// misclassified as retryable network trouble.
|
|
364
|
+
// Shape check, same discipline as the events plane: `data: null` / `data: 42` is valid JSON but
|
|
365
|
+
// protocol drift.
|
|
345
366
|
if (typeof event !== "object" || event === null || typeof event.type !== "string") {
|
|
346
367
|
yield {
|
|
347
368
|
type: "failed",
|
|
@@ -358,29 +379,23 @@ export function connectAgent(options) {
|
|
|
358
379
|
yield { type: "failed", details: "remote invoke: stream ended without a terminal", retryable: true };
|
|
359
380
|
}
|
|
360
381
|
catch (error) {
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
};
|
|
368
|
-
}
|
|
369
|
-
return; // the consumer walked away — cancellation, not an error
|
|
370
|
-
}
|
|
382
|
+
const ended = endedBecause(abort.signal);
|
|
383
|
+
// Cancellation is the consumer's own doing — reporting it back would be news to nobody. Any other stated
|
|
384
|
+
// reason is a connection that died under the run, and this stream still owes its caller a terminal —
|
|
385
|
+
// unless one was already sent, since a second would describe the run rather than the connection.
|
|
386
|
+
if (ended?.kind === "cancelled")
|
|
387
|
+
return;
|
|
371
388
|
if (!terminalSeen)
|
|
372
|
-
yield toFailed(error);
|
|
389
|
+
yield ended ? { type: "failed", details: ended.message, retryable: true } : toFailed(error);
|
|
373
390
|
}
|
|
374
391
|
finally {
|
|
375
|
-
|
|
392
|
+
budget.disarm();
|
|
376
393
|
abort.abort();
|
|
377
394
|
}
|
|
378
395
|
})();
|
|
379
|
-
// ONE stream per invoke, like a local async generator (which is its own iterator): a second
|
|
380
|
-
//
|
|
381
|
-
|
|
382
|
-
// while suspended on a quiet read.
|
|
383
|
-
const iterator = abortFirstIterator(openStream(), () => abort.abort());
|
|
396
|
+
// ONE stream per invoke, like a local async generator (which is its own iterator): a second iteration must
|
|
397
|
+
// never re-POST.
|
|
398
|
+
const iterator = abortFirstIterator(openStream(), () => abort.abort(new StreamEnded("cancelled", "remote invoke: cancelled by the consumer")));
|
|
384
399
|
return {
|
|
385
400
|
[Symbol.asyncIterator]() {
|
|
386
401
|
return iterator;
|
|
@@ -389,10 +404,7 @@ export function connectAgent(options) {
|
|
|
389
404
|
},
|
|
390
405
|
};
|
|
391
406
|
}
|
|
392
|
-
/** Minimal SSE reader: yields each `data:` payload; ignores comments (heartbeats) and other
|
|
393
|
-
* fields. The explicit reader loop (not for-await) exists for the watchdog: armed strictly
|
|
394
|
-
* around each pending read, so only "we are listening and nothing arrives" counts as idle — a
|
|
395
|
-
* consumer pausing at a yield leaves the watch disarmed (see {@link ReadWatch}). */
|
|
407
|
+
/** Minimal SSE reader: yields each `data:` payload; ignores comments (heartbeats) and other fields. */
|
|
396
408
|
async function* sseData(body, watch) {
|
|
397
409
|
const decoder = new TextDecoder();
|
|
398
410
|
let buffer = "";
|
|
@@ -404,8 +416,7 @@ async function* sseData(body, watch) {
|
|
|
404
416
|
watch?.disarm(); // bytes (ANY bytes — heartbeats included) or a clean end arrived
|
|
405
417
|
if (done)
|
|
406
418
|
return;
|
|
407
|
-
// SSE permits CRLF line endings (proxies/other servers may produce them)
|
|
408
|
-
// appending so a \r\n split across chunks still collapses once its second half arrives.
|
|
419
|
+
// SSE permits CRLF line endings (proxies/other servers may produce them).
|
|
409
420
|
buffer = (buffer + decoder.decode(value, { stream: true })).replace(/\r\n/g, "\n");
|
|
410
421
|
let sep = buffer.indexOf("\n\n");
|
|
411
422
|
while (sep !== -1) {
|