@fastagent-sh/fastagent 0.24.4 → 0.25.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 +53 -82
- package/dist/agent-api.d.ts +452 -0
- package/dist/agent-api.js +201 -0
- package/dist/answer-text.d.ts +8 -0
- package/dist/answer-text.js +18 -0
- package/dist/atomic-write.d.ts +2 -2
- package/dist/atomic-write.js +2 -2
- package/dist/authoring.d.ts +77 -0
- package/dist/authoring.js +238 -0
- package/dist/channel.d.ts +29 -15
- package/dist/channel.js +4 -1
- package/dist/channels/agentcore-protocol.d.ts +36 -50
- package/dist/channels/agentcore-protocol.js +3 -3
- package/dist/channels/agentcore-service.d.ts +8 -18
- package/dist/channels/agentcore-service.js +108 -127
- package/dist/channels/agentcore.d.ts +8 -24
- package/dist/channels/agentcore.js +32 -65
- package/dist/channels/body.d.ts +0 -16
- package/dist/channels/body.js +0 -25
- package/dist/channels/busy.js +6 -1
- package/dist/channels/context.d.ts +4 -0
- package/dist/channels/context.js +35 -0
- package/dist/channels/discover.d.ts +18 -20
- package/dist/channels/discover.js +41 -99
- package/dist/channels/feishu/feishu-api.d.ts +54 -4
- package/dist/channels/feishu/feishu-api.js +192 -149
- package/dist/channels/feishu/feishu.d.ts +3 -0
- package/dist/channels/feishu/feishu.js +117 -106
- package/dist/channels/feishu/history.d.ts +55 -0
- package/dist/channels/feishu/history.js +199 -0
- package/dist/channels/feishu/invoke-turn.d.ts +13 -11
- package/dist/channels/feishu/invoke-turn.js +9 -10
- package/dist/channels/feishu/model.d.ts +2 -1
- package/dist/channels/feishu/normalize.js +100 -0
- package/dist/channels/feishu/preview.d.ts +2 -2
- package/dist/channels/feishu/register-webhook.d.ts +1 -0
- package/dist/channels/feishu/register-webhook.js +3 -2
- package/dist/channels/feishu/scaffold/channel.ts +65 -46
- package/dist/channels/feishu/scaffold/feishu-send.ts +38 -33
- package/dist/channels/feishu/scaffold/feishu-threads.ts +25 -0
- package/dist/channels/feishu/setup-mode.d.ts +45 -25
- package/dist/channels/feishu/setup-mode.js +74 -23
- package/dist/channels/feishu/shared-api.d.ts +24 -6
- package/dist/channels/feishu/shared-api.js +30 -24
- package/dist/channels/kit/context-buffer.d.ts +11 -2
- package/dist/channels/kit/context-buffer.js +7 -4
- package/dist/channels/kit/delivery.d.ts +19 -3
- package/dist/channels/kit/delivery.js +74 -15
- package/dist/channels/kit/event-stream.js +2 -1
- package/dist/channels/kit/invoke-turn-kit.d.ts +41 -41
- package/dist/channels/kit/invoke-turn-kit.js +62 -71
- package/dist/channels/kit/place-history.d.ts +107 -0
- package/dist/channels/kit/place-history.js +182 -0
- package/dist/channels/kit/preview-kit.d.ts +9 -5
- package/dist/channels/kit/preview-kit.js +36 -15
- package/dist/channels/kit/room-threads.d.ts +46 -0
- package/dist/channels/kit/room-threads.js +81 -0
- package/dist/channels/kit/seen.js +1 -1
- package/dist/channels/kit/stop-command.d.ts +6 -3
- package/dist/channels/kit/stop-command.js +20 -18
- package/dist/channels/kit/thread-fork.d.ts +17 -0
- package/dist/channels/kit/thread-fork.js +43 -0
- package/dist/channels/kit/thread-participants.js +1 -1
- package/dist/channels/kit/transport.d.ts +5 -0
- package/dist/channels/kit/transport.js +5 -0
- package/dist/channels/kit/turn-runner.d.ts +10 -9
- package/dist/channels/kit/turn-runner.js +17 -19
- package/dist/channels/kit/turn-store.d.ts +5 -4
- package/dist/channels/kit/turn-store.js +5 -5
- package/dist/channels/lark/lark.d.ts +3 -0
- package/dist/channels/lark/lark.js +5 -0
- package/dist/channels/lark/onboard.d.ts +5 -3
- package/dist/channels/lark/onboard.js +14 -14
- package/dist/channels/lark/scaffold/channel.ts +67 -44
- package/dist/channels/lark/scaffold/lark-send.ts +38 -33
- package/dist/channels/lark/scaffold/lark-threads.ts +25 -0
- package/dist/channels/respond.d.ts +0 -4
- package/dist/channels/respond.js +1 -1
- package/dist/channels/router.d.ts +18 -0
- package/dist/channels/router.js +68 -0
- package/dist/channels/serve.d.ts +5 -70
- package/dist/channels/serve.js +105 -287
- package/dist/channels/slack/history.d.ts +42 -0
- package/dist/channels/slack/history.js +282 -0
- package/dist/channels/slack/invoke-turn.d.ts +12 -5
- package/dist/channels/slack/invoke-turn.js +7 -5
- package/dist/channels/slack/manifest.d.ts +1 -4
- package/dist/channels/slack/manifest.js +22 -10
- package/dist/channels/slack/model.d.ts +2 -1
- package/dist/channels/slack/onboard.d.ts +0 -2
- package/dist/channels/slack/onboard.js +2 -4
- package/dist/channels/slack/onboarding-state.d.ts +0 -2
- package/dist/channels/slack/onboarding-state.js +0 -1
- package/dist/channels/slack/parse.d.ts +0 -5
- package/dist/channels/slack/parse.js +1 -16
- package/dist/channels/slack/preview.d.ts +2 -3
- package/dist/channels/slack/preview.js +78 -52
- package/dist/channels/slack/register-webhook.d.ts +1 -0
- package/dist/channels/slack/register-webhook.js +6 -5
- package/dist/channels/slack/scaffold/channel.ts +23 -24
- package/dist/channels/slack/scaffold/slack-send.ts +50 -43
- package/dist/channels/slack/scaffold/slack-threads.ts +29 -0
- package/dist/channels/slack/setup-server.d.ts +19 -0
- package/dist/channels/slack/setup-server.js +113 -11
- package/dist/channels/slack/shared-api.d.ts +15 -4
- package/dist/channels/slack/shared-api.js +19 -14
- package/dist/channels/slack/slack-api.d.ts +61 -2
- package/dist/channels/slack/slack-api.js +28 -5
- package/dist/channels/slack/slack.js +97 -52
- package/dist/channels/slack/welcomed.js +1 -1
- package/dist/channels/telegram/context-buffer.d.ts +6 -1
- package/dist/channels/telegram/context-buffer.js +19 -0
- package/dist/channels/telegram/invoke-turn.d.ts +8 -4
- package/dist/channels/telegram/invoke-turn.js +7 -7
- package/dist/channels/telegram/parse.d.ts +7 -2
- package/dist/channels/telegram/parse.js +7 -0
- package/dist/channels/telegram/preview.d.ts +2 -2
- package/dist/channels/telegram/register-webhook.d.ts +1 -0
- package/dist/channels/telegram/register-webhook.js +3 -2
- package/dist/channels/telegram/scaffold/channel.ts +27 -28
- package/dist/channels/telegram/scaffold/telegram-send.ts +61 -108
- package/dist/channels/telegram/shared-api.d.ts +34 -0
- package/dist/channels/telegram/shared-api.js +79 -0
- package/dist/channels/telegram/telegram-api.d.ts +15 -5
- package/dist/channels/telegram/telegram-api.js +10 -6
- package/dist/channels/telegram/telegram.js +27 -21
- package/dist/channels/telegram/turn-store.js +1 -1
- package/dist/channels/{kit/turn-queue.d.ts → turn-queue.d.ts} +2 -2
- package/dist/channels/{kit/turn-queue.js → turn-queue.js} +10 -10
- package/dist/cli/add-feishu.d.ts +42 -12
- package/dist/cli/add-feishu.js +177 -124
- package/dist/cli/add-slack.d.ts +8 -3
- package/dist/cli/add-slack.js +115 -63
- package/dist/cli/auth-view.d.ts +10 -3
- package/dist/cli/auth-view.js +8 -6
- package/dist/cli/box-login.d.ts +17 -7
- package/dist/cli/box-login.js +46 -12
- package/dist/cli/commands/add.d.ts +15 -4
- package/dist/cli/commands/add.js +252 -143
- package/dist/cli/commands/auth.d.ts +6 -0
- package/dist/cli/commands/auth.js +50 -0
- package/dist/cli/commands/content.d.ts +6 -0
- package/dist/cli/commands/content.js +45 -0
- package/dist/cli/commands/deploy/agentcore.js +50 -77
- package/dist/cli/commands/deploy/docker.js +38 -39
- package/dist/cli/commands/deploy/fly.js +21 -18
- package/dist/cli/commands/deploy/host-login.d.ts +26 -0
- package/dist/cli/commands/deploy/host-login.js +90 -0
- package/dist/cli/commands/deploy/railway.js +21 -26
- package/dist/cli/commands/deploy/shared.d.ts +45 -17
- package/dist/cli/commands/deploy/shared.js +34 -15
- package/dist/cli/commands/deploy.d.ts +0 -4
- package/dist/cli/commands/deploy.js +237 -50
- package/dist/cli/commands/destroy.js +8 -6
- package/dist/cli/commands/dev.d.ts +0 -2
- package/dist/cli/commands/dev.js +20 -19
- package/dist/cli/commands/env.d.ts +12 -0
- package/dist/cli/commands/env.js +190 -0
- package/dist/cli/commands/info.js +95 -156
- package/dist/cli/commands/init.d.ts +2 -2
- package/dist/cli/commands/init.js +35 -33
- package/dist/cli/commands/invoke.d.ts +6 -0
- package/dist/cli/commands/invoke.js +131 -16
- package/dist/cli/commands/login.d.ts +31 -2
- package/dist/cli/commands/login.js +413 -64
- package/dist/cli/commands/logout.d.ts +4 -0
- package/dist/cli/commands/logout.js +53 -0
- package/dist/cli/commands/logs.js +8 -6
- package/dist/cli/commands/models.d.ts +2 -1
- package/dist/cli/commands/models.js +39 -37
- package/dist/cli/commands/schedules.d.ts +21 -0
- package/dist/cli/commands/schedules.js +143 -0
- package/dist/cli/commands/start.d.ts +11 -10
- package/dist/cli/commands/start.js +35 -62
- package/dist/cli/content-view.d.ts +9 -0
- package/dist/cli/content-view.js +26 -0
- package/dist/cli/fail.d.ts +9 -12
- package/dist/cli/fail.js +17 -14
- package/dist/cli/invoke-stream.d.ts +12 -3
- package/dist/cli/invoke-stream.js +85 -23
- package/dist/cli/kernel.d.ts +9 -2
- package/dist/cli/kernel.js +15 -6
- package/dist/cli/login-relay.d.ts +3 -2
- package/dist/cli/login-relay.js +18 -6
- package/dist/cli/models-view.d.ts +5 -1
- package/dist/cli/models-view.js +1 -1
- package/dist/cli/person.d.ts +10 -0
- package/dist/cli/person.js +23 -0
- package/dist/cli/program.js +421 -203
- package/dist/cli/serve.d.ts +13 -63
- package/dist/cli/serve.js +31 -121
- package/dist/cli/shared.d.ts +21 -48
- package/dist/cli/shared.js +68 -89
- package/dist/cli/ticket.d.ts +136 -0
- package/dist/cli/ticket.js +476 -0
- package/dist/client.d.ts +7 -0
- package/dist/client.js +2 -0
- package/dist/content/declare.d.ts +70 -0
- package/dist/content/declare.js +163 -0
- package/dist/content/file.d.ts +32 -0
- package/dist/content/file.js +119 -0
- package/dist/content/git.d.ts +41 -0
- package/dist/content/git.js +217 -0
- package/dist/content/mount.d.ts +2 -0
- package/dist/content/mount.js +15 -0
- package/dist/content/resolve.d.ts +52 -0
- package/dist/content/resolve.js +151 -0
- package/dist/content/source.d.ts +27 -0
- package/dist/content/source.js +53 -0
- package/dist/conversation-sessions.d.ts +26 -0
- package/dist/conversation-sessions.js +99 -0
- package/dist/declared-secrets.d.ts +19 -37
- package/dist/declared-secrets.js +16 -58
- package/dist/deploy/agentcore/destroy.d.ts +3 -3
- package/dist/deploy/agentcore/destroy.js +46 -28
- package/dist/deploy/agentcore/forwarder.js +105 -22
- package/dist/deploy/agentcore/logs.js +1 -1
- package/dist/deploy/agentcore/plan.d.ts +10 -24
- package/dist/deploy/agentcore/plan.js +31 -135
- package/dist/deploy/agentcore/run.d.ts +1 -1
- package/dist/deploy/agentcore/run.js +8 -5
- package/dist/deploy/agentcore/shell.d.ts +1 -1
- package/dist/deploy/agentcore/shell.js +5 -5
- package/dist/deploy/api.d.ts +34 -0
- package/dist/deploy/api.js +50 -0
- package/dist/deploy/box-shell.d.ts +6 -0
- package/dist/deploy/box-shell.js +7 -0
- package/dist/deploy/build-context.d.ts +7 -5
- package/dist/deploy/build-context.js +41 -35
- package/dist/deploy/channel-ingress.d.ts +6 -1
- package/dist/deploy/channel-ingress.js +18 -5
- package/dist/deploy/container.d.ts +22 -9
- package/dist/deploy/container.js +96 -55
- package/dist/deploy/docker/plan.d.ts +3 -2
- package/dist/deploy/docker/plan.js +13 -14
- package/dist/deploy/docker/run.d.ts +2 -2
- package/dist/deploy/docker/run.js +8 -4
- package/dist/deploy/fly/plan.d.ts +1 -1
- package/dist/deploy/fly/plan.js +12 -21
- package/dist/deploy/fly/run.d.ts +10 -6
- package/dist/deploy/fly/run.js +11 -6
- package/dist/deploy/hosts.d.ts +2 -1
- package/dist/deploy/hosts.js +2 -1
- package/dist/deploy/preflight.d.ts +21 -23
- package/dist/deploy/preflight.js +222 -201
- package/dist/deploy/railway/plan.d.ts +8 -6
- package/dist/deploy/railway/plan.js +15 -58
- package/dist/deploy/railway/run.d.ts +8 -3
- package/dist/deploy/railway/run.js +12 -8
- package/dist/deploy/residency.d.ts +5 -11
- package/dist/deploy/residency.js +0 -6
- package/dist/deploy/runner.d.ts +1 -1
- package/dist/deploy/runner.js +1 -1
- package/dist/deploy/secrets.d.ts +16 -2
- package/dist/deploy/secrets.js +33 -6
- package/dist/deploy/workspace.d.ts +11 -6
- package/dist/deploy/workspace.js +55 -38
- package/dist/dev-supervisor.d.ts +11 -2
- package/dist/dev-supervisor.js +51 -26
- package/dist/effect-port.d.ts +4 -4
- package/dist/effect-port.js +4 -4
- package/dist/environment/apt.d.ts +4 -0
- package/dist/environment/apt.js +32 -0
- package/dist/environment/declare.d.ts +59 -0
- package/dist/environment/declare.js +210 -0
- package/dist/environment/mise.d.ts +50 -0
- package/dist/environment/mise.js +268 -0
- package/dist/feishu.d.ts +2 -2
- package/dist/feishu.js +2 -2
- package/dist/git.d.ts +13 -0
- package/dist/git.js +55 -0
- package/dist/harnesses/models/catalog.d.ts +20 -0
- package/dist/harnesses/models/catalog.js +254 -0
- package/dist/{engines/pi/auth.d.ts → harnesses/models/credentials.d.ts} +3 -3
- package/dist/{engines/pi/auth.js → harnesses/models/credentials.js} +3 -3
- package/dist/harnesses/models/environment.d.ts +43 -0
- package/dist/harnesses/models/environment.js +82 -0
- package/dist/{engines/pi → harnesses/models}/login.d.ts +57 -26
- package/dist/{engines/pi → harnesses/models}/login.js +187 -79
- package/dist/harnesses/models/models.d.ts +83 -0
- package/dist/harnesses/models/models.js +158 -0
- package/dist/harnesses/pi-durable/agent.d.ts +42 -0
- package/dist/harnesses/pi-durable/agent.js +579 -0
- package/dist/harnesses/pi-durable/defaults.d.ts +28 -0
- package/dist/harnesses/pi-durable/defaults.js +26 -0
- package/dist/harnesses/pi-durable/define.d.ts +77 -0
- package/dist/harnesses/pi-durable/define.js +28 -0
- package/dist/harnesses/pi-durable/entries.d.ts +32 -0
- package/dist/harnesses/pi-durable/entries.js +126 -0
- package/dist/harnesses/pi-durable/events.d.ts +25 -0
- package/dist/harnesses/pi-durable/events.js +238 -0
- package/dist/harnesses/pi-durable/ids.d.ts +16 -0
- package/dist/harnesses/pi-durable/ids.js +55 -0
- package/dist/harnesses/pi-durable/messages.d.ts +41 -0
- package/dist/harnesses/pi-durable/messages.js +116 -0
- package/dist/harnesses/pi-durable/open.d.ts +88 -0
- package/dist/harnesses/pi-durable/open.js +329 -0
- package/dist/harnesses/pi-durable/reads.d.ts +30 -0
- package/dist/harnesses/pi-durable/reads.js +94 -0
- package/dist/harnesses/pi-durable/registry.d.ts +18 -0
- package/dist/harnesses/pi-durable/registry.js +56 -0
- package/dist/harnesses/pi-durable/release.d.ts +13 -0
- package/dist/harnesses/pi-durable/release.js +40 -0
- package/dist/harnesses/pi-durable/serve.d.ts +80 -0
- package/dist/harnesses/pi-durable/serve.js +156 -0
- package/dist/harnesses/pi-durable/sessions.d.ts +27 -0
- package/dist/harnesses/pi-durable/sessions.js +47 -0
- package/dist/harnesses/pi-durable/storage.d.ts +4 -0
- package/dist/harnesses/pi-durable/storage.js +49 -0
- package/dist/harnesses/pi-durable/stream.d.ts +20 -0
- package/dist/harnesses/pi-durable/stream.js +221 -0
- package/dist/harnesses/pi-durable/text.d.ts +30 -0
- package/dist/harnesses/pi-durable/text.js +292 -0
- package/dist/harnesses/pi-durable/wake.d.ts +4 -0
- package/dist/harnesses/pi-durable/wake.js +81 -0
- package/dist/http/client.d.ts +6 -0
- package/dist/http/client.js +256 -0
- package/dist/http/contract.d.ts +541 -0
- package/dist/http/contract.js +179 -0
- package/dist/http/server.d.ts +34 -0
- package/dist/http/server.js +194 -0
- package/dist/index.d.ts +17 -4
- package/dist/index.js +10 -5
- package/dist/lark.d.ts +2 -2
- package/dist/lark.js +2 -2
- package/dist/locked-file.d.ts +17 -0
- package/dist/{engines/pi/locked-file.js → locked-file.js} +16 -10
- package/dist/once.d.ts +6 -0
- package/dist/once.js +23 -0
- package/dist/paths.d.ts +36 -52
- package/dist/paths.js +66 -182
- package/dist/runtime.d.ts +25 -2
- package/dist/runtime.js +27 -0
- package/dist/scaffold/add-channel.d.ts +14 -27
- package/dist/scaffold/add-channel.js +82 -180
- package/dist/scaffold/init.d.ts +17 -18
- package/dist/scaffold/init.js +87 -99
- package/dist/scaffold/templates/SYSTEM.md +12 -0
- package/dist/scaffold/templates/env.example +4 -10
- package/dist/scaffold/templates/fastagent.config.ts +21 -33
- package/dist/scaffold/templates/gitignore +8 -1
- package/dist/scaffold/templates/skills/writing-great-skills/SKILL.md +1 -2
- package/dist/scaffold/templates.d.ts +9 -4
- package/dist/scaffold/templates.js +7 -5
- package/dist/scaffold/vendor-skill.d.ts +1 -4
- package/dist/scaffold/vendor-skill.js +5 -6
- package/dist/schedule/cron.d.ts +13 -0
- package/dist/schedule/cron.js +52 -0
- package/dist/schedule/eventbridge-cron.d.ts +17 -0
- package/dist/schedule/eventbridge-cron.js +88 -0
- package/dist/schedule/schedule.d.ts +23 -0
- package/dist/schedule/schedule.js +34 -0
- package/dist/schedule/scheduler.d.ts +34 -33
- package/dist/schedule/scheduler.js +251 -137
- package/dist/schedule/state.d.ts +4 -14
- package/dist/schedule/state.js +7 -7
- package/dist/schedule/wake-alarm.d.ts +22 -2
- package/dist/schedule/wake-alarm.js +66 -20
- package/dist/schedule/wakeups.d.ts +16 -15
- package/dist/schedule/wakeups.js +47 -45
- package/dist/service.d.ts +56 -169
- package/dist/service.js +91 -187
- package/dist/serving-command.d.ts +10 -0
- package/dist/serving-command.js +16 -0
- package/dist/slack.d.ts +1 -1
- package/dist/slack.js +1 -1
- package/dist/{channels/kit/state.js → state-file.js} +6 -6
- package/dist/telegram.d.ts +1 -0
- package/dist/telegram.js +1 -0
- package/dist/tunnel.d.ts +3 -0
- package/dist/tunnel.js +12 -4
- package/dist/value-file.d.ts +21 -0
- package/dist/value-file.js +108 -0
- package/dist/version.d.ts +5 -0
- package/dist/version.js +14 -1
- package/package.json +18 -28
- package/dist/agent.d.ts +0 -106
- package/dist/agent.js +0 -21
- package/dist/channels/control.d.ts +0 -31
- package/dist/channels/control.js +0 -329
- package/dist/channels/define-channel.d.ts +0 -39
- package/dist/channels/define-channel.js +0 -9
- package/dist/channels/feishu/context-buffer.d.ts +0 -35
- package/dist/channels/feishu/context-buffer.js +0 -65
- package/dist/channels/feishu/scaffold/channel.websocket.ts +0 -31
- package/dist/channels/http.d.ts +0 -23
- package/dist/channels/http.js +0 -57
- package/dist/channels/lark/scaffold/channel.websocket.ts +0 -31
- package/dist/channels/slack/context-buffer.d.ts +0 -23
- package/dist/channels/slack/context-buffer.js +0 -37
- package/dist/channels/sse.d.ts +0 -4
- package/dist/channels/sse.js +0 -66
- package/dist/cli/commands/chat.d.ts +0 -3
- package/dist/cli/commands/chat.js +0 -14
- package/dist/cli/commands/routine-run.d.ts +0 -6
- package/dist/cli/commands/routine-run.js +0 -46
- package/dist/cli/commands/routine.d.ts +0 -25
- package/dist/cli/commands/routine.js +0 -122
- package/dist/cli/commands/tool.d.ts +0 -1
- package/dist/cli/commands/tool.js +0 -57
- package/dist/collect.d.ts +0 -63
- package/dist/collect.js +0 -90
- package/dist/core.d.ts +0 -13
- package/dist/core.js +0 -15
- package/dist/engines/pi/agent-models.d.ts +0 -83
- package/dist/engines/pi/agent-models.js +0 -120
- package/dist/engines/pi/agent-session-factory.d.ts +0 -121
- package/dist/engines/pi/agent-session-factory.js +0 -535
- package/dist/engines/pi/chat.d.ts +0 -3
- package/dist/engines/pi/chat.js +0 -8
- package/dist/engines/pi/config.d.ts +0 -71
- package/dist/engines/pi/config.js +0 -199
- package/dist/engines/pi/create.d.ts +0 -170
- package/dist/engines/pi/create.js +0 -343
- package/dist/engines/pi/definition.d.ts +0 -74
- package/dist/engines/pi/definition.js +0 -236
- package/dist/engines/pi/entry-images.d.ts +0 -14
- package/dist/engines/pi/entry-images.js +0 -23
- package/dist/engines/pi/invoke-session.d.ts +0 -18
- package/dist/engines/pi/invoke-session.js +0 -403
- package/dist/engines/pi/locked-file.d.ts +0 -9
- package/dist/engines/pi/machine.d.ts +0 -53
- package/dist/engines/pi/machine.js +0 -137
- package/dist/engines/pi/models.d.ts +0 -200
- package/dist/engines/pi/models.js +0 -533
- package/dist/engines/pi/open.d.ts +0 -180
- package/dist/engines/pi/open.js +0 -270
- package/dist/engines/pi/openai-account-models.d.ts +0 -35
- package/dist/engines/pi/openai-account-models.js +0 -116
- package/dist/engines/pi/report.d.ts +0 -18
- package/dist/engines/pi/report.js +0 -59
- package/dist/engines/pi/retry-event.d.ts +0 -6
- package/dist/engines/pi/retry-event.js +0 -15
- package/dist/engines/pi/service.d.ts +0 -9
- package/dist/engines/pi/service.js +0 -14
- package/dist/engines/pi/session-builder.d.ts +0 -13
- package/dist/engines/pi/session-builder.js +0 -117
- package/dist/engines/pi/session-control.d.ts +0 -77
- package/dist/engines/pi/session-control.js +0 -908
- package/dist/engines/pi/session-effects.d.ts +0 -14
- package/dist/engines/pi/session-effects.js +0 -30
- package/dist/engines/pi/session-inheritance.d.ts +0 -21
- package/dist/engines/pi/session-inheritance.js +0 -204
- package/dist/engines/pi/session-markers.d.ts +0 -49
- package/dist/engines/pi/session-markers.js +0 -60
- package/dist/engines/pi/session-settings.d.ts +0 -67
- package/dist/engines/pi/session-settings.js +0 -119
- package/dist/engines/pi/session-store.d.ts +0 -69
- package/dist/engines/pi/session-store.js +0 -430
- package/dist/engines/pi/tool-context.d.ts +0 -45
- package/dist/engines/pi/tool-context.js +0 -31
- package/dist/engines/pi/tool.d.ts +0 -99
- package/dist/engines/pi/tool.js +0 -118
- package/dist/engines/pi/turn-kit.d.ts +0 -92
- package/dist/engines/pi/turn-kit.js +0 -288
- package/dist/engines/pi/wake-tool.d.ts +0 -9
- package/dist/engines/pi/wake-tool.js +0 -92
- package/dist/loader.d.ts +0 -41
- package/dist/loader.js +0 -116
- package/dist/node.d.ts +0 -3
- package/dist/node.js +0 -4
- package/dist/observe.d.ts +0 -6
- package/dist/observe.js +0 -47
- package/dist/pi.d.ts +0 -25
- package/dist/pi.js +0 -15
- package/dist/scaffold/templates/APPEND_SYSTEM.md +0 -12
- package/dist/scaffold/templates/tools/fetch-url.ts +0 -25
- package/dist/schedule/discover.d.ts +0 -17
- package/dist/schedule/discover.js +0 -61
- package/dist/schedule/routine.d.ts +0 -43
- package/dist/schedule/routine.js +0 -32
- package/dist/schedule/run.d.ts +0 -55
- package/dist/schedule/run.js +0 -146
- package/dist/secrets-gate.d.ts +0 -32
- package/dist/secrets-gate.js +0 -33
- package/dist/session-remote.d.ts +0 -47
- package/dist/session-remote.js +0 -460
- package/dist/session.d.ts +0 -466
- package/dist/session.js +0 -57
- /package/dist/{channels/kit/state.d.ts → state-file.d.ts} +0 -0
|
@@ -1,908 +0,0 @@
|
|
|
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";
|
|
10
|
-
import { getSupportedThinkingLevels } from "@earendil-works/pi-ai";
|
|
11
|
-
import { MISSING_MODEL_CODE, SESSION_BUSY_CODE } from "../../agent.js";
|
|
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";
|
|
13
|
-
import { forkProvenance, isConversationMessage, isNavigable, publishedLeaf } from "./session-markers.js";
|
|
14
|
-
import { entryImages, imageAt } from "./entry-images.js";
|
|
15
|
-
import { answerOutcome, answerThinking, endsRun, } from "./turn-kit.js";
|
|
16
|
-
import { startCompaction } from "./agent-session-factory.js";
|
|
17
|
-
import { toRetryScheduledEvent } from "./retry-event.js";
|
|
18
|
-
import { MissingModel, THINKING_LEVELS, activePath, describeModels, resolveSessionSettings, } from "./session-settings.js";
|
|
19
|
-
import { log } from "../../log.js";
|
|
20
|
-
// ── Usage (read from the record) ─────────────────────────────────────────────
|
|
21
|
-
/**
|
|
22
|
-
* What the newest answer cost and how full the context is, from the record rather than a live session (a served
|
|
23
|
-
* session exists only during its run). The numbers are the provider's own, on the newest assistant pi counts as
|
|
24
|
-
* carrying usage (`getLastAssistantUsage`). The context size adds pi's estimate of what followed that answer, which
|
|
25
|
-
* after a finished turn is nothing: then it is exactly pi's `getContextUsage()`.
|
|
26
|
-
*
|
|
27
|
-
* `contextTokens` is omitted, never guessed, once a compaction or a context edit follows that answer: the provider's
|
|
28
|
-
* count no longer describes the context (pi's own rule). pi then estimates from characters; this reports unknown
|
|
29
|
-
* until the next response.
|
|
30
|
-
*/
|
|
31
|
-
function sessionUsage(path, record, contextWindow) {
|
|
32
|
-
const usage = getLastAssistantUsage(path);
|
|
33
|
-
if (!usage)
|
|
34
|
-
return undefined;
|
|
35
|
-
let at = path.length - 1;
|
|
36
|
-
while (at >= 0) {
|
|
37
|
-
const entry = path[at];
|
|
38
|
-
if (entry?.type === "message" && entry.message.usage === usage)
|
|
39
|
-
break;
|
|
40
|
-
at--;
|
|
41
|
-
}
|
|
42
|
-
const answer = path[at];
|
|
43
|
-
const stale = path.slice(at + 1).some((entry) => entry.type === "compaction" || entry.type === "context_edit");
|
|
44
|
-
let contextTokens;
|
|
45
|
-
if (answer && !stale) {
|
|
46
|
-
let trailing = 0;
|
|
47
|
-
let after = false;
|
|
48
|
-
for (const { sourceEntry, messages } of record.buildSessionProjection().entries) {
|
|
49
|
-
if (after)
|
|
50
|
-
for (const message of messages)
|
|
51
|
-
trailing += estimateTokens(message);
|
|
52
|
-
if (sourceEntry.id === answer.id)
|
|
53
|
-
after = true;
|
|
54
|
-
}
|
|
55
|
-
contextTokens = calculateContextTokens(usage) + trailing;
|
|
56
|
-
}
|
|
57
|
-
return {
|
|
58
|
-
inputTokens: usage.input,
|
|
59
|
-
outputTokens: usage.output,
|
|
60
|
-
cacheReadTokens: usage.cacheRead,
|
|
61
|
-
cacheWriteTokens: usage.cacheWrite,
|
|
62
|
-
cost: usage.cost.total,
|
|
63
|
-
...(contextTokens !== undefined ? { contextTokens } : {}),
|
|
64
|
-
...(contextWindow !== undefined && contextWindow > 0 ? { contextWindow } : {}),
|
|
65
|
-
};
|
|
66
|
-
}
|
|
67
|
-
// ── Entry normalization (durable plane) ──────────────────────────────────────
|
|
68
|
-
/**
|
|
69
|
-
* pi `PiSessionEntry` → neutral {@link SessionEntry}. Message entries map onto the guaranteed
|
|
70
|
-
* kind vocabulary (user/assistant/tool) with a minimal render payload; every other engine record
|
|
71
|
-
* keeps its pi type as an open-set kind with an EMPTY payload — present so `parentId` chains and
|
|
72
|
-
* cursors stay intact, skippable by contract, and no pi message class leaks through the adapter.
|
|
73
|
-
* One exception: `context_edit` carries `{ targetId, omitted }`, never the replacement content.
|
|
74
|
-
*/
|
|
75
|
-
function toSessionEntry(entry, parentId) {
|
|
76
|
-
const base = {
|
|
77
|
-
id: entry.id,
|
|
78
|
-
parentId,
|
|
79
|
-
timestamp: Date.parse(entry.timestamp),
|
|
80
|
-
};
|
|
81
|
-
if (entry.type === "message") {
|
|
82
|
-
const m = entry.message;
|
|
83
|
-
if (m.role === "user") {
|
|
84
|
-
return { ...base, kind: "user", data: { text: contentText(m.content, ""), ...entryImages(entry.id, m) } };
|
|
85
|
-
}
|
|
86
|
-
if (m.role === "assistant") {
|
|
87
|
-
// `args` is what `tool_started` carries live (pi emits the call's recorded `arguments` there), so a reopened
|
|
88
|
-
// conversation shows the same call a watcher saw.
|
|
89
|
-
const toolCalls = m.content
|
|
90
|
-
.filter((b) => b.type === "toolCall")
|
|
91
|
-
.map((b) => ({ id: b.id ?? "", name: b.name ?? "", args: b.arguments ?? {} }));
|
|
92
|
-
const data = { text: contentText(m.content, "") };
|
|
93
|
-
if (toolCalls.length > 0)
|
|
94
|
-
data.toolCalls = toolCalls;
|
|
95
|
-
// The same text the live `message_delta { channel: "thinking" }` added up to (turn-kit.ts answerThinking).
|
|
96
|
-
const thinking = answerThinking(m);
|
|
97
|
-
if (thinking !== undefined)
|
|
98
|
-
data.thinking = thinking;
|
|
99
|
-
const outcome = answerOutcome(m);
|
|
100
|
-
if (outcome)
|
|
101
|
-
data.outcome = outcome;
|
|
102
|
-
return { ...base, kind: "assistant", data };
|
|
103
|
-
}
|
|
104
|
-
if (m.role === "toolResult") {
|
|
105
|
-
return {
|
|
106
|
-
...base,
|
|
107
|
-
kind: "tool",
|
|
108
|
-
data: {
|
|
109
|
-
toolCallId: m.toolCallId,
|
|
110
|
-
toolName: m.toolName,
|
|
111
|
-
isError: m.isError ?? false,
|
|
112
|
-
text: contentText(m.content, ""),
|
|
113
|
-
...entryImages(entry.id, m),
|
|
114
|
-
// Same flag as the live `tool_finished` (turn-kit.ts asksToEndRun).
|
|
115
|
-
...(endsRun(m) ? { terminate: true } : {}),
|
|
116
|
-
},
|
|
117
|
-
};
|
|
118
|
-
}
|
|
119
|
-
// Not a conversation turn (`isConversationMessage`): the engine's own `system` bookkeeping, or a custom
|
|
120
|
-
// AgentMessage role a channel/extension defined. Open-set kind with an EMPTY payload, skippable by contract —
|
|
121
|
-
// which is also what keeps the assembled prompt out of everything this plane publishes.
|
|
122
|
-
return { ...base, kind: `message:${m.role}`, data: {} };
|
|
123
|
-
}
|
|
124
|
-
// Which transcript entry the model no longer sees as written: `omitted` when it left the model context (pi's own
|
|
125
|
-
// abandoned retry and overflow attempts, or an extension's edit), otherwise its content was replaced. The target
|
|
126
|
-
// stays in the transcript either way, and the edit applies only on a path that contains it.
|
|
127
|
-
if (entry.type === "context_edit") {
|
|
128
|
-
return { ...base, kind: entry.type, data: { targetId: entry.targetId, omitted: entry.replacement === null } };
|
|
129
|
-
}
|
|
130
|
-
return { ...base, kind: entry.type, data: {} };
|
|
131
|
-
}
|
|
132
|
-
// ── Live fan-out (events plane) ──────────────────────────────────────────────
|
|
133
|
-
/** Per-subscriber backlog limits: event count and UTF-8 JSON bytes. Overflow freezes the accepted
|
|
134
|
-
* prefix; the reader drains it before seeing done, then reconnects and backfills via entries(). */
|
|
135
|
-
export const SUBSCRIBER_BUFFER_CAP = 10_000;
|
|
136
|
-
export const SUBSCRIBER_BUFFER_BYTES = 8 * 1024 * 1024;
|
|
137
|
-
/**
|
|
138
|
-
* One subscriber's bounded push→pull queue.
|
|
139
|
-
*
|
|
140
|
-
* The mechanics are Effect's, because both halves are things this class hand-rolled and got wrong
|
|
141
|
-
* once each. `Queue.end` stops accepting offers, lets the reader drain what WAS accepted, and only
|
|
142
|
-
* then reports done — which is both the overflow policy above and the door teardown needs (an async
|
|
143
|
-
* generator suspended on a quiet stream cannot be ended by `return()` alone: that queues behind the
|
|
144
|
-
* never-settling await). And a queue has a SET of takers, so two concurrent `next()` calls — legal,
|
|
145
|
-
* any wrapper may poll twice — cannot overwrite each other's resolver.
|
|
146
|
-
*
|
|
147
|
-
* What stays here is the arithmetic Queue has no opinion on: the two backlog limits, and the JSON
|
|
148
|
-
* snapshot (taken at push, so a producer that later mutates its event or tool result cannot grow a
|
|
149
|
-
* queued one). Cost of the swap: one fiber per delivered event instead of an array shift, on a
|
|
150
|
-
* stream that carries per-token deltas — cheap next to a model call, and paid only while someone is
|
|
151
|
-
* attached.
|
|
152
|
-
*/
|
|
153
|
-
class Subscriber {
|
|
154
|
-
/** For the overflow diagnostic only — a warn without the session is not actionable on a
|
|
155
|
-
* multi-session serve. (Explicit assignment: TS parameter properties break Node's strip-only
|
|
156
|
-
* type erasure, which the CLI runs under.) */
|
|
157
|
-
session;
|
|
158
|
-
constructor(session) {
|
|
159
|
-
this.session = session;
|
|
160
|
-
}
|
|
161
|
-
queue = Effect.runSync(Queue.unbounded());
|
|
162
|
-
buffered = 0;
|
|
163
|
-
bufferedBytes = 0;
|
|
164
|
-
ended = false;
|
|
165
|
-
push(event) {
|
|
166
|
-
if (this.ended)
|
|
167
|
-
return;
|
|
168
|
-
const json = JSON.stringify(event);
|
|
169
|
-
const bytes = Buffer.byteLength(json);
|
|
170
|
-
if (this.buffered >= SUBSCRIBER_BUFFER_CAP || this.bufferedBytes + bytes > SUBSCRIBER_BUFFER_BYTES) {
|
|
171
|
-
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()`);
|
|
172
|
-
this.close();
|
|
173
|
-
return;
|
|
174
|
-
}
|
|
175
|
-
Queue.offerUnsafe(this.queue, { json, bytes });
|
|
176
|
-
this.buffered++;
|
|
177
|
-
this.bufferedBytes += bytes;
|
|
178
|
-
}
|
|
179
|
-
close() {
|
|
180
|
-
this.ended = true;
|
|
181
|
-
Queue.endUnsafe(this.queue);
|
|
182
|
-
}
|
|
183
|
-
next() {
|
|
184
|
-
return Effect.runPromise(Queue.take(this.queue).pipe(Effect.map((item) => {
|
|
185
|
-
this.buffered--;
|
|
186
|
-
this.bufferedBytes -= item.bytes;
|
|
187
|
-
return { done: false, value: JSON.parse(item.json) };
|
|
188
|
-
}), Effect.catchTag("Done", () => Effect.succeed({ done: true, value: undefined }))));
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
/**
|
|
192
|
-
* Build the observation hub. Wire `observer` into the SAME agent assembly that serves the sessions:
|
|
193
|
-
*
|
|
194
|
-
* ```ts
|
|
195
|
-
* const { control, observer } = createPiSessionControl({ sessions });
|
|
196
|
-
* const agent = createPiAgent({ ..., sessions, observer });
|
|
197
|
-
* ```
|
|
198
|
-
*/
|
|
199
|
-
export function createPiSessionControl(options) {
|
|
200
|
-
const { sessions, boundary } = options;
|
|
201
|
-
/** Live run state per session — derived purely from run_started/run_settled and the controls
|
|
202
|
-
* registered with run_started. */
|
|
203
|
-
const active = new Map();
|
|
204
|
-
const subscribers = new Map();
|
|
205
|
-
/** Sessions with a manual compaction in flight — reported as `status: "compacting"`, keyed to
|
|
206
|
-
* the summarization's AbortController so `abort` has a door into it (run/compaction symmetry:
|
|
207
|
-
* both are model calls a client must be able to stop). Set at ADMISSION, cleared by the
|
|
208
|
-
* detached task before `compaction_finished`. */
|
|
209
|
-
const compacting = new Map();
|
|
210
|
-
/** Fan an event out to this session's subscribers — shared by the observer (run events) and the
|
|
211
|
-
* boundary mutations (session-level events, no runId). */
|
|
212
|
-
const fanOut = (session, event) => {
|
|
213
|
-
const subs = subscribers.get(session);
|
|
214
|
-
if (subs)
|
|
215
|
-
for (const sub of [...subs])
|
|
216
|
-
sub.push(event);
|
|
217
|
-
};
|
|
218
|
-
/** Emit a HUB-generated event: subscribers first, then the external tap — the boundary-event
|
|
219
|
-
* half of a full-vocabulary tap (run events reach it through the observer composition). */
|
|
220
|
-
const emitOwn = (session, event) => {
|
|
221
|
-
fanOut(session, event);
|
|
222
|
-
try {
|
|
223
|
-
options.tap?.(session, event);
|
|
224
|
-
}
|
|
225
|
-
catch (error) {
|
|
226
|
-
// Same discipline as the data plane's observer guard: a broken tap is its own problem.
|
|
227
|
-
log.warn(`[fastagent] session-control tap threw (event ${event.type}): ${String(error)}`);
|
|
228
|
-
}
|
|
229
|
-
};
|
|
230
|
-
const observer = (session, event, run) => {
|
|
231
|
-
if (event.type === "run_started" && event.runId) {
|
|
232
|
-
active.set(session, { runId: event.runId, controls: run, pending: { steering: [], followUp: [] } });
|
|
233
|
-
}
|
|
234
|
-
else if (event.type === "run_settled") {
|
|
235
|
-
active.delete(session);
|
|
236
|
-
}
|
|
237
|
-
else if (event.type === "queue_changed") {
|
|
238
|
-
const entry = active.get(session);
|
|
239
|
-
if (entry)
|
|
240
|
-
entry.pending = event.data;
|
|
241
|
-
}
|
|
242
|
-
fanOut(session, event);
|
|
243
|
-
if (event.type === "run_settled")
|
|
244
|
-
publishUsage(session);
|
|
245
|
-
};
|
|
246
|
-
/**
|
|
247
|
-
* Report `usage` as the record now reads it: after a run (a new answer) and after a compaction (the context it
|
|
248
|
-
* described is gone). Read after the fact, so it follows the event that caused it.
|
|
249
|
-
*/
|
|
250
|
-
const publishUsage = (session) => {
|
|
251
|
-
// Every serve runs a hub, most with nobody attached (a chat channel): reading the whole record each turn for an
|
|
252
|
-
// empty audience would cost a full parse per turn, growing with the conversation.
|
|
253
|
-
if (!subscribers.get(session)?.size && !options.tap)
|
|
254
|
-
return;
|
|
255
|
-
reads.state(session).then(({ usage }) => {
|
|
256
|
-
if (usage)
|
|
257
|
-
emitOwn(session, { type: "state_changed", timestamp: Date.now(), data: { usage } });
|
|
258
|
-
}, (error) => log.warn(`[fastagent] session ${session}: usage not published: ${String(error)}`));
|
|
259
|
-
};
|
|
260
|
-
// The reads, plus the two sessionless declarations. Bound onto a handle below.
|
|
261
|
-
const reads = {
|
|
262
|
-
async commands() {
|
|
263
|
-
return (await options.commands?.()) ?? [];
|
|
264
|
-
},
|
|
265
|
-
capabilities: () => {
|
|
266
|
-
const b = boundary;
|
|
267
|
-
return {
|
|
268
|
-
steering: true,
|
|
269
|
-
followUp: true,
|
|
270
|
-
compaction: !!b,
|
|
271
|
-
// Every write — a property, a copied record, a removed one — needs the boundary wiring for
|
|
272
|
-
// its LEASE, so they answer the same question: a write that races a run would hang the next
|
|
273
|
-
// turn off a stale branch, or pull the record out from under it.
|
|
274
|
-
fork: !!b,
|
|
275
|
-
delete: !!b,
|
|
276
|
-
// The CONTRACT's list, not a copy of it: a field added to SessionUpdate is advertised
|
|
277
|
-
// without anyone remembering to, and one removed cannot linger here.
|
|
278
|
-
updatable: b ? [...UPDATE_FIELDS] : [],
|
|
279
|
-
// The registry is a deployment fact (any session may be pointed at any of it). Each model carries the levels
|
|
280
|
-
// a session on it accepts, so a picker can offer them before the session runs on that model;
|
|
281
|
-
// `state().availableThinkingLevels` answers for the model the session is on.
|
|
282
|
-
...(b ? { allowedModels: describeModels(b.models.getModels()) } : {}),
|
|
283
|
-
toolProgress: true, // tool_progress IS delivered (replace-semantics snapshots)
|
|
284
|
-
usage: true, // state().usage, and state_changed{usage} after each run and compaction
|
|
285
|
-
};
|
|
286
|
-
},
|
|
287
|
-
async state(session) {
|
|
288
|
-
const run = active.get(session);
|
|
289
|
-
const opened = await sessions.openIfExists(session);
|
|
290
|
-
const leafEntryId = opened ? publishedLeaf(opened) : undefined;
|
|
291
|
-
// What will RUN, not the raw record: a client steering a session needs the pair that executes.
|
|
292
|
-
// Without a boundary there is no model to resolve against, and the fields are absent.
|
|
293
|
-
// OBSERVATION IS TOTAL: an unreadable entry chain leaves the pair absent too (the same shape a
|
|
294
|
-
// control-less deployment answers with) rather than rejecting a read that has no error-code
|
|
295
|
-
// channel to explain itself. The fault is not swallowed — it surfaces where codes exist: the
|
|
296
|
-
// next invoke fails (binding a session walks the same chain) and a boundary dispatch answers
|
|
297
|
-
// `boundary_command_failed`. Here it is a server-side warn.
|
|
298
|
-
const b = boundary;
|
|
299
|
-
let settings;
|
|
300
|
-
let usage;
|
|
301
|
-
if (!opened) {
|
|
302
|
-
// An id with no record is an empty conversation at the defaults: its first turn would run on these.
|
|
303
|
-
if (b)
|
|
304
|
-
settings = resolveSessionSettings([], b.models, b.defaults);
|
|
305
|
-
}
|
|
306
|
-
else {
|
|
307
|
-
try {
|
|
308
|
-
const path = activePath(opened);
|
|
309
|
-
if (b)
|
|
310
|
-
settings = resolveSessionSettings(path, b.models, b.defaults);
|
|
311
|
-
const selected = settings?.model;
|
|
312
|
-
const latest = selected?.api === "pi-virtual"
|
|
313
|
-
? opened
|
|
314
|
-
.buildSessionContext()
|
|
315
|
-
.messages.filter((message) => message.role === "assistant" &&
|
|
316
|
-
message.stopReason !== "error" &&
|
|
317
|
-
message.stopReason !== "aborted")
|
|
318
|
-
.at(-1)
|
|
319
|
-
: undefined;
|
|
320
|
-
const physical = latest?.role === "assistant" ? b?.models.getModel(latest.provider, latest.model) : undefined;
|
|
321
|
-
usage = sessionUsage(path, opened, (physical ?? selected)?.contextWindow);
|
|
322
|
-
}
|
|
323
|
-
catch (error) {
|
|
324
|
-
log.warn(`[fastagent] session ${session}: state unreadable (entry chain): ${String(error)}`);
|
|
325
|
-
}
|
|
326
|
-
}
|
|
327
|
-
const name = opened?.getSessionName();
|
|
328
|
-
return {
|
|
329
|
-
status: run ? "running" : compacting.has(session) ? "compacting" : "idle",
|
|
330
|
-
...(name ? { name } : {}),
|
|
331
|
-
...(run ? { activeRunId: run.runId } : {}),
|
|
332
|
-
...(settings
|
|
333
|
-
? {
|
|
334
|
-
model: `${settings.model.provider}/${settings.model.id}`,
|
|
335
|
-
thinkingLevel: settings.thinkingLevel,
|
|
336
|
-
availableThinkingLevels: settings.availableThinkingLevels,
|
|
337
|
-
}
|
|
338
|
-
: {}),
|
|
339
|
-
pending: run
|
|
340
|
-
? { steering: [...run.pending.steering], followUp: [...run.pending.followUp] }
|
|
341
|
-
: { steering: [], followUp: [] },
|
|
342
|
-
...(usage ? { usage } : {}),
|
|
343
|
-
...(leafEntryId ? { leafEntryId } : {}),
|
|
344
|
-
};
|
|
345
|
-
},
|
|
346
|
-
async entries(session, opts) {
|
|
347
|
-
const opened = await sessions.openIfExists(session);
|
|
348
|
-
if (!opened)
|
|
349
|
-
return { entries: [] };
|
|
350
|
-
// LEAF FIRST, then the journal: `getEntries()` hands back a SNAPSHOT, so reading it first
|
|
351
|
-
// would race any concurrent append into a leaf the snapshot cannot contain — a live turn
|
|
352
|
-
// reading as a dangling head. This order makes the journal a superset of the leaf's chain,
|
|
353
|
-
// which is what lets the published head be trusted as one of the published entries.
|
|
354
|
-
const leafEntryId = publishedLeaf(opened);
|
|
355
|
-
const journal = opened.getEntries();
|
|
356
|
-
// The published tree must be SELF-CONTAINED: a `parentId` pointing at an entry this plane does
|
|
357
|
-
// not publish (a label, one of our markers) would break the walk a client does from
|
|
358
|
-
// `leafEntryId` upward — it would stop at an id it cannot look up and report a short path.
|
|
359
|
-
// So a skipped entry is spliced out: its children point at the nearest published ancestor.
|
|
360
|
-
const byId = new Map(journal.map((e) => [e.id, e]));
|
|
361
|
-
const publishedParent = (entry) => {
|
|
362
|
-
let parent = entry.parentId ?? undefined;
|
|
363
|
-
while (parent) {
|
|
364
|
-
const found = byId.get(parent);
|
|
365
|
-
// A gap in the chain is left as a gap — `state()` reports it and the next invoke fails on
|
|
366
|
-
// it (design §7); inventing a parent here would hide a corrupt journal.
|
|
367
|
-
if (!found)
|
|
368
|
-
return parent;
|
|
369
|
-
if (isNavigable(found))
|
|
370
|
-
return parent;
|
|
371
|
-
parent = found.parentId ?? undefined;
|
|
372
|
-
}
|
|
373
|
-
return undefined;
|
|
374
|
-
};
|
|
375
|
-
const all = journal.filter(isNavigable).map((e) => toSessionEntry(e, publishedParent(e)));
|
|
376
|
-
let entries = all;
|
|
377
|
-
if (opts?.since !== undefined) {
|
|
378
|
-
const idx = all.findIndex((e) => e.id === opts.since);
|
|
379
|
-
// Unknown cursor → full backfill (correct, merely larger): the client's cursor may predate
|
|
380
|
-
// a repository the session was rebuilt into; silently skipping records would lose history.
|
|
381
|
-
if (idx >= 0)
|
|
382
|
-
entries = all.slice(idx + 1);
|
|
383
|
-
}
|
|
384
|
-
return { entries, ...(leafEntryId ? { leafEntryId } : {}) };
|
|
385
|
-
},
|
|
386
|
-
async image(session, ref) {
|
|
387
|
-
// ponytail: opens (parses) the whole record per image; an index when long image-heavy sessions make it show.
|
|
388
|
-
const opened = await sessions.openIfExists(session);
|
|
389
|
-
return opened ? imageAt(opened, ref) : undefined;
|
|
390
|
-
},
|
|
391
|
-
events(session) {
|
|
392
|
-
// ONE CALL IS ONE SUBSCRIPTION, and `ready` is its boundary: the moment after which nothing emitted for this
|
|
393
|
-
// session can be missed (session.ts, SessionEventStream). A second iteration of the same stream would be a
|
|
394
|
-
// second subscription inheriting the first one's readiness — a guarantee it never earned — so it is refused
|
|
395
|
-
// rather than documented. Matching the remote client, which opens one connection per stream.
|
|
396
|
-
//
|
|
397
|
-
// Registration happens on the FIRST next(), not at iterator creation: subscription semantics = you are
|
|
398
|
-
// subscribed while you iterate; an iterator obtained but never driven must not buffer. Teardown goes through
|
|
399
|
-
// Subscriber.close() so a `return()` on a QUIET stream resolves promptly instead of queueing behind a
|
|
400
|
-
// never-settling pull — without it every attach/detach against an idle session would leak a permanently
|
|
401
|
-
// registered subscriber.
|
|
402
|
-
let registered;
|
|
403
|
-
let unestablished;
|
|
404
|
-
const ready = new Promise((resolve, reject) => {
|
|
405
|
-
registered = resolve;
|
|
406
|
-
unestablished = reject;
|
|
407
|
-
});
|
|
408
|
-
// An awaiter gets the rejection; a consumer that only iterates would otherwise take an unhandled one for a
|
|
409
|
-
// cancellation it performed itself.
|
|
410
|
-
ready.catch(() => { });
|
|
411
|
-
let iterated = false;
|
|
412
|
-
return {
|
|
413
|
-
ready,
|
|
414
|
-
[Symbol.asyncIterator]() {
|
|
415
|
-
if (iterated) {
|
|
416
|
-
throw new Error("session events: this stream is one subscription — call events() again for another");
|
|
417
|
-
}
|
|
418
|
-
iterated = true;
|
|
419
|
-
let sub;
|
|
420
|
-
// `finished` is its own state: `sub === undefined` alone would conflate "not yet
|
|
421
|
-
// registered" with "terminated", and a post-done next() would silently REGISTER A FRESH
|
|
422
|
-
// subscription — the exact ghost-subscriber leak this class exists to prevent, reachable
|
|
423
|
-
// by any wrapper that polls one extra time. done is terminal, per the iterator protocol.
|
|
424
|
-
let finished = false;
|
|
425
|
-
const cleanup = () => {
|
|
426
|
-
finished = true;
|
|
427
|
-
if (!sub) {
|
|
428
|
-
// Cancelled before it registered: `ready` has a promise it cannot keep, and hanging on it is the one
|
|
429
|
-
// outcome a caller cannot diagnose. The remote client answers the same way.
|
|
430
|
-
unestablished(new Error("session events: cancelled before the subscription was established"));
|
|
431
|
-
return;
|
|
432
|
-
}
|
|
433
|
-
sub.close();
|
|
434
|
-
const set = subscribers.get(session);
|
|
435
|
-
if (set) {
|
|
436
|
-
set.delete(sub);
|
|
437
|
-
if (set.size === 0)
|
|
438
|
-
subscribers.delete(session);
|
|
439
|
-
}
|
|
440
|
-
sub = undefined;
|
|
441
|
-
};
|
|
442
|
-
return {
|
|
443
|
-
async next() {
|
|
444
|
-
if (finished)
|
|
445
|
-
return { done: true, value: undefined };
|
|
446
|
-
if (!sub) {
|
|
447
|
-
sub = new Subscriber(session);
|
|
448
|
-
let set = subscribers.get(session);
|
|
449
|
-
if (!set) {
|
|
450
|
-
set = new Set();
|
|
451
|
-
subscribers.set(session, set);
|
|
452
|
-
}
|
|
453
|
-
set.add(sub);
|
|
454
|
-
registered();
|
|
455
|
-
}
|
|
456
|
-
const result = await sub.next();
|
|
457
|
-
if (result.done)
|
|
458
|
-
cleanup();
|
|
459
|
-
return result;
|
|
460
|
-
},
|
|
461
|
-
async return(value) {
|
|
462
|
-
cleanup();
|
|
463
|
-
return { done: true, value: value };
|
|
464
|
-
},
|
|
465
|
-
};
|
|
466
|
-
},
|
|
467
|
-
};
|
|
468
|
-
},
|
|
469
|
-
};
|
|
470
|
-
/** No boundary wiring — the write path does not exist in this deployment. A capability-gating
|
|
471
|
-
* client never lands here; one that does gets the same answer every gate publishes. */
|
|
472
|
-
const unsupported = (what) => ({
|
|
473
|
-
ok: false,
|
|
474
|
-
error: {
|
|
475
|
-
code: UNSUPPORTED_CAPABILITY_CODE,
|
|
476
|
-
message: `${what} is not supported by this runtime (no boundary wiring)`,
|
|
477
|
-
retryable: false,
|
|
478
|
-
},
|
|
479
|
-
});
|
|
480
|
-
const noSuchSession = (session) => ({
|
|
481
|
-
ok: false,
|
|
482
|
-
error: { code: NO_SUCH_SESSION_CODE, message: `session "${session}" does not exist`, retryable: false },
|
|
483
|
-
});
|
|
484
|
-
/**
|
|
485
|
-
* The refusal for an id no client could then open: the empty string, `.` and `..` are not URL path segments
|
|
486
|
-
* (isAddressableSession), so minting one would put a row in list() that nothing can address — listed, unopenable by
|
|
487
|
-
* the client that just listed it. Both writes that create a record (fork's `into`, and an update to an id with no
|
|
488
|
-
* record) refuse it.
|
|
489
|
-
*/
|
|
490
|
-
const unaddressable = (session) => invalid(`${JSON.stringify(session)} cannot be a session id — the control plane could not address it`);
|
|
491
|
-
const nothingToCompact = () => ({
|
|
492
|
-
ok: false,
|
|
493
|
-
error: {
|
|
494
|
-
code: NOTHING_TO_COMPACT_CODE,
|
|
495
|
-
message: "nothing to compact — the session has no compactable history yet; retry after more turns",
|
|
496
|
-
retryable: false,
|
|
497
|
-
},
|
|
498
|
-
});
|
|
499
|
-
const invalid = (message) => ({
|
|
500
|
-
ok: false,
|
|
501
|
-
error: { code: INVALID_COMMAND_CODE, message, retryable: false },
|
|
502
|
-
});
|
|
503
|
-
const busy = () => ({
|
|
504
|
-
ok: false,
|
|
505
|
-
error: {
|
|
506
|
-
code: SESSION_BUSY_CODE,
|
|
507
|
-
message: "session busy: a run (or another write) is in flight — retry at idle",
|
|
508
|
-
retryable: true,
|
|
509
|
-
},
|
|
510
|
-
});
|
|
511
|
-
const failed = (error) => ({
|
|
512
|
-
ok: false,
|
|
513
|
-
// A command that binds the session (compaction is a model call) on a session with no model: the same code the
|
|
514
|
-
// invoke answers, since the same fix (`update({ model })`) applies.
|
|
515
|
-
error: error instanceof MissingModel
|
|
516
|
-
? { code: MISSING_MODEL_CODE, message: error.message, retryable: false }
|
|
517
|
-
: { code: BOUNDARY_COMMAND_FAILED_CODE, message: String(error), retryable: true },
|
|
518
|
-
});
|
|
519
|
-
const runBoundary = (command) => Effect.runPromise(Effect.scoped(command).pipe(Effect.catchTag("SessionBusy", () => Effect.succeed(busy())), Effect.catchCause((cause) => Effect.succeed(failed(portError(cause))))));
|
|
520
|
-
/** steer / follow_up / abort — the run actions. They reach the LIVE run through the controls
|
|
521
|
-
* registered with `run_started`; nothing durable is written. */
|
|
522
|
-
const runAction = (session, action) => Effect.runPromise(Effect.gen(function* () {
|
|
523
|
-
// pi dequeues a queued prompt by matching its text when it enters the conversation, and skips an empty one
|
|
524
|
-
// (`_handleAgentEvent`): an image-only prompt would sit in `pending` for the whole run and then read as dropped.
|
|
525
|
-
if ((action.type === "steer" || action.type === "follow_up") && action.prompt.text === "")
|
|
526
|
-
return invalid(`${action.type} needs non-empty text: a queued prompt is reported by its text`);
|
|
527
|
-
const run = active.get(session);
|
|
528
|
-
if (!run) {
|
|
529
|
-
// Run/compaction symmetry: an in-flight compaction is a model call too, and `abort` is its
|
|
530
|
-
// only door — interrupting it converges through the detached task's catch into
|
|
531
|
-
// `compaction_finished{aborted}` with the lease released; answering no_active_run against a
|
|
532
|
-
// state() that says "compacting" would be a lie.
|
|
533
|
-
const comp = action.type === "abort" ? compacting.get(session) : undefined;
|
|
534
|
-
if (comp) {
|
|
535
|
-
comp.abort();
|
|
536
|
-
return { ok: true }; // no runId — the outcome travels as compaction_finished{aborted}
|
|
537
|
-
}
|
|
538
|
-
// Rejected BEFORE acceptance: no run exists, nothing happened. retryable: false — the same
|
|
539
|
-
// call fails again; call it after state() shows an active run.
|
|
540
|
-
return {
|
|
541
|
-
ok: false,
|
|
542
|
-
error: {
|
|
543
|
-
code: NO_ACTIVE_RUN_CODE,
|
|
544
|
-
message: `no active run for this session — ${action.type} modulates a run an invoke is driving`,
|
|
545
|
-
retryable: false,
|
|
546
|
-
},
|
|
547
|
-
};
|
|
548
|
-
}
|
|
549
|
-
if (!run.controls) {
|
|
550
|
-
// A run EXISTS (state() rightly reports running) but was registered observation-only (the
|
|
551
|
-
// observer seam allows run_started without controls). That is a CAPABILITY problem, not a run
|
|
552
|
-
// problem — permanent for this wiring, so neither no_active_run (would poll forever) nor
|
|
553
|
-
// run_command_failed (transient) fits.
|
|
554
|
-
return {
|
|
555
|
-
ok: false,
|
|
556
|
-
error: {
|
|
557
|
-
code: UNSUPPORTED_CAPABILITY_CODE,
|
|
558
|
-
message: `the active run registered without modulation controls (observation-only) — ${action.type} cannot reach it`,
|
|
559
|
-
retryable: false,
|
|
560
|
-
},
|
|
561
|
-
};
|
|
562
|
-
}
|
|
563
|
-
const controls = run.controls;
|
|
564
|
-
if (action.type === "abort") {
|
|
565
|
-
yield* port(() => controls.abort());
|
|
566
|
-
return { ok: true, runId: run.runId };
|
|
567
|
-
}
|
|
568
|
-
const disposition = yield* port(() => action.type === "steer" ? controls.steer(action.prompt) : controls.followUp(action.prompt));
|
|
569
|
-
return { ok: true, runId: run.runId, disposition };
|
|
570
|
-
}).pipe(Effect.catchCause((cause) => Effect.succeed({
|
|
571
|
-
ok: false,
|
|
572
|
-
error: { code: RUN_COMMAND_FAILED_CODE, message: String(portError(cause)), retryable: false },
|
|
573
|
-
}))));
|
|
574
|
-
/**
|
|
575
|
-
* {@link Session.update} — validate the whole patch, take the lease once, hand the writes to the
|
|
576
|
-
* store, report what landed.
|
|
577
|
-
*
|
|
578
|
-
* Everything before the lease is validation, which is what makes a rejected patch leave nothing
|
|
579
|
-
* behind. What a value MEANS is decided here (a model spec against the registry, a level against
|
|
580
|
-
* the model it lands on); how a record takes it is the store's.
|
|
581
|
-
*/
|
|
582
|
-
const updateOf = (session, patch) => runBoundary(Effect.gen(function* () {
|
|
583
|
-
// Keys, not values: a field this runtime does not know must not be silently skipped — that is a
|
|
584
|
-
// client typo, or a newer client talking to an older serve, and both need to hear about it.
|
|
585
|
-
const named = Object.keys(patch);
|
|
586
|
-
const unknown = named.filter((f) => !UPDATE_FIELDS.includes(f));
|
|
587
|
-
if (unknown.length > 0) {
|
|
588
|
-
return unsupported(`update field(s) ${unknown.join(", ")} — capabilities().updatable lists what this serve sets`);
|
|
589
|
-
}
|
|
590
|
-
const fields = named.filter((f) => patch[f] !== undefined);
|
|
591
|
-
if (fields.length === 0)
|
|
592
|
-
return { ok: true }; // an empty patch asks for nothing, and gets it
|
|
593
|
-
const b = boundary;
|
|
594
|
-
if (!b)
|
|
595
|
-
return unsupported(`update(${fields.join(", ")})`);
|
|
596
|
-
// PAYLOAD validation first — before the session is even opened, and long before the lease: an
|
|
597
|
-
// invalid value must not briefly block a run.
|
|
598
|
-
let model;
|
|
599
|
-
if (patch.model !== undefined) {
|
|
600
|
-
const slash = patch.model.indexOf("/");
|
|
601
|
-
model = slash > 0 ? b.models.getModel(patch.model.slice(0, slash), patch.model.slice(slash + 1)) : undefined;
|
|
602
|
-
if (!model) {
|
|
603
|
-
return invalid(`unknown model "${patch.model}" — capabilities().allowedModels lists the accepted models`);
|
|
604
|
-
}
|
|
605
|
-
}
|
|
606
|
-
if (patch.thinkingLevel !== undefined && !THINKING_LEVELS.has(patch.thinkingLevel)) {
|
|
607
|
-
return invalid(`unknown thinking level "${patch.thinkingLevel}" — state().availableThinkingLevels lists what this session accepts`);
|
|
608
|
-
}
|
|
609
|
-
// A name is the client's own label; the only thing that cannot be one is nothing.
|
|
610
|
-
if (patch.name !== undefined && patch.name.trim() === "")
|
|
611
|
-
return invalid("a session name cannot be empty");
|
|
612
|
-
// An id with no record takes properties too: its first turn is the "next turn" they apply to, so the write
|
|
613
|
-
// creates its record (below, under the lease). (Read-only handle — the WRITE one is opened under the lease,
|
|
614
|
-
// and this one is discarded.)
|
|
615
|
-
const existing = yield* port(() => sessions.openIfExists(session));
|
|
616
|
-
if (!existing && !isAddressableSession(session))
|
|
617
|
-
return unaddressable(session);
|
|
618
|
-
if (patch.leafEntryId !== undefined) {
|
|
619
|
-
// A target that cannot BE a leaf is a permanent payload error, not a session error — the same
|
|
620
|
-
// disposition as an unknown model spec. Same predicate `entries()` publishes by, so
|
|
621
|
-
// "everything published is a position" holds by construction rather than by two literals
|
|
622
|
-
// agreeing.
|
|
623
|
-
const entry = existing?.getEntry(patch.leafEntryId);
|
|
624
|
-
if (!entry || !isNavigable(entry)) {
|
|
625
|
-
return invalid(entry
|
|
626
|
-
? `entry "${patch.leafEntryId}" is not a position — entries() publishes every id you can move to, and this is not one of them`
|
|
627
|
-
: `entry "${patch.leafEntryId}" does not exist in session "${session}" — entries() lists the positions`);
|
|
628
|
-
}
|
|
629
|
-
}
|
|
630
|
-
if (patch.thinkingLevel !== undefined) {
|
|
631
|
-
// The same set `state()` showed the client. Reject here rather than record a level the run
|
|
632
|
-
// would not use. The read is guarded because this must never REJECT — the contract promises a
|
|
633
|
-
// SessionResult, so an unreadable chain has to arrive as a code.
|
|
634
|
-
let resolved;
|
|
635
|
-
try {
|
|
636
|
-
// Against the path this patch LANDS on: a leaf move is written first, and the branch it
|
|
637
|
-
// moves to can carry a model override of its own — validating on the path being left would
|
|
638
|
-
// reject a level the destination supports, and accept one it does not.
|
|
639
|
-
const path = existing ? activePath(existing, patch.leafEntryId) : [];
|
|
640
|
-
resolved = resolveSessionSettings(path, b.models, b.defaults);
|
|
641
|
-
}
|
|
642
|
-
catch (error) {
|
|
643
|
-
return failed(error);
|
|
644
|
-
}
|
|
645
|
-
// An explicit model in the same patch wins over the one that path resolves to: it is applied
|
|
646
|
-
// after the move, so it is what the session ends up running. With neither (no model recorded, no
|
|
647
|
-
// default), any level of the vocabulary is recorded, and clamped once the session has a model, like a
|
|
648
|
-
// recorded level after a model change.
|
|
649
|
-
const target = model ?? resolved?.model;
|
|
650
|
-
if (target) {
|
|
651
|
-
const levels = getSupportedThinkingLevels(target);
|
|
652
|
-
if (!levels.includes(patch.thinkingLevel)) {
|
|
653
|
-
return invalid(`thinking level "${patch.thinkingLevel}" is not supported by ${target.provider}/${target.id} (allowed: ${levels.join(", ")})`);
|
|
654
|
-
}
|
|
655
|
-
}
|
|
656
|
-
}
|
|
657
|
-
// Release before publishing state_changed: an observer may immediately issue another command.
|
|
658
|
-
const applied = yield* Effect.scoped(Effect.gen(function* () {
|
|
659
|
-
yield* acquireSessionLease(b.lease, session);
|
|
660
|
-
// portJoin, not port: the lease is held for the duration of this write, so an interrupted
|
|
661
|
-
// fiber must not release it while the store is still writing (effect-port.ts, rule 1).
|
|
662
|
-
// A never-run session's record is created through the data plane's own path, under the same lease an
|
|
663
|
-
// invoke creating it takes; one that invoke created meanwhile is simply opened.
|
|
664
|
-
if (!existing)
|
|
665
|
-
yield* portJoin(() => sessions.openOrCreate(session));
|
|
666
|
-
return yield* portJoin(() => sessions.applyProperties(session, {
|
|
667
|
-
...(patch.name !== undefined ? { name: patch.name } : {}),
|
|
668
|
-
...(model ? { model: { provider: model.provider, id: model.id } } : {}),
|
|
669
|
-
...(patch.thinkingLevel !== undefined ? { thinkingLevel: patch.thinkingLevel } : {}),
|
|
670
|
-
...(patch.leafEntryId !== undefined ? { leafEntryId: patch.leafEntryId } : {}),
|
|
671
|
-
}));
|
|
672
|
-
}));
|
|
673
|
-
if (!applied)
|
|
674
|
-
return noSuchSession(session); // deleted in the window between the read and the lease
|
|
675
|
-
if (applied.landed.length > 0) {
|
|
676
|
-
// ONE event for the patch, built from what the RECORD holds. The settings pair rides along
|
|
677
|
-
// whenever anything but the name changed: model and thinking level are one setting, and a
|
|
678
|
-
// moved leaf can drop an override that used to apply.
|
|
679
|
-
let settings;
|
|
680
|
-
if (applied.path) {
|
|
681
|
-
try {
|
|
682
|
-
settings = resolveSessionSettings(applied.path, b.models, b.defaults);
|
|
683
|
-
}
|
|
684
|
-
catch (error) {
|
|
685
|
-
// Already durable, so an unresolvable pair must NOT read as "nothing took effect": report
|
|
686
|
-
// the position without it and let the next invoke — which walks the same path — be where
|
|
687
|
-
// the fault surfaces.
|
|
688
|
-
log.warn(`[fastagent] session ${session}: updated, settings unresolvable: ${String(error)}`);
|
|
689
|
-
}
|
|
690
|
-
}
|
|
691
|
-
// A moved head reads a different newest answer, and another model has another window: the event carries
|
|
692
|
-
// the usage the session now has, and carries none when it has none. Read back through `state()`, the one
|
|
693
|
-
// derivation; a read failure only costs the field, since the write has already landed.
|
|
694
|
-
const usage = applied.landed.some((f) => f === "leafEntryId" || f === "model")
|
|
695
|
-
? yield* port(() => reads.state(session)).pipe(Effect.map((state) => state.usage), Effect.catchCause((cause) => Effect.sync(() => {
|
|
696
|
-
log.warn(`[fastagent] session ${session}: updated, usage unreadable: ${String(portError(cause))}`);
|
|
697
|
-
return undefined;
|
|
698
|
-
})))
|
|
699
|
-
: undefined;
|
|
700
|
-
emitOwn(session, {
|
|
701
|
-
type: "state_changed",
|
|
702
|
-
timestamp: Date.now(),
|
|
703
|
-
data: {
|
|
704
|
-
...(applied.landed.includes("leafEntryId") ? { leafEntryId: applied.leafEntryId } : {}),
|
|
705
|
-
...(settings && applied.landed.some((f) => f !== "name")
|
|
706
|
-
? { model: `${settings.model.provider}/${settings.model.id}`, thinkingLevel: settings.thinkingLevel }
|
|
707
|
-
: {}),
|
|
708
|
-
...(applied.landed.includes("name") && applied.name ? { name: applied.name } : {}),
|
|
709
|
-
...(usage ? { usage } : {}),
|
|
710
|
-
},
|
|
711
|
-
});
|
|
712
|
-
}
|
|
713
|
-
if (applied.failure !== undefined) {
|
|
714
|
-
// `boundary_command_failed` means nothing durable landed. When something did, the client needs
|
|
715
|
-
// a different sentence — and the fields, so it knows what its retry would repeat.
|
|
716
|
-
return applied.landed.length === 0
|
|
717
|
-
? failed(applied.failure)
|
|
718
|
-
: {
|
|
719
|
-
ok: false,
|
|
720
|
-
error: {
|
|
721
|
-
code: PARTIAL_UPDATE_CODE,
|
|
722
|
-
message: `applied ${applied.landed.join(", ")}, then failed: ${String(applied.failure)} — read state() before retrying`,
|
|
723
|
-
retryable: false,
|
|
724
|
-
},
|
|
725
|
-
};
|
|
726
|
-
}
|
|
727
|
-
return { ok: true };
|
|
728
|
-
}));
|
|
729
|
-
/** Admission waits for binding and pi's own admission (its cut point), not the model call.
|
|
730
|
-
* Its execution scope survives the control response; completion is published only after releasing the shared
|
|
731
|
-
* lease. */
|
|
732
|
-
const compactOf = (session, instructions) => {
|
|
733
|
-
const admitted = Deferred.makeUnsafe();
|
|
734
|
-
let accepted = false;
|
|
735
|
-
let aborted = false;
|
|
736
|
-
const work = Effect.scoped(Effect.gen(function* () {
|
|
737
|
-
const b = boundary;
|
|
738
|
-
if (!b)
|
|
739
|
-
return unsupported("compact()");
|
|
740
|
-
const existing = yield* port(() => sessions.openIfExists(session));
|
|
741
|
-
if (!existing)
|
|
742
|
-
return noSuchSession(session);
|
|
743
|
-
// A record with no conversation in it (the list's `messageCount: 0`, e.g. one `update()` created before a first
|
|
744
|
-
// turn) has nothing to compact on any branch. Said before binding, because binding is a write: pi records the
|
|
745
|
-
// model and thinking level on a session that has no messages, so asking pi would add two entries and then
|
|
746
|
-
// refuse. Whether a record WITH messages has a cut point stays pi's decision (below).
|
|
747
|
-
if (!existing.getEntries().some(isConversationMessage))
|
|
748
|
-
return nothingToCompact();
|
|
749
|
-
yield* acquireSessionLease(b.lease, session);
|
|
750
|
-
const bound = yield* acquireSession(b.sessionFactory, session);
|
|
751
|
-
yield* Effect.acquireRelease(Effect.try({
|
|
752
|
-
try: () => bound.subscribe((event) => {
|
|
753
|
-
if (event.type !== "summarization_retry_scheduled")
|
|
754
|
-
return;
|
|
755
|
-
log.warn(`[fastagent] compaction retry ${event.attempt}/${event.maxAttempts} in ${event.delayMs}ms (session ${session}): ${event.errorMessage}`);
|
|
756
|
-
emitOwn(session, toRetryScheduledEvent(event));
|
|
757
|
-
}),
|
|
758
|
-
catch: (error) => new PortFailure(error),
|
|
759
|
-
}), (unsubscribe) => portCleanup("unsubscribe", unsubscribe));
|
|
760
|
-
// Admission is pi's own decision, observed rather than predicted: a prediction has to track pi's private
|
|
761
|
-
// cut-point rules release by release. It settles before the model call.
|
|
762
|
-
const compaction = startCompaction(bound, instructions, () => {
|
|
763
|
-
accepted = true;
|
|
764
|
-
compacting.set(session, {
|
|
765
|
-
abort: () => {
|
|
766
|
-
aborted = true;
|
|
767
|
-
bound.abortCompaction();
|
|
768
|
-
},
|
|
769
|
-
});
|
|
770
|
-
emitOwn(session, { type: "compaction_started", timestamp: Date.now(), data: {} });
|
|
771
|
-
Deferred.doneUnsafe(admitted, Effect.succeed({ ok: true }));
|
|
772
|
-
});
|
|
773
|
-
const admission = yield* portAbort("compact admission", () => compaction.admission, () => bound.abortCompaction());
|
|
774
|
-
if (admission === "nothing_to_compact")
|
|
775
|
-
return nothingToCompact();
|
|
776
|
-
// Any other error pi raises before admission. A served session has no known trigger: it always binds a
|
|
777
|
-
// model, and pi swallows a credential failure here and meets it at the model call (compaction_finished).
|
|
778
|
-
// NOT retryable all the same: it is pi's decision on this session, not a transport or lease fault that
|
|
779
|
-
// clears on its own, so repeating the call would meet it again.
|
|
780
|
-
if (admission !== "admitted") {
|
|
781
|
-
return {
|
|
782
|
-
ok: false,
|
|
783
|
-
error: { code: BOUNDARY_COMMAND_FAILED_CODE, message: String(admission.refused), retryable: false },
|
|
784
|
-
};
|
|
785
|
-
}
|
|
786
|
-
const done = yield* portAbort("compact", () => compaction.done, () => bound.abortCompaction());
|
|
787
|
-
return { summary: done.summary };
|
|
788
|
-
})).pipe(Effect.catchTag("SessionBusy", () => Effect.succeed(busy())), Effect.catchCause((cause) => Effect.succeed(failed(portError(cause)))), Effect.tap((result) => Effect.sync(() => {
|
|
789
|
-
if (accepted) {
|
|
790
|
-
compacting.delete(session);
|
|
791
|
-
emitOwn(session, {
|
|
792
|
-
type: "compaction_finished",
|
|
793
|
-
timestamp: Date.now(),
|
|
794
|
-
data: "summary" in result ? result : aborted ? { aborted: true } : { error: result.error.message },
|
|
795
|
-
});
|
|
796
|
-
if ("summary" in result)
|
|
797
|
-
publishUsage(session);
|
|
798
|
-
}
|
|
799
|
-
Deferred.doneUnsafe(admitted, Effect.succeed("summary" in result ? { ok: true } : result));
|
|
800
|
-
})));
|
|
801
|
-
// The admitted operation, rather than the request waiting on this Deferred, owns the scope.
|
|
802
|
-
Effect.runFork(work);
|
|
803
|
-
return Effect.runPromise(Deferred.await(admitted));
|
|
804
|
-
};
|
|
805
|
-
/**
|
|
806
|
-
* Copy a history into a new session. IDEMPOTENT by construction: `into` is the caller's id, so a
|
|
807
|
-
* repeat of a fork that already landed answers `ok: true` and writes nothing — a client retrying a
|
|
808
|
-
* request whose response it never saw does not get a second record. Provenance is what makes that
|
|
809
|
-
* safe rather than merely quiet: the same `into` naming a session that came from somewhere else is
|
|
810
|
-
* a rejection, not an overwrite.
|
|
811
|
-
*/
|
|
812
|
-
const forkOf = (options) => runBoundary(Effect.gen(function* () {
|
|
813
|
-
const { from, at, into } = options;
|
|
814
|
-
/** WHICH fork this is: source + branch point. Two forks of one session at different entries are
|
|
815
|
-
* different requests, so a retry of one must not be answered by the other. */
|
|
816
|
-
const provenance = `${from}@${at}`;
|
|
817
|
-
const b = boundary;
|
|
818
|
-
if (!b)
|
|
819
|
-
return unsupported("fork()");
|
|
820
|
-
if (!isAddressableSession(into))
|
|
821
|
-
return unaddressable(into);
|
|
822
|
-
const source = yield* port(() => sessions.openIfExists(from));
|
|
823
|
-
if (!source)
|
|
824
|
-
return noSuchSession(from);
|
|
825
|
-
// The entry predicate is the one `entries()` publishes by, so "everything published is forkable"
|
|
826
|
-
// holds by construction — the same argument the leaf move makes.
|
|
827
|
-
const entry = source.getEntry(at);
|
|
828
|
-
if (!entry || !isNavigable(entry)) {
|
|
829
|
-
return invalid(`entry "${at}" is not a forkable position in session "${from}" — entries() lists the ids`);
|
|
830
|
-
}
|
|
831
|
-
const existingTarget = yield* port(() => sessions.openIfExists(into));
|
|
832
|
-
if (existingTarget) {
|
|
833
|
-
// Already forked from HERE: the request already happened, so answering ok is the truth rather
|
|
834
|
-
// than a convenience. Anything else under that id is a different history, and saying yes would
|
|
835
|
-
// be the id lying about what it holds.
|
|
836
|
-
return forkProvenance(existingTarget) === provenance
|
|
837
|
-
? { ok: true }
|
|
838
|
-
: invalid(`session "${into}" already exists with a different history — fork mints nothing over it`);
|
|
839
|
-
}
|
|
840
|
-
// BOTH ends. The source lease keeps the copy from reading a history a run is mid-write on; the
|
|
841
|
-
// destination lease closes the window the existence check above leaves open — an invoke creating
|
|
842
|
-
// `into`, or a second fork from a DIFFERENT source (whose source lease is another key entirely),
|
|
843
|
-
// otherwise lands between that check and this write. tryAcquire never blocks, so taking two
|
|
844
|
-
// cannot deadlock.
|
|
845
|
-
yield* acquireSessionLease(b.lease, from);
|
|
846
|
-
yield* acquireSessionLease(b.lease, into);
|
|
847
|
-
// Recheck under both leases: another creator may have taken the target since validation.
|
|
848
|
-
const raced = yield* port(() => sessions.openIfExists(into));
|
|
849
|
-
if (raced) {
|
|
850
|
-
return forkProvenance(raced) === provenance
|
|
851
|
-
? { ok: true }
|
|
852
|
-
: invalid(`session "${into}" already exists with a different history — fork mints nothing over it`);
|
|
853
|
-
}
|
|
854
|
-
// portJoin: both leases are held for this write and must outlive it (effect-port.ts, rule 1).
|
|
855
|
-
yield* portJoin(() => sessions.fork(from, at, into, provenance));
|
|
856
|
-
return { ok: true };
|
|
857
|
-
}));
|
|
858
|
-
const deleteOf = (session) => runBoundary(Effect.gen(function* () {
|
|
859
|
-
const b = boundary;
|
|
860
|
-
if (!b)
|
|
861
|
-
return unsupported("delete()");
|
|
862
|
-
const existing = yield* port(() => sessions.openIfExists(session));
|
|
863
|
-
if (!existing)
|
|
864
|
-
return noSuchSession(session);
|
|
865
|
-
const removed = yield* Effect.scoped(Effect.gen(function* () {
|
|
866
|
-
yield* acquireSessionLease(b.lease, session);
|
|
867
|
-
// portJoin: the lease must outlive the removal (effect-port.ts, rule 1).
|
|
868
|
-
return yield* portJoin(() => sessions.delete(session));
|
|
869
|
-
}));
|
|
870
|
-
if (!removed)
|
|
871
|
-
return noSuchSession(session);
|
|
872
|
-
// The session is gone, so its live streams have nothing left to report: end them rather than
|
|
873
|
-
// hold connections open on a record that no longer exists. A client's reconnect then reads an
|
|
874
|
-
// empty `state()`, which is the truth.
|
|
875
|
-
for (const sub of [...(subscribers.get(session) ?? [])])
|
|
876
|
-
sub.close();
|
|
877
|
-
subscribers.delete(session);
|
|
878
|
-
return { ok: true };
|
|
879
|
-
}));
|
|
880
|
-
const control = {
|
|
881
|
-
capabilities: reads.capabilities,
|
|
882
|
-
commands: reads.commands,
|
|
883
|
-
sessions: {
|
|
884
|
-
// The one read that may REJECT (design §13): `[]` is what a deployment with no sessions
|
|
885
|
-
// answers, so a store that cannot be read must not borrow that shape. The transport turns the
|
|
886
|
-
// throw into a coded non-2xx; nothing here swallows it.
|
|
887
|
-
list: () => sessions.list(),
|
|
888
|
-
fork: forkOf,
|
|
889
|
-
// A PURE BINDING: an id and the closures above it. Nothing is checked here — the calls answer
|
|
890
|
-
// that, each in its own vocabulary — and nothing is cached, so two handles for one id are
|
|
891
|
-
// interchangeable.
|
|
892
|
-
get: (session) => ({
|
|
893
|
-
id: session,
|
|
894
|
-
state: () => reads.state(session),
|
|
895
|
-
entries: (options) => reads.entries(session, options),
|
|
896
|
-
image: (ref) => reads.image(session, ref),
|
|
897
|
-
events: () => reads.events(session),
|
|
898
|
-
update: (patch) => updateOf(session, patch),
|
|
899
|
-
steer: (prompt) => runAction(session, { type: "steer", prompt }),
|
|
900
|
-
followUp: (prompt) => runAction(session, { type: "follow_up", prompt }),
|
|
901
|
-
abort: () => runAction(session, { type: "abort" }),
|
|
902
|
-
compact: (options) => compactOf(session, options?.instructions),
|
|
903
|
-
delete: () => deleteOf(session),
|
|
904
|
-
}),
|
|
905
|
-
},
|
|
906
|
-
};
|
|
907
|
-
return { control, observer };
|
|
908
|
-
}
|