@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
|
@@ -1,46 +1,75 @@
|
|
|
1
|
-
|
|
1
|
+
// The L0 rendering payload reads message text the way the engine does — ONE reading, joined without a
|
|
2
|
+
// separator because this payload is a transcript, not a preview line.
|
|
3
|
+
import { contentText } from "@earendil-works/pi-ai";
|
|
4
|
+
import * as Deferred from "effect/Deferred";
|
|
5
|
+
import * as Effect from "effect/Effect";
|
|
6
|
+
import * as Queue from "effect/Queue";
|
|
7
|
+
import { PortFailure, port, portAbort, portCleanup, portError, portJoin } from "../../effect-port.js";
|
|
8
|
+
import { acquireSession, acquireSessionLease } from "./session-effects.js";
|
|
9
|
+
import { calculateContextTokens, estimateTokens, getLastAssistantUsage, } from "@earendil-works/pi-coding-agent";
|
|
2
10
|
import { getSupportedThinkingLevels } from "@earendil-works/pi-ai";
|
|
3
11
|
import { SESSION_BUSY_CODE } from "../../agent.js";
|
|
4
12
|
import { BOUNDARY_COMMAND_FAILED_CODE, INVALID_COMMAND_CODE, isAddressableSession, NO_ACTIVE_RUN_CODE, NOTHING_TO_COMPACT_CODE, NO_SUCH_SESSION_CODE, PARTIAL_UPDATE_CODE, UPDATE_FIELDS, RUN_COMMAND_FAILED_CODE, UNSUPPORTED_CAPABILITY_CODE, } from "../../session.js";
|
|
5
13
|
import { listModels } from "./config.js";
|
|
6
14
|
import { forkProvenance, isNavigable, publishedLeaf } from "./session-markers.js";
|
|
15
|
+
import { startCompaction } from "./agent-session-factory.js";
|
|
7
16
|
import { toRetryScheduledEvent } from "./retry-event.js";
|
|
8
17
|
import { THINKING_LEVELS, activePath, resolveSessionSettings } from "./session-settings.js";
|
|
9
18
|
import { log } from "../../log.js";
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
19
|
+
// ── Usage (read from the record) ─────────────────────────────────────────────
|
|
20
|
+
/**
|
|
21
|
+
* What the newest answer cost and how full the context is, from the record rather than a live session (a served
|
|
22
|
+
* session exists only during its run). The numbers are the provider's own, on the newest assistant pi counts as
|
|
23
|
+
* carrying usage (`getLastAssistantUsage`). The context size adds pi's estimate of what followed that answer, which
|
|
24
|
+
* after a finished turn is nothing: then it is exactly pi's `getContextUsage()`.
|
|
25
|
+
*
|
|
26
|
+
* `contextTokens` is omitted, never guessed, once a compaction or a context edit follows that answer: the provider's
|
|
27
|
+
* count no longer describes the context (pi's own rule). pi then estimates from characters; this reports unknown
|
|
28
|
+
* until the next response.
|
|
29
|
+
*/
|
|
30
|
+
function sessionUsage(path, record, contextWindow) {
|
|
31
|
+
const usage = getLastAssistantUsage(path);
|
|
32
|
+
if (!usage)
|
|
33
|
+
return undefined;
|
|
34
|
+
let at = path.length - 1;
|
|
35
|
+
while (at >= 0) {
|
|
36
|
+
const entry = path[at];
|
|
37
|
+
if (entry?.type === "message" && entry.message.usage === usage)
|
|
38
|
+
break;
|
|
39
|
+
at--;
|
|
20
40
|
}
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
41
|
+
const answer = path[at];
|
|
42
|
+
const stale = path.slice(at + 1).some((entry) => entry.type === "compaction" || entry.type === "context_edit");
|
|
43
|
+
let contextTokens;
|
|
44
|
+
if (answer && !stale) {
|
|
45
|
+
let trailing = 0;
|
|
46
|
+
let after = false;
|
|
47
|
+
for (const { sourceEntry, messages } of record.buildSessionProjection().entries) {
|
|
48
|
+
if (after)
|
|
49
|
+
for (const message of messages)
|
|
50
|
+
trailing += estimateTokens(message);
|
|
51
|
+
if (sourceEntry.id === answer.id)
|
|
52
|
+
after = true;
|
|
53
|
+
}
|
|
54
|
+
contextTokens = calculateContextTokens(usage) + trailing;
|
|
55
|
+
}
|
|
56
|
+
return {
|
|
57
|
+
inputTokens: usage.input,
|
|
58
|
+
outputTokens: usage.output,
|
|
59
|
+
cacheReadTokens: usage.cacheRead,
|
|
60
|
+
cacheWriteTokens: usage.cacheWrite,
|
|
61
|
+
cost: usage.cost.total,
|
|
62
|
+
...(contextTokens !== undefined ? { contextTokens } : {}),
|
|
63
|
+
...(contextWindow !== undefined && contextWindow > 0 ? { contextWindow } : {}),
|
|
64
|
+
};
|
|
25
65
|
}
|
|
26
66
|
// ── Entry normalization (durable plane) ──────────────────────────────────────
|
|
27
|
-
/** Concatenated plain text of a message's content blocks (the L0 rendering payload). A custom
|
|
28
|
-
* AgentMessage role may carry no `content` at all — that reads as empty, not a crash. */
|
|
29
|
-
function textOf(content) {
|
|
30
|
-
if (typeof content === "string")
|
|
31
|
-
return content;
|
|
32
|
-
if (!Array.isArray(content))
|
|
33
|
-
return "";
|
|
34
|
-
return content
|
|
35
|
-
.filter((b) => b.type === "text" && typeof b.text === "string")
|
|
36
|
-
.map((b) => b.text)
|
|
37
|
-
.join("");
|
|
38
|
-
}
|
|
39
67
|
/**
|
|
40
68
|
* pi `PiSessionEntry` → neutral {@link SessionEntry}. Message entries map onto the guaranteed
|
|
41
69
|
* kind vocabulary (user/assistant/tool) with a minimal render payload; every other engine record
|
|
42
70
|
* keeps its pi type as an open-set kind with an EMPTY payload — present so `parentId` chains and
|
|
43
71
|
* cursors stay intact, skippable by contract, and no pi message class leaks through the adapter.
|
|
72
|
+
* One exception: `context_edit` carries `{ targetId, omitted }`, never the replacement content.
|
|
44
73
|
*/
|
|
45
74
|
function toSessionEntry(entry, parentId) {
|
|
46
75
|
const base = {
|
|
@@ -51,12 +80,12 @@ function toSessionEntry(entry, parentId) {
|
|
|
51
80
|
if (entry.type === "message") {
|
|
52
81
|
const m = entry.message;
|
|
53
82
|
if (m.role === "user")
|
|
54
|
-
return { ...base, kind: "user", data: { text:
|
|
83
|
+
return { ...base, kind: "user", data: { text: contentText(m.content, "") } };
|
|
55
84
|
if (m.role === "assistant") {
|
|
56
85
|
const toolCalls = m.content
|
|
57
86
|
.filter((b) => b.type === "toolCall")
|
|
58
87
|
.map((b) => ({ id: b.id ?? "", name: b.name ?? "" }));
|
|
59
|
-
const data = { text:
|
|
88
|
+
const data = { text: contentText(m.content, "") };
|
|
60
89
|
if (toolCalls.length > 0)
|
|
61
90
|
data.toolCalls = toolCalls;
|
|
62
91
|
return { ...base, kind: "assistant", data };
|
|
@@ -69,13 +98,21 @@ function toSessionEntry(entry, parentId) {
|
|
|
69
98
|
toolCallId: m.toolCallId,
|
|
70
99
|
toolName: m.toolName,
|
|
71
100
|
isError: m.isError ?? false,
|
|
72
|
-
text:
|
|
101
|
+
text: contentText(m.content, ""),
|
|
73
102
|
},
|
|
74
103
|
};
|
|
75
104
|
}
|
|
76
|
-
//
|
|
105
|
+
// Not a conversation turn (`isConversationMessage`): the engine's own `system` bookkeeping, or a custom
|
|
106
|
+
// AgentMessage role a channel/extension defined. Open-set kind with an EMPTY payload, skippable by contract —
|
|
107
|
+
// which is also what keeps the assembled prompt out of everything this plane publishes.
|
|
77
108
|
return { ...base, kind: `message:${m.role}`, data: {} };
|
|
78
109
|
}
|
|
110
|
+
// Which transcript entry the model no longer sees as written: `omitted` when it left the model context (pi's own
|
|
111
|
+
// abandoned retry and overflow attempts, or an extension's edit), otherwise its content was replaced. The target
|
|
112
|
+
// stays in the transcript either way, and the edit applies only on a path that contains it.
|
|
113
|
+
if (entry.type === "context_edit") {
|
|
114
|
+
return { ...base, kind: entry.type, data: { targetId: entry.targetId, omitted: entry.replacement === null } };
|
|
115
|
+
}
|
|
79
116
|
return { ...base, kind: entry.type, data: {} };
|
|
80
117
|
}
|
|
81
118
|
// ── Live fan-out (events plane) ──────────────────────────────────────────────
|
|
@@ -83,9 +120,22 @@ function toSessionEntry(entry, parentId) {
|
|
|
83
120
|
* prefix; the reader drains it before seeing done, then reconnects and backfills via entries(). */
|
|
84
121
|
export const SUBSCRIBER_BUFFER_CAP = 10_000;
|
|
85
122
|
export const SUBSCRIBER_BUFFER_BYTES = 8 * 1024 * 1024;
|
|
86
|
-
/**
|
|
87
|
-
*
|
|
88
|
-
*
|
|
123
|
+
/**
|
|
124
|
+
* One subscriber's bounded push→pull queue.
|
|
125
|
+
*
|
|
126
|
+
* The mechanics are Effect's, because both halves are things this class hand-rolled and got wrong
|
|
127
|
+
* once each. `Queue.end` stops accepting offers, lets the reader drain what WAS accepted, and only
|
|
128
|
+
* then reports done — which is both the overflow policy above and the door teardown needs (an async
|
|
129
|
+
* generator suspended on a quiet stream cannot be ended by `return()` alone: that queues behind the
|
|
130
|
+
* never-settling await). And a queue has a SET of takers, so two concurrent `next()` calls — legal,
|
|
131
|
+
* any wrapper may poll twice — cannot overwrite each other's resolver.
|
|
132
|
+
*
|
|
133
|
+
* What stays here is the arithmetic Queue has no opinion on: the two backlog limits, and the JSON
|
|
134
|
+
* snapshot (taken at push, so a producer that later mutates its event or tool result cannot grow a
|
|
135
|
+
* queued one). Cost of the swap: one fiber per delivered event instead of an array shift, on a
|
|
136
|
+
* stream that carries per-token deltas — cheap next to a model call, and paid only while someone is
|
|
137
|
+
* attached.
|
|
138
|
+
*/
|
|
89
139
|
class Subscriber {
|
|
90
140
|
/** For the overflow diagnostic only — a warn without the session is not actionable on a
|
|
91
141
|
* multi-session serve. (Explicit assignment: TS parameter properties break Node's strip-only
|
|
@@ -94,52 +144,34 @@ class Subscriber {
|
|
|
94
144
|
constructor(session) {
|
|
95
145
|
this.session = session;
|
|
96
146
|
}
|
|
97
|
-
|
|
98
|
-
|
|
147
|
+
queue = Effect.runSync(Queue.unbounded());
|
|
148
|
+
buffered = 0;
|
|
99
149
|
bufferedBytes = 0;
|
|
100
|
-
|
|
101
|
-
// may poll twice), and a single `wake` field would let the second await overwrite the first's
|
|
102
|
-
// resolver — hanging the first next() forever. Every wake flushes all waiters; each re-checks the
|
|
103
|
-
// buffer and re-queues if another consumer won the event.
|
|
104
|
-
wakes = [];
|
|
105
|
-
closed = false;
|
|
106
|
-
flush() {
|
|
107
|
-
const wakes = this.wakes;
|
|
108
|
-
this.wakes = [];
|
|
109
|
-
for (const wake of wakes)
|
|
110
|
-
wake();
|
|
111
|
-
}
|
|
150
|
+
ended = false;
|
|
112
151
|
push(event) {
|
|
113
|
-
if (this.
|
|
152
|
+
if (this.ended)
|
|
114
153
|
return;
|
|
115
154
|
const json = JSON.stringify(event);
|
|
116
155
|
const bytes = Buffer.byteLength(json);
|
|
117
|
-
if (this.
|
|
156
|
+
if (this.buffered >= SUBSCRIBER_BUFFER_CAP || this.bufferedBytes + bytes > SUBSCRIBER_BUFFER_BYTES) {
|
|
118
157
|
log.warn(`[fastagent] session-control subscriber for session "${this.session}" exceeded its buffer limit (${SUBSCRIBER_BUFFER_CAP} events / ${SUBSCRIBER_BUFFER_BYTES} bytes; ${this.bufferedBytes} bytes queued, ${bytes} incoming); its stream ends after draining the backlog, then the client resyncs via entries()`);
|
|
119
158
|
this.close();
|
|
120
159
|
return;
|
|
121
160
|
}
|
|
122
|
-
this.
|
|
161
|
+
Queue.offerUnsafe(this.queue, { json, bytes });
|
|
162
|
+
this.buffered++;
|
|
123
163
|
this.bufferedBytes += bytes;
|
|
124
|
-
this.flush();
|
|
125
164
|
}
|
|
126
165
|
close() {
|
|
127
|
-
this.
|
|
128
|
-
this.
|
|
166
|
+
this.ended = true;
|
|
167
|
+
Queue.endUnsafe(this.queue);
|
|
129
168
|
}
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
}
|
|
137
|
-
if (this.closed)
|
|
138
|
-
return { done: true, value: undefined };
|
|
139
|
-
await new Promise((resolve) => {
|
|
140
|
-
this.wakes.push(resolve);
|
|
141
|
-
});
|
|
142
|
-
}
|
|
169
|
+
next() {
|
|
170
|
+
return Effect.runPromise(Queue.take(this.queue).pipe(Effect.map((item) => {
|
|
171
|
+
this.buffered--;
|
|
172
|
+
this.bufferedBytes -= item.bytes;
|
|
173
|
+
return { done: false, value: JSON.parse(item.json) };
|
|
174
|
+
}), Effect.catchTag("Done", () => Effect.succeed({ done: true, value: undefined }))));
|
|
143
175
|
}
|
|
144
176
|
}
|
|
145
177
|
/**
|
|
@@ -183,7 +215,7 @@ export function createPiSessionControl(options) {
|
|
|
183
215
|
};
|
|
184
216
|
const observer = (session, event, run) => {
|
|
185
217
|
if (event.type === "run_started" && event.runId) {
|
|
186
|
-
active.set(session, { runId: event.runId, controls: run, pending: { steering:
|
|
218
|
+
active.set(session, { runId: event.runId, controls: run, pending: { steering: [], followUp: [] } });
|
|
187
219
|
}
|
|
188
220
|
else if (event.type === "run_settled") {
|
|
189
221
|
active.delete(session);
|
|
@@ -194,6 +226,22 @@ export function createPiSessionControl(options) {
|
|
|
194
226
|
entry.pending = event.data;
|
|
195
227
|
}
|
|
196
228
|
fanOut(session, event);
|
|
229
|
+
if (event.type === "run_settled")
|
|
230
|
+
publishUsage(session);
|
|
231
|
+
};
|
|
232
|
+
/**
|
|
233
|
+
* Report `usage` as the record now reads it: after a run (a new answer) and after a compaction (the context it
|
|
234
|
+
* described is gone). Read after the fact, so it follows the event that caused it.
|
|
235
|
+
*/
|
|
236
|
+
const publishUsage = (session) => {
|
|
237
|
+
// Every serve runs a hub, most with nobody attached (a chat channel): reading the whole record each turn for an
|
|
238
|
+
// empty audience would cost a full parse per turn, growing with the conversation.
|
|
239
|
+
if (!subscribers.get(session)?.size && !options.tap)
|
|
240
|
+
return;
|
|
241
|
+
reads.state(session).then(({ usage }) => {
|
|
242
|
+
if (usage)
|
|
243
|
+
emitOwn(session, { type: "state_changed", timestamp: Date.now(), data: { usage } });
|
|
244
|
+
}, (error) => log.warn(`[fastagent] session ${session}: usage not published: ${String(error)}`));
|
|
197
245
|
};
|
|
198
246
|
// The reads, plus the two sessionless declarations. Bound onto a handle below.
|
|
199
247
|
const reads = {
|
|
@@ -219,7 +267,7 @@ export function createPiSessionControl(options) {
|
|
|
219
267
|
// `state().availableThinkingLevels` — a list here could only answer for one model.
|
|
220
268
|
...(b ? { allowedModels: listModels(b.models) } : {}),
|
|
221
269
|
toolProgress: true, // tool_progress IS delivered (replace-semantics snapshots)
|
|
222
|
-
usage:
|
|
270
|
+
usage: true, // state().usage, and state_changed{usage} after each run and compaction
|
|
223
271
|
};
|
|
224
272
|
},
|
|
225
273
|
async state(session) {
|
|
@@ -235,12 +283,16 @@ export function createPiSessionControl(options) {
|
|
|
235
283
|
// `boundary_command_failed`. Here it is a server-side warn.
|
|
236
284
|
const b = boundary;
|
|
237
285
|
let settings;
|
|
238
|
-
|
|
286
|
+
let usage;
|
|
287
|
+
if (opened) {
|
|
239
288
|
try {
|
|
240
|
-
|
|
289
|
+
const path = activePath(opened);
|
|
290
|
+
if (b)
|
|
291
|
+
settings = resolveSessionSettings(path, b.models, b.defaults);
|
|
292
|
+
usage = sessionUsage(path, opened, settings?.model.contextWindow);
|
|
241
293
|
}
|
|
242
294
|
catch (error) {
|
|
243
|
-
log.warn(`[fastagent] session ${session}:
|
|
295
|
+
log.warn(`[fastagent] session ${session}: state unreadable (entry chain): ${String(error)}`);
|
|
244
296
|
}
|
|
245
297
|
}
|
|
246
298
|
const name = opened?.getSessionName();
|
|
@@ -255,7 +307,10 @@ export function createPiSessionControl(options) {
|
|
|
255
307
|
availableThinkingLevels: settings.availableThinkingLevels,
|
|
256
308
|
}
|
|
257
309
|
: {}),
|
|
258
|
-
pending: run
|
|
310
|
+
pending: run
|
|
311
|
+
? { steering: [...run.pending.steering], followUp: [...run.pending.followUp] }
|
|
312
|
+
: { steering: [], followUp: [] },
|
|
313
|
+
...(usage ? { usage } : {}),
|
|
259
314
|
...(leafEntryId ? { leafEntryId } : {}),
|
|
260
315
|
};
|
|
261
316
|
},
|
|
@@ -300,16 +355,33 @@ export function createPiSessionControl(options) {
|
|
|
300
355
|
return { entries, ...(leafEntryId ? { leafEntryId } : {}) };
|
|
301
356
|
},
|
|
302
357
|
events(session) {
|
|
303
|
-
//
|
|
304
|
-
//
|
|
305
|
-
//
|
|
306
|
-
//
|
|
307
|
-
//
|
|
308
|
-
//
|
|
309
|
-
//
|
|
310
|
-
//
|
|
358
|
+
// ONE CALL IS ONE SUBSCRIPTION, and `ready` is its boundary: the moment after which nothing emitted for this
|
|
359
|
+
// session can be missed (session.ts, SessionEventStream). A second iteration of the same stream would be a
|
|
360
|
+
// second subscription inheriting the first one's readiness — a guarantee it never earned — so it is refused
|
|
361
|
+
// rather than documented. Matching the remote client, which opens one connection per stream.
|
|
362
|
+
//
|
|
363
|
+
// Registration happens on the FIRST next(), not at iterator creation: subscription semantics = you are
|
|
364
|
+
// subscribed while you iterate; an iterator obtained but never driven must not buffer. Teardown goes through
|
|
365
|
+
// Subscriber.close() so a `return()` on a QUIET stream resolves promptly instead of queueing behind a
|
|
366
|
+
// never-settling pull — without it every attach/detach against an idle session would leak a permanently
|
|
367
|
+
// registered subscriber.
|
|
368
|
+
let registered;
|
|
369
|
+
let unestablished;
|
|
370
|
+
const ready = new Promise((resolve, reject) => {
|
|
371
|
+
registered = resolve;
|
|
372
|
+
unestablished = reject;
|
|
373
|
+
});
|
|
374
|
+
// An awaiter gets the rejection; a consumer that only iterates would otherwise take an unhandled one for a
|
|
375
|
+
// cancellation it performed itself.
|
|
376
|
+
ready.catch(() => { });
|
|
377
|
+
let iterated = false;
|
|
311
378
|
return {
|
|
379
|
+
ready,
|
|
312
380
|
[Symbol.asyncIterator]() {
|
|
381
|
+
if (iterated) {
|
|
382
|
+
throw new Error("session events: this stream is one subscription — call events() again for another");
|
|
383
|
+
}
|
|
384
|
+
iterated = true;
|
|
313
385
|
let sub;
|
|
314
386
|
// `finished` is its own state: `sub === undefined` alone would conflate "not yet
|
|
315
387
|
// registered" with "terminated", and a post-done next() would silently REGISTER A FRESH
|
|
@@ -318,8 +390,12 @@ export function createPiSessionControl(options) {
|
|
|
318
390
|
let finished = false;
|
|
319
391
|
const cleanup = () => {
|
|
320
392
|
finished = true;
|
|
321
|
-
if (!sub)
|
|
393
|
+
if (!sub) {
|
|
394
|
+
// Cancelled before it registered: `ready` has a promise it cannot keep, and hanging on it is the one
|
|
395
|
+
// outcome a caller cannot diagnose. The remote client answers the same way.
|
|
396
|
+
unestablished(new Error("session events: cancelled before the subscription was established"));
|
|
322
397
|
return;
|
|
398
|
+
}
|
|
323
399
|
sub.close();
|
|
324
400
|
const set = subscribers.get(session);
|
|
325
401
|
if (set) {
|
|
@@ -341,6 +417,7 @@ export function createPiSessionControl(options) {
|
|
|
341
417
|
subscribers.set(session, set);
|
|
342
418
|
}
|
|
343
419
|
set.add(sub);
|
|
420
|
+
registered();
|
|
344
421
|
}
|
|
345
422
|
const result = await sub.next();
|
|
346
423
|
if (result.done)
|
|
@@ -386,9 +463,14 @@ export function createPiSessionControl(options) {
|
|
|
386
463
|
ok: false,
|
|
387
464
|
error: { code: BOUNDARY_COMMAND_FAILED_CODE, message: String(error), retryable: true },
|
|
388
465
|
});
|
|
466
|
+
const runBoundary = (command) => Effect.runPromise(Effect.scoped(command).pipe(Effect.catchTag("SessionBusy", () => Effect.succeed(busy())), Effect.catchCause((cause) => Effect.succeed(failed(portError(cause))))));
|
|
389
467
|
/** steer / follow_up / abort — the run actions. They reach the LIVE run through the controls
|
|
390
468
|
* registered with `run_started`; nothing durable is written. */
|
|
391
|
-
const runAction =
|
|
469
|
+
const runAction = (session, action) => Effect.runPromise(Effect.gen(function* () {
|
|
470
|
+
// pi dequeues a queued prompt by matching its text when it enters the conversation, and skips an empty one
|
|
471
|
+
// (`_handleAgentEvent`): an image-only prompt would sit in `pending` for the whole run and then read as dropped.
|
|
472
|
+
if ((action.type === "steer" || action.type === "follow_up") && action.prompt.text === "")
|
|
473
|
+
return invalid(`${action.type} needs non-empty text: a queued prompt is reported by its text`);
|
|
392
474
|
const run = active.get(session);
|
|
393
475
|
if (!run) {
|
|
394
476
|
// Run/compaction symmetry: an in-flight compaction is a model call too, and `abort` is its
|
|
@@ -425,23 +507,17 @@ export function createPiSessionControl(options) {
|
|
|
425
507
|
},
|
|
426
508
|
};
|
|
427
509
|
}
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
await run.controls.abort();
|
|
435
|
-
}
|
|
436
|
-
catch (error) {
|
|
437
|
-
// The run raced us to settlement, failed setup, or the engine refused: still pre-acceptance
|
|
438
|
-
// (nothing was queued), distinct from "no run existed". retryable: false for the same reason —
|
|
439
|
-
// the run is gone; consult state() before calling again.
|
|
440
|
-
return { ok: false, error: { code: RUN_COMMAND_FAILED_CODE, message: String(error), retryable: false } };
|
|
441
|
-
}
|
|
442
|
-
// Accepted: joined (or stopped) THIS run. The outcome arrives as run_settled.
|
|
510
|
+
const controls = run.controls;
|
|
511
|
+
yield* port(() => action.type === "steer"
|
|
512
|
+
? controls.steer(action.prompt)
|
|
513
|
+
: action.type === "follow_up"
|
|
514
|
+
? controls.followUp(action.prompt)
|
|
515
|
+
: controls.abort());
|
|
443
516
|
return { ok: true, runId: run.runId };
|
|
444
|
-
}
|
|
517
|
+
}).pipe(Effect.catchCause((cause) => Effect.succeed({
|
|
518
|
+
ok: false,
|
|
519
|
+
error: { code: RUN_COMMAND_FAILED_CODE, message: String(portError(cause)), retryable: false },
|
|
520
|
+
}))));
|
|
445
521
|
/**
|
|
446
522
|
* {@link Session.update} — validate the whole patch, take the lease once, hand the writes to the
|
|
447
523
|
* store, report what landed.
|
|
@@ -450,7 +526,7 @@ export function createPiSessionControl(options) {
|
|
|
450
526
|
* behind. What a value MEANS is decided here (a model spec against the registry, a level against
|
|
451
527
|
* the model it lands on); how a record takes it is the store's.
|
|
452
528
|
*/
|
|
453
|
-
const updateOf =
|
|
529
|
+
const updateOf = (session, patch) => runBoundary(Effect.gen(function* () {
|
|
454
530
|
// Keys, not values: a field this runtime does not know must not be silently skipped — that is a
|
|
455
531
|
// client typo, or a newer client talking to an older serve, and both need to hear about it.
|
|
456
532
|
const named = Object.keys(patch);
|
|
@@ -483,7 +559,7 @@ export function createPiSessionControl(options) {
|
|
|
483
559
|
// Sessions are created by invoke or copied by fork, never minted by an update: an unknown id is
|
|
484
560
|
// rejected, not turned into a ghost record. (Read-only handle — the WRITE one is opened under
|
|
485
561
|
// the lease below, and this one is discarded.)
|
|
486
|
-
const existing =
|
|
562
|
+
const existing = yield* port(() => sessions.openIfExists(session));
|
|
487
563
|
if (!existing)
|
|
488
564
|
return noSuchSession(session);
|
|
489
565
|
if (patch.leafEntryId !== undefined) {
|
|
@@ -520,30 +596,18 @@ export function createPiSessionControl(options) {
|
|
|
520
596
|
return invalid(`thinking level "${patch.thinkingLevel}" is not supported by ${target.provider}/${target.id} (allowed: ${levels.join(", ")})`);
|
|
521
597
|
}
|
|
522
598
|
}
|
|
523
|
-
//
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
try {
|
|
530
|
-
// HOW a record takes a property — order, the leaf pointer, the name pi rewrites — is the
|
|
531
|
-
// store's to know. This asks for the writes and is told what landed.
|
|
532
|
-
applied = await sessions.applyProperties(session, {
|
|
599
|
+
// Release before publishing state_changed: an observer may immediately issue another command.
|
|
600
|
+
const applied = yield* Effect.scoped(Effect.gen(function* () {
|
|
601
|
+
yield* acquireSessionLease(b.lease, session);
|
|
602
|
+
// portJoin, not port: the lease is held for the duration of this write, so an interrupted
|
|
603
|
+
// fiber must not release it while the store is still writing (effect-port.ts, rule 1).
|
|
604
|
+
return yield* portJoin(() => sessions.applyProperties(session, {
|
|
533
605
|
...(patch.name !== undefined ? { name: patch.name } : {}),
|
|
534
606
|
...(model ? { model: { provider: model.provider, id: model.id } } : {}),
|
|
535
607
|
...(patch.thinkingLevel !== undefined ? { thinkingLevel: patch.thinkingLevel } : {}),
|
|
536
608
|
...(patch.leafEntryId !== undefined ? { leafEntryId: patch.leafEntryId } : {}),
|
|
537
|
-
});
|
|
538
|
-
}
|
|
539
|
-
catch (error) {
|
|
540
|
-
// Opening the record failed — nothing was written; the same patch may succeed on retry. The
|
|
541
|
-
// lease is freed by the `finally` on the way out, once.
|
|
542
|
-
return failed(error);
|
|
543
|
-
}
|
|
544
|
-
finally {
|
|
545
|
-
release();
|
|
546
|
-
}
|
|
609
|
+
}));
|
|
610
|
+
}));
|
|
547
611
|
if (!applied)
|
|
548
612
|
return noSuchSession(session); // vanished in the window: same condition, same code
|
|
549
613
|
if (applied.landed.length > 0) {
|
|
@@ -562,6 +626,15 @@ export function createPiSessionControl(options) {
|
|
|
562
626
|
log.warn(`[fastagent] session ${session}: updated, settings unresolvable: ${String(error)}`);
|
|
563
627
|
}
|
|
564
628
|
}
|
|
629
|
+
// A moved head reads a different newest answer, and another model has another window: the event carries
|
|
630
|
+
// the usage the session now has, and carries none when it has none. Read back through `state()`, the one
|
|
631
|
+
// derivation; a read failure only costs the field, since the write has already landed.
|
|
632
|
+
const usage = applied.landed.some((f) => f === "leafEntryId" || f === "model")
|
|
633
|
+
? yield* port(() => reads.state(session)).pipe(Effect.map((state) => state.usage), Effect.catchCause((cause) => Effect.sync(() => {
|
|
634
|
+
log.warn(`[fastagent] session ${session}: updated, usage unreadable: ${String(portError(cause))}`);
|
|
635
|
+
return undefined;
|
|
636
|
+
})))
|
|
637
|
+
: undefined;
|
|
565
638
|
emitOwn(session, {
|
|
566
639
|
type: "state_changed",
|
|
567
640
|
timestamp: Date.now(),
|
|
@@ -571,6 +644,7 @@ export function createPiSessionControl(options) {
|
|
|
571
644
|
? { model: `${settings.model.provider}/${settings.model.id}`, thinkingLevel: settings.thinkingLevel }
|
|
572
645
|
: {}),
|
|
573
646
|
...(applied.landed.includes("name") && applied.name ? { name: applied.name } : {}),
|
|
647
|
+
...(usage ? { usage } : {}),
|
|
574
648
|
},
|
|
575
649
|
});
|
|
576
650
|
}
|
|
@@ -589,57 +663,47 @@ export function createPiSessionControl(options) {
|
|
|
589
663
|
};
|
|
590
664
|
}
|
|
591
665
|
return { ok: true };
|
|
592
|
-
};
|
|
593
|
-
/**
|
|
594
|
-
*
|
|
595
|
-
*
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
}
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
// The SAME settings pi will use inside compact(): asking with different thresholds would
|
|
634
|
-
// either reject a compaction pi would have run, or admit one it refuses — and its refusal
|
|
635
|
-
// arrives too late to be a pre-acceptance answer.
|
|
636
|
-
const path = bound.sessionManager.getBranch();
|
|
637
|
-
if (!hasCompactableHistory(path, bound.settingsManager.getCompactionSettings().keepRecentTokens)) {
|
|
638
|
-
teardown();
|
|
639
|
-
release();
|
|
640
|
-
// A no-op, not a failure — its OWN code (the NO_ACTIVE_RUN pattern): a client must
|
|
641
|
-
// machine-distinguish "give up" from "call again once the session grows", and branching on
|
|
642
|
-
// message prose is forbidden by contract.
|
|
666
|
+
}));
|
|
667
|
+
/** Admission waits for binding and pi's own admission (its cut point), not the model call.
|
|
668
|
+
* Its execution scope survives the control response; completion is published only after releasing the shared
|
|
669
|
+
* lease. */
|
|
670
|
+
const compactOf = (session, instructions) => {
|
|
671
|
+
const admitted = Deferred.makeUnsafe();
|
|
672
|
+
let accepted = false;
|
|
673
|
+
let aborted = false;
|
|
674
|
+
const work = Effect.scoped(Effect.gen(function* () {
|
|
675
|
+
const b = boundary;
|
|
676
|
+
if (!b)
|
|
677
|
+
return unsupported("compact()");
|
|
678
|
+
const existing = yield* port(() => sessions.openIfExists(session));
|
|
679
|
+
if (!existing)
|
|
680
|
+
return noSuchSession(session);
|
|
681
|
+
yield* acquireSessionLease(b.lease, session);
|
|
682
|
+
const bound = yield* acquireSession(b.sessionFactory, session);
|
|
683
|
+
yield* Effect.acquireRelease(Effect.try({
|
|
684
|
+
try: () => bound.subscribe((event) => {
|
|
685
|
+
if (event.type !== "summarization_retry_scheduled")
|
|
686
|
+
return;
|
|
687
|
+
log.warn(`[fastagent] compaction retry ${event.attempt}/${event.maxAttempts} in ${event.delayMs}ms (session ${session}): ${event.errorMessage}`);
|
|
688
|
+
emitOwn(session, toRetryScheduledEvent(event));
|
|
689
|
+
}),
|
|
690
|
+
catch: (error) => new PortFailure(error),
|
|
691
|
+
}), (unsubscribe) => portCleanup("unsubscribe", unsubscribe));
|
|
692
|
+
// Admission is pi's own decision, observed rather than predicted: a prediction has to track pi's private
|
|
693
|
+
// cut-point rules release by release. It settles before the model call.
|
|
694
|
+
const compaction = startCompaction(bound, instructions, () => {
|
|
695
|
+
accepted = true;
|
|
696
|
+
compacting.set(session, {
|
|
697
|
+
abort: () => {
|
|
698
|
+
aborted = true;
|
|
699
|
+
bound.abortCompaction();
|
|
700
|
+
},
|
|
701
|
+
});
|
|
702
|
+
emitOwn(session, { type: "compaction_started", timestamp: Date.now(), data: {} });
|
|
703
|
+
Deferred.doneUnsafe(admitted, Effect.succeed({ ok: true }));
|
|
704
|
+
});
|
|
705
|
+
const admission = yield* portAbort("compact admission", () => compaction.admission, () => bound.abortCompaction());
|
|
706
|
+
if (admission === "nothing_to_compact") {
|
|
643
707
|
return {
|
|
644
708
|
ok: false,
|
|
645
709
|
error: {
|
|
@@ -649,54 +713,34 @@ export function createPiSessionControl(options) {
|
|
|
649
713
|
},
|
|
650
714
|
};
|
|
651
715
|
}
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
compacting.set(session, {
|
|
662
|
-
abort: () => {
|
|
663
|
-
aborted = true;
|
|
664
|
-
bound.abortCompaction();
|
|
665
|
-
},
|
|
666
|
-
});
|
|
667
|
-
emitOwn(session, { type: "compaction_started", timestamp: Date.now(), data: {} });
|
|
668
|
-
void (async () => {
|
|
669
|
-
let outcome;
|
|
670
|
-
// Retries are otherwise invisible between compaction_started and _finished — surface each
|
|
671
|
-
// backoff so a long gap is diagnosable (not confusable with a hang): as a session event for
|
|
672
|
-
// attached observers, as a warn for server logs.
|
|
673
|
-
const unsub = bound.subscribe((event) => {
|
|
674
|
-
if (event.type === "compaction_start" && event.reason === "manual" && aborted)
|
|
675
|
-
bound.abortCompaction();
|
|
676
|
-
if (event.type !== "summarization_retry_scheduled")
|
|
677
|
-
return;
|
|
678
|
-
log.warn(`[fastagent] compaction retry ${event.attempt}/${event.maxAttempts} in ${event.delayMs}ms (session ${session}): ${event.errorMessage}`);
|
|
679
|
-
emitOwn(session, toRetryScheduledEvent(event));
|
|
680
|
-
});
|
|
681
|
-
try {
|
|
682
|
-
const done = await bound.compact(instructions);
|
|
683
|
-
outcome = { summary: done.summary };
|
|
716
|
+
// Any other error pi raises before admission. A served session has no known trigger: it always binds a
|
|
717
|
+
// model, and pi swallows a credential failure here and meets it at the model call (compaction_finished).
|
|
718
|
+
// NOT retryable all the same: it is pi's decision on this session, not a transport or lease fault that
|
|
719
|
+
// clears on its own, so repeating the call would meet it again.
|
|
720
|
+
if (admission !== "admitted") {
|
|
721
|
+
return {
|
|
722
|
+
ok: false,
|
|
723
|
+
error: { code: BOUNDARY_COMMAND_FAILED_CODE, message: String(admission.refused), retryable: false },
|
|
724
|
+
};
|
|
684
725
|
}
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
726
|
+
const done = yield* portAbort("compact", () => compaction.done, () => bound.abortCompaction());
|
|
727
|
+
return { summary: done.summary };
|
|
728
|
+
})).pipe(Effect.catchTag("SessionBusy", () => Effect.succeed(busy())), Effect.catchCause((cause) => Effect.succeed(failed(portError(cause)))), Effect.tap((result) => Effect.sync(() => {
|
|
729
|
+
if (accepted) {
|
|
730
|
+
compacting.delete(session);
|
|
731
|
+
emitOwn(session, {
|
|
732
|
+
type: "compaction_finished",
|
|
733
|
+
timestamp: Date.now(),
|
|
734
|
+
data: "summary" in result ? result : aborted ? { aborted: true } : { error: result.error.message },
|
|
735
|
+
});
|
|
736
|
+
if ("summary" in result)
|
|
737
|
+
publishUsage(session);
|
|
690
738
|
}
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
release();
|
|
697
|
-
emitOwn(session, { type: "compaction_finished", timestamp: Date.now(), data: outcome });
|
|
698
|
-
})();
|
|
699
|
-
return { ok: true };
|
|
739
|
+
Deferred.doneUnsafe(admitted, Effect.succeed("summary" in result ? { ok: true } : result));
|
|
740
|
+
})));
|
|
741
|
+
// The admitted operation, rather than the request waiting on this Deferred, owns the scope.
|
|
742
|
+
Effect.runFork(work);
|
|
743
|
+
return Effect.runPromise(Deferred.await(admitted));
|
|
700
744
|
};
|
|
701
745
|
/**
|
|
702
746
|
* Copy a history into a new session. IDEMPOTENT by construction: `into` is the caller's id, so a
|
|
@@ -705,7 +749,7 @@ export function createPiSessionControl(options) {
|
|
|
705
749
|
* safe rather than merely quiet: the same `into` naming a session that came from somewhere else is
|
|
706
750
|
* a rejection, not an overwrite.
|
|
707
751
|
*/
|
|
708
|
-
const forkOf =
|
|
752
|
+
const forkOf = (options) => runBoundary(Effect.gen(function* () {
|
|
709
753
|
const { from, at, into } = options;
|
|
710
754
|
/** WHICH fork this is: source + branch point. Two forks of one session at different entries are
|
|
711
755
|
* different requests, so a retry of one must not be answered by the other. */
|
|
@@ -719,7 +763,7 @@ export function createPiSessionControl(options) {
|
|
|
719
763
|
if (!isAddressableSession(into)) {
|
|
720
764
|
return invalid(`${JSON.stringify(into)} cannot be a session id — the control plane could not address it`);
|
|
721
765
|
}
|
|
722
|
-
const source =
|
|
766
|
+
const source = yield* port(() => sessions.openIfExists(from));
|
|
723
767
|
if (!source)
|
|
724
768
|
return noSuchSession(from);
|
|
725
769
|
// The entry predicate is the one `entries()` publishes by, so "everything published is forkable"
|
|
@@ -728,7 +772,7 @@ export function createPiSessionControl(options) {
|
|
|
728
772
|
if (!entry || !isNavigable(entry)) {
|
|
729
773
|
return invalid(`entry "${at}" is not a forkable position in session "${from}" — entries() lists the ids`);
|
|
730
774
|
}
|
|
731
|
-
const existingTarget =
|
|
775
|
+
const existingTarget = yield* port(() => sessions.openIfExists(into));
|
|
732
776
|
if (existingTarget) {
|
|
733
777
|
// Already forked from HERE: the request already happened, so answering ok is the truth rather
|
|
734
778
|
// than a convenience. Anything else under that id is a different history, and saying yes would
|
|
@@ -742,60 +786,33 @@ export function createPiSessionControl(options) {
|
|
|
742
786
|
// `into`, or a second fork from a DIFFERENT source (whose source lease is another key entirely),
|
|
743
787
|
// otherwise lands between that check and this write. tryAcquire never blocks, so taking two
|
|
744
788
|
// cannot deadlock.
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
const
|
|
749
|
-
if (
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
try {
|
|
754
|
-
// Holding the lease is not the same as having looked: re-asked under it, as the update path
|
|
755
|
-
// re-opens its record, so an id taken inside the window is a payload error rather than a
|
|
756
|
-
// store failure the client would read as retryable.
|
|
757
|
-
const raced = await sessions.openIfExists(into);
|
|
758
|
-
if (raced) {
|
|
759
|
-
return forkProvenance(raced) === provenance
|
|
760
|
-
? { ok: true }
|
|
761
|
-
: invalid(`session "${into}" already exists with a different history — fork mints nothing over it`);
|
|
762
|
-
}
|
|
763
|
-
await sessions.fork(from, at, into, provenance);
|
|
764
|
-
}
|
|
765
|
-
catch (error) {
|
|
766
|
-
// Nothing durable landed: the copy is staged and published by rename.
|
|
767
|
-
return failed(error);
|
|
768
|
-
}
|
|
769
|
-
finally {
|
|
770
|
-
releaseInto();
|
|
771
|
-
release();
|
|
789
|
+
yield* acquireSessionLease(b.lease, from);
|
|
790
|
+
yield* acquireSessionLease(b.lease, into);
|
|
791
|
+
// Recheck under both leases: another creator may have taken the target since validation.
|
|
792
|
+
const raced = yield* port(() => sessions.openIfExists(into));
|
|
793
|
+
if (raced) {
|
|
794
|
+
return forkProvenance(raced) === provenance
|
|
795
|
+
? { ok: true }
|
|
796
|
+
: invalid(`session "${into}" already exists with a different history — fork mints nothing over it`);
|
|
772
797
|
}
|
|
798
|
+
// portJoin: both leases are held for this write and must outlive it (effect-port.ts, rule 1).
|
|
799
|
+
yield* portJoin(() => sessions.fork(from, at, into, provenance));
|
|
773
800
|
return { ok: true };
|
|
774
|
-
};
|
|
775
|
-
const deleteOf =
|
|
801
|
+
}));
|
|
802
|
+
const deleteOf = (session) => runBoundary(Effect.gen(function* () {
|
|
776
803
|
const b = boundary;
|
|
777
804
|
if (!b)
|
|
778
805
|
return unsupported("delete()");
|
|
779
|
-
const existing =
|
|
806
|
+
const existing = yield* port(() => sessions.openIfExists(session));
|
|
780
807
|
if (!existing)
|
|
781
808
|
return noSuchSession(session);
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
return
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
if (!(await sessions.delete(session)))
|
|
790
|
-
return noSuchSession(session);
|
|
791
|
-
}
|
|
792
|
-
catch (error) {
|
|
793
|
-
// A delete that throws left the record in place.
|
|
794
|
-
return failed(error);
|
|
795
|
-
}
|
|
796
|
-
finally {
|
|
797
|
-
release();
|
|
798
|
-
}
|
|
809
|
+
const removed = yield* Effect.scoped(Effect.gen(function* () {
|
|
810
|
+
yield* acquireSessionLease(b.lease, session);
|
|
811
|
+
// portJoin: the lease must outlive the removal (effect-port.ts, rule 1).
|
|
812
|
+
return yield* portJoin(() => sessions.delete(session));
|
|
813
|
+
}));
|
|
814
|
+
if (!removed)
|
|
815
|
+
return noSuchSession(session);
|
|
799
816
|
// The session is gone, so its live streams have nothing left to report: end them rather than
|
|
800
817
|
// hold connections open on a record that no longer exists. A client's reconnect then reads an
|
|
801
818
|
// empty `state()`, which is the truth.
|
|
@@ -803,7 +820,7 @@ export function createPiSessionControl(options) {
|
|
|
803
820
|
sub.close();
|
|
804
821
|
subscribers.delete(session);
|
|
805
822
|
return { ok: true };
|
|
806
|
-
};
|
|
823
|
+
}));
|
|
807
824
|
const control = {
|
|
808
825
|
capabilities: reads.capabilities,
|
|
809
826
|
commands: reads.commands,
|