@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,44 +1,18 @@
|
|
|
1
1
|
/** Resident cron and wake polling. */
|
|
2
2
|
import * as Cause from "effect/Cause";
|
|
3
3
|
import * as Clock from "effect/Clock";
|
|
4
|
+
import * as Deferred from "effect/Deferred";
|
|
5
|
+
import * as Result from "effect/Result";
|
|
4
6
|
import * as Effect from "effect/Effect";
|
|
5
|
-
import {
|
|
7
|
+
import { join } from "node:path";
|
|
8
|
+
import { AgentError } from "../agent-api.js";
|
|
9
|
+
import { conversationSessions } from "../conversation-sessions.js";
|
|
6
10
|
import { PortFailure } from "../effect-port.js";
|
|
7
11
|
import { beginWork } from "../channels/busy.js";
|
|
8
12
|
import { log } from "../log.js";
|
|
9
13
|
import { nextRun } from "./cron.js";
|
|
10
|
-
import { routineSession } from "./routine.js";
|
|
11
14
|
import { claimSlot, latestFire, settleClaim } from "./state.js";
|
|
12
|
-
import {
|
|
13
|
-
/**
|
|
14
|
-
* Account for a claim whose turn never reported. The slot is claimed BEFORE the turn runs and shutdown does not wait
|
|
15
|
-
* for that turn (see `AgentService.close`), so a restart landing mid-fire leaves a claim that the next boot skips —
|
|
16
|
-
* the exact silence the history exists to prevent. Settled once, at the next boot, which also makes it idempotent.
|
|
17
|
-
*
|
|
18
|
-
* The claim is the ONE record here: it IS the decision, and the outcome is written back into it.
|
|
19
|
-
*
|
|
20
|
-
* NOT re-fired: a turn that kills its own process would then replay on every boot. Cron only: a killed wake-up
|
|
21
|
-
* leaves no claim behind to reconcile (`takeFirstDueWakeup` removes it before the turn starts).
|
|
22
|
-
*
|
|
23
|
-
* THE NEWEST CLAIM ONLY. A schedule's loop runs one turn at a time, so a killed process leaves exactly one claim
|
|
24
|
-
* unsettled — the one it was in. Walking the whole retained window would add nothing a resident process can
|
|
25
|
-
* produce, and would instead rewrite every claim an OLDER FORMAT left without an outcome, turning a window of runs
|
|
26
|
-
* that actually succeeded into `interrupted`.
|
|
27
|
-
*
|
|
28
|
-
* KNOWN FALSE POSITIVE: a second scheduler booting while the first is mid-turn sees a claim nothing accounts for
|
|
29
|
-
* YET, and reports it; the first process then overwrites the outcome when its turn settles. Telling them apart needs
|
|
30
|
-
* the claimer's liveness, which is a lease, not a claim — and the whole point of the claim is that it needs none.
|
|
31
|
-
*/
|
|
32
|
-
function markInterruptedFire(stateRoot, name, fire) {
|
|
33
|
-
if (fire === undefined || fire.outcome !== undefined)
|
|
34
|
-
return;
|
|
35
|
-
log.warn(`[schedule] ${name}: the fire claimed at ${fire.firedAt} never finished — the process stopped mid-turn ` +
|
|
36
|
-
`and that slot stays skipped (see \`fastagent routine history ${name}\`)`);
|
|
37
|
-
// `fire.slot` came from a claim file name, which `listClaims` admits only when it round-trips through this same
|
|
38
|
-
// conversion — so the Date is valid and `settleClaim` writes back the file it was read from. No duration: this
|
|
39
|
-
// turn was never timed, and the process that could have timed it is gone.
|
|
40
|
-
settleClaim(stateRoot, name, new Date(fire.slot), "interrupted");
|
|
41
|
-
}
|
|
15
|
+
import { requeueWakeup, takeFirstDueWakeup } from "./wakeups.js";
|
|
42
16
|
/**
|
|
43
17
|
* The longest a single armed wait sleeps before looking at the wall clock again.
|
|
44
18
|
*
|
|
@@ -66,67 +40,94 @@ function oneLine(text) {
|
|
|
66
40
|
const folded = text.replace(/\s+/g, " ").trim();
|
|
67
41
|
return folded === "" ? "" : `: ${folded}`;
|
|
68
42
|
}
|
|
43
|
+
/** What a wait for a message's end gave instead of its end, as the one line this log says it with. */
|
|
44
|
+
async function untilEnded(agent, key, signal) {
|
|
45
|
+
return agent.messages.get(key, { wait: Number.POSITIVE_INFINITY, signal }).then((message) => ({ ended: message }), (error) => ({
|
|
46
|
+
unfinished: `waiting for its end failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
47
|
+
}));
|
|
48
|
+
}
|
|
69
49
|
/**
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
50
|
+
* Send one message and wait for its end; `onAccepted` hears that it was recorded. Interrupted, it stops waiting, and
|
|
51
|
+
* the agent goes on: a Caller that stops waiting only lets go (SPEC.md §4).
|
|
52
|
+
*/
|
|
53
|
+
async function sendAndWait(agent, request, onAccepted, signal) {
|
|
54
|
+
let message;
|
|
55
|
+
try {
|
|
56
|
+
message = await agent.messages.send(request);
|
|
57
|
+
}
|
|
58
|
+
catch (error) {
|
|
59
|
+
if (!(error instanceof AgentError))
|
|
60
|
+
throw error;
|
|
61
|
+
if (error.code === "unconfirmed")
|
|
62
|
+
return { unfinished: `whether it was recorded is not known: ${error.message}` };
|
|
63
|
+
return { refused: error.message, busy: error.code === "busy" };
|
|
64
|
+
}
|
|
65
|
+
onAccepted();
|
|
66
|
+
return untilEnded(agent, { sessionId: message.sessionId, messageId: message.id }, signal);
|
|
67
|
+
}
|
|
68
|
+
/** How a message ended, as the one word a claim keeps, and the detail worth a log line. */
|
|
69
|
+
function settled(message) {
|
|
70
|
+
switch (message.status) {
|
|
71
|
+
case "completed":
|
|
72
|
+
case "incomplete":
|
|
73
|
+
return { fire: "completed" };
|
|
74
|
+
case "canceled":
|
|
75
|
+
return { fire: "failed", detail: "canceled" };
|
|
76
|
+
default:
|
|
77
|
+
return { fire: "failed", detail: message.error?.message ?? `the message ended ${message.status}` };
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* One invoke, read to its message's end, with what became of it said in the log.
|
|
77
82
|
*
|
|
78
|
-
*
|
|
79
|
-
*
|
|
83
|
+
* WHAT THE AGENT SAID IS NOT LOGGED. It is already durable: every fire runs in the session named in the line below
|
|
84
|
+
* (the schedule's own, the asking conversation's for a wake-up), kept in the instance's storage like any other.
|
|
85
|
+
* Copying the reply into the log would be a second store of the same text, unstructured, in a stream with a wider
|
|
86
|
+
* audience and a bound that differs per host.
|
|
80
87
|
*/
|
|
81
|
-
|
|
88
|
+
function runTurn(agent, label, request, onAccepted = () => { }) {
|
|
82
89
|
return Effect.gen(function* () {
|
|
83
90
|
const clock = yield* Clock.Clock;
|
|
84
91
|
const startedAt = clock.currentTimeMillisUnsafe();
|
|
85
92
|
const elapsed = () => clock.currentTimeMillisUnsafe() - startedAt;
|
|
86
|
-
log.info(`[schedule] ${label} firing (session=${
|
|
87
|
-
|
|
88
|
-
try:
|
|
89
|
-
let failed;
|
|
90
|
-
let busy = false;
|
|
91
|
-
// The text deltas are iterated and dropped. Accumulating them would be a second copy of what the session
|
|
92
|
-
// already holds, and no caller here reads it — the outcome is the whole record (docs/design/core.md §8).
|
|
93
|
-
for await (const e of agent.invoke({ session }, { text: prompt })) {
|
|
94
|
-
if (e.type === "failed") {
|
|
95
|
-
failed = e.details;
|
|
96
|
-
busy = e.code === SESSION_BUSY_CODE;
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
return { busy: failed !== undefined && busy, failed };
|
|
100
|
-
},
|
|
93
|
+
log.info(`[schedule] ${label} firing (session=${request.sessionId})`);
|
|
94
|
+
const read = yield* Effect.tryPromise({
|
|
95
|
+
try: (signal) => sendAndWait(agent, request, onAccepted, signal),
|
|
101
96
|
catch: (cause) => new PortFailure(cause),
|
|
102
|
-
}).pipe(Effect.
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
}
|
|
122
|
-
}
|
|
97
|
+
}).pipe(Effect.result);
|
|
98
|
+
const ms = elapsed();
|
|
99
|
+
if (Result.isFailure(read)) {
|
|
100
|
+
// Not an AgentError: a fault of the agent's, so this fire's failure, never a refusal.
|
|
101
|
+
const failed = String(read.failure.cause);
|
|
102
|
+
log.error(`[schedule] ${label} errored (${ms}ms)${oneLine(failed)}`);
|
|
103
|
+
return { fire: "failed", busy: false, failed, ms };
|
|
104
|
+
}
|
|
105
|
+
const end = read.success;
|
|
106
|
+
if ("unfinished" in end) {
|
|
107
|
+
log.error(`[schedule] ${label} failed (${ms}ms)${oneLine(end.unfinished)}`);
|
|
108
|
+
return { fire: "failed", busy: false, failed: end.unfinished, ms };
|
|
109
|
+
}
|
|
110
|
+
if ("refused" in end) {
|
|
111
|
+
// BUSY IS NOT A FAILURE: "the previous run is still going" and "the model call died" need different reactions.
|
|
112
|
+
if (end.busy)
|
|
113
|
+
log.info(`[schedule] ${label}: occurrence skipped (session busy) (${ms}ms)`);
|
|
114
|
+
else
|
|
115
|
+
log.error(`[schedule] ${label} refused (${ms}ms)${oneLine(end.refused)}`);
|
|
116
|
+
return { fire: "failed", busy: end.busy, failed: end.refused, ms };
|
|
117
|
+
}
|
|
118
|
+
const { fire, detail } = settled(end.ended);
|
|
119
|
+
if (fire === "completed")
|
|
120
|
+
log.info(`[schedule] ${label} completed (${ms}ms)`);
|
|
121
|
+
else
|
|
122
|
+
log.error(`[schedule] ${label} ${fire} (${ms}ms)${oneLine(detail ?? "")}`);
|
|
123
|
+
return { fire, busy: false, failed: fire === "completed" ? undefined : detail, ms };
|
|
123
124
|
});
|
|
124
125
|
}
|
|
125
126
|
/** Shared resident/external claim → run → settle. */
|
|
126
|
-
export function
|
|
127
|
+
export function fireScheduleOnce(opts) {
|
|
127
128
|
return Effect.gen(function* () {
|
|
128
129
|
const clock = yield* Clock.Clock;
|
|
129
|
-
const { agent, stateRoot,
|
|
130
|
+
const { agent, stateRoot, schedule: s, slot, now = () => new Date(clock.currentTimeMillisUnsafe()) } = opts;
|
|
130
131
|
const firedAt = now();
|
|
131
132
|
const skippedReason = yield* Effect.try({
|
|
132
133
|
try: () => {
|
|
@@ -154,9 +155,29 @@ export function fireRoutineOnce(opts) {
|
|
|
154
155
|
});
|
|
155
156
|
if (skippedReason !== undefined)
|
|
156
157
|
return { fired: false, skippedReason, ms: 0 };
|
|
157
|
-
|
|
158
|
+
// Before anything is sent: the session it continues (created and recorded the first time), and its prompt.
|
|
159
|
+
const prepared = yield* Effect.tryPromise({
|
|
160
|
+
try: async () => ({
|
|
161
|
+
sessionId: await scheduleSessions(stateRoot).of(s.name, agent),
|
|
162
|
+
text: typeof s.prompt === "string" ? s.prompt : await s.prompt({ at: slot }),
|
|
163
|
+
}),
|
|
164
|
+
catch: (cause) => new PortFailure(cause),
|
|
165
|
+
}).pipe(Effect.result);
|
|
166
|
+
if (Result.isFailure(prepared)) {
|
|
167
|
+
const failed = `could not prepare the fire: ${String(prepared.failure.cause)}`;
|
|
168
|
+
log.error(`[schedule] ${s.name} failed${oneLine(failed)}`);
|
|
169
|
+
settleClaim(stateRoot, s.name, slot, "failed", 0);
|
|
170
|
+
return { fired: true, failed, ms: 0 };
|
|
171
|
+
}
|
|
172
|
+
// An occurrence on a busy session is refused, and so skipped; its key is the instant, so one occurrence is one run.
|
|
173
|
+
const r = yield* runTurn(agent, s.name, {
|
|
174
|
+
sessionId: prepared.success.sessionId,
|
|
175
|
+
content: [{ type: "text", text: prepared.success.text }],
|
|
176
|
+
whenBusy: "reject",
|
|
177
|
+
idempotencyKey: slot.toISOString(),
|
|
178
|
+
});
|
|
158
179
|
// OVERLAP IS NOT FAILURE. The claim still stands — this occurrence is decided and will not be retried — but
|
|
159
|
-
// what decided it was the previous turn still holding
|
|
180
|
+
// what decided it was the previous turn still holding the schedule's session, which is the policy every scheduler
|
|
160
181
|
// names (k8s `concurrencyPolicy: Forbid`, Temporal's `Skip`) and none of them reports as an error.
|
|
161
182
|
if (r.busy) {
|
|
162
183
|
settleClaim(stateRoot, s.name, slot, "skipped", r.ms);
|
|
@@ -167,10 +188,89 @@ export function fireRoutineOnce(opts) {
|
|
|
167
188
|
ms: r.ms,
|
|
168
189
|
};
|
|
169
190
|
}
|
|
170
|
-
settleClaim(stateRoot, s.name, slot, r.
|
|
171
|
-
return { fired: true, failed: r.failed, ms: r.ms };
|
|
191
|
+
settleClaim(stateRoot, s.name, slot, r.fire, r.ms);
|
|
192
|
+
return { fired: true, ...(r.failed === undefined ? {} : { failed: r.failed }), ms: r.ms };
|
|
172
193
|
}).pipe(Effect.uninterruptible);
|
|
173
194
|
}
|
|
195
|
+
/**
|
|
196
|
+
* Fire `schedule` once, now, outside its clock (`fastagent schedules run`), the way its clock would: a newest claim no
|
|
197
|
+
* fire reported is accounted for first, as a boot does, then this instant is the slot, claimed and settled as any.
|
|
198
|
+
*/
|
|
199
|
+
export function fireScheduleNow(opts) {
|
|
200
|
+
return Effect.gen(function* () {
|
|
201
|
+
const { agent, stateRoot, schedule } = opts;
|
|
202
|
+
const fire = yield* Effect.try({
|
|
203
|
+
try: () => latestFire(stateRoot, schedule.name),
|
|
204
|
+
catch: (cause) => new PortFailure(cause),
|
|
205
|
+
});
|
|
206
|
+
if (fire !== undefined && fire.outcome === undefined) {
|
|
207
|
+
yield* accountForUnreportedFire(agent, stateRoot, schedule.name, fire);
|
|
208
|
+
}
|
|
209
|
+
const slot = new Date((yield* Clock.Clock).currentTimeMillisUnsafe());
|
|
210
|
+
return { slot, ...(yield* fireScheduleOnce({ agent, stateRoot, schedule, slot })) };
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* Account for a claim whose fire never reported: the slot is claimed BEFORE its message is sent, and shutdown does not
|
|
215
|
+
* wait for the message (see `AgentService.close`), so a restart landing mid-fire leaves a claim nothing settled — the
|
|
216
|
+
* exact silence the history exists to prevent. Read once, at the next boot. The claim is the ONE record here: it IS
|
|
217
|
+
* the decision, and the outcome is written back into it.
|
|
218
|
+
*
|
|
219
|
+
* WHAT BECAME OF ITS MESSAGE decides the outcome. One the agent took is durable: it is answered after the restart
|
|
220
|
+
* (or already was), so its end is read through its key and written back, as the fire that ran would have. One that was
|
|
221
|
+
* never taken is `interrupted`, and NOT re-fired: a fire that kills its own process would then replay on every boot.
|
|
222
|
+
* Cron only: a killed wake-up leaves no claim behind (`takeFirstDueWakeup` removes it before its message is sent).
|
|
223
|
+
*
|
|
224
|
+
* THE NEWEST CLAIM ONLY. A schedule's loop runs one fire at a time, so a killed process leaves exactly one claim
|
|
225
|
+
* unsettled — the one it was in. Walking the whole retained window would add nothing a resident process can produce.
|
|
226
|
+
*/
|
|
227
|
+
function accountForUnreportedFire(agent, stateRoot, name, fire) {
|
|
228
|
+
return Effect.gen(function* () {
|
|
229
|
+
// `fire.slot` came from a claim file name, which `listClaims` admits only when it round-trips through this same
|
|
230
|
+
// conversion — so the Date is valid and `settleClaim` writes back the file it was read from.
|
|
231
|
+
const slot = new Date(fire.slot);
|
|
232
|
+
const key = slot.toISOString();
|
|
233
|
+
const sessionId = scheduleSessions(stateRoot).get(name);
|
|
234
|
+
const taken = sessionId === undefined
|
|
235
|
+
? undefined
|
|
236
|
+
: yield* Effect.tryPromise({
|
|
237
|
+
try: () => agent.messages.get({ sessionId, idempotencyKey: key }).catch((error) => {
|
|
238
|
+
if (error instanceof AgentError && error.code === "not_found")
|
|
239
|
+
return undefined;
|
|
240
|
+
throw error;
|
|
241
|
+
}),
|
|
242
|
+
catch: (cause) => new PortFailure(cause),
|
|
243
|
+
});
|
|
244
|
+
if (sessionId === undefined || taken === undefined) {
|
|
245
|
+
log.warn(`[schedule] ${name}: the fire claimed at ${fire.firedAt} never sent its message — the process stopped ` +
|
|
246
|
+
`before it, and that slot stays skipped (see \`fastagent schedules list\`)`);
|
|
247
|
+
// No duration: this fire was never timed, and the process that could have timed it is gone.
|
|
248
|
+
settleClaim(stateRoot, name, slot, "interrupted");
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
log.info(`[schedule] ${name}: the fire claimed at ${fire.firedAt} was taken before a restart — reading its end`);
|
|
252
|
+
const end = yield* Effect.tryPromise({
|
|
253
|
+
try: (signal) => untilEnded(agent, { sessionId, idempotencyKey: key }, signal),
|
|
254
|
+
catch: (cause) => new PortFailure(cause),
|
|
255
|
+
});
|
|
256
|
+
if (!("ended" in end)) {
|
|
257
|
+
// Unsettled still, so the next boot reads it again.
|
|
258
|
+
log.warn(`[schedule] ${name}: the end of the fire claimed at ${fire.firedAt} is not known yet${oneLine("refused" in end ? end.refused : end.unfinished)}`);
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
const { fire: outcome, detail } = settled(end.ended);
|
|
262
|
+
if (outcome === "completed")
|
|
263
|
+
log.info(`[schedule] ${name}: the fire claimed at ${fire.firedAt} completed`);
|
|
264
|
+
else
|
|
265
|
+
log.error(`[schedule] ${name}: the fire claimed at ${fire.firedAt} ${outcome}${oneLine(detail ?? "")}`);
|
|
266
|
+
// No duration either: only its end was seen here.
|
|
267
|
+
settleClaim(stateRoot, name, slot, outcome);
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
/** Which session each schedule continues: one per schedule, so the agent sees what its earlier runs did. */
|
|
271
|
+
export function scheduleSessions(stateRoot) {
|
|
272
|
+
return conversationSessions(join(stateRoot, "schedule", "sessions.json"));
|
|
273
|
+
}
|
|
174
274
|
/** Wakes re-enter a chat session; identify the agent's own instruction and its cancellation handle. */
|
|
175
275
|
function wakeEnvelope(w) {
|
|
176
276
|
const tag = w.cron
|
|
@@ -182,21 +282,19 @@ export function createScheduler(options) {
|
|
|
182
282
|
return Effect.gen(function* () {
|
|
183
283
|
const clock = yield* Clock.Clock;
|
|
184
284
|
const fork = Effect.runForkWith(yield* Effect.context());
|
|
185
|
-
const { agent, stateRoot,
|
|
285
|
+
const { agent, stateRoot, schedules, localClock = true, now = () => new Date(clock.currentTimeMillisUnsafe()), } = options;
|
|
186
286
|
const loops = new Set();
|
|
187
287
|
let stopped = false;
|
|
188
|
-
const launch = (label, work) => {
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
}));
|
|
199
|
-
};
|
|
288
|
+
const launch = (label, work) => fork(Effect.withFiber((fiber) => {
|
|
289
|
+
// Publish ownership before a synchronous agent callback can re-enter stop().
|
|
290
|
+
loops.add(fiber);
|
|
291
|
+
return work.pipe(Effect.catchCause((cause) => Effect.sync(() => {
|
|
292
|
+
if (!Cause.hasInterruptsOnly(cause))
|
|
293
|
+
log.error(`[schedule] ${label}: stopped unexpectedly: ${String(Cause.squash(cause))}`);
|
|
294
|
+
})), Effect.ensuring(Effect.sync(() => {
|
|
295
|
+
loops.delete(fiber);
|
|
296
|
+
})));
|
|
297
|
+
}));
|
|
200
298
|
const cronLoop = (s, first) => Effect.gen(function* () {
|
|
201
299
|
let due = first;
|
|
202
300
|
while (due) {
|
|
@@ -209,14 +307,19 @@ export function createScheduler(options) {
|
|
|
209
307
|
}
|
|
210
308
|
// `slot: due` is what two schedulers have in common: both compute the same cron instant, so both try to
|
|
211
309
|
// claim the same name and exactly one wins. Claiming `now()` would give them different names.
|
|
212
|
-
yield*
|
|
310
|
+
yield* fireScheduleOnce({ agent, stateRoot, schedule: s, slot: due, now }).pipe(Effect.catchTag("PortFailure", (error) => Effect.sync(() => {
|
|
213
311
|
// Nothing to record: this fault happens BEFORE the claim exists (that is what keeps the slot
|
|
214
312
|
// unburned), so there is no claim to write an outcome into.
|
|
215
313
|
log.error(`[schedule] ${s.name}: fire failed (skipping this run, schedule stays armed): ${String(error.cause)}`);
|
|
216
314
|
})), Effect.uninterruptible);
|
|
217
|
-
due =
|
|
315
|
+
due = nextRun(s.cron, s.tz, now());
|
|
218
316
|
}
|
|
219
317
|
});
|
|
318
|
+
/**
|
|
319
|
+
* Fire the first due wake-up into its session, queued behind whatever runs there. The wake-up is taken out of the
|
|
320
|
+
* store first, so accepted is delivered: the pump goes on to the next one, and how the message ends is followed
|
|
321
|
+
* apart.
|
|
322
|
+
*/
|
|
220
323
|
const wakeOnce = Effect.gen(function* () {
|
|
221
324
|
const w = yield* Effect.try({
|
|
222
325
|
try: () => takeFirstDueWakeup(stateRoot, now()),
|
|
@@ -224,27 +327,31 @@ export function createScheduler(options) {
|
|
|
224
327
|
});
|
|
225
328
|
if (!w)
|
|
226
329
|
return false;
|
|
330
|
+
const label = `wake ${w.id.slice(0, 8)}`;
|
|
331
|
+
const request = {
|
|
332
|
+
sessionId: w.session,
|
|
333
|
+
content: [{ type: "text", text: wakeEnvelope(w) }],
|
|
334
|
+
whenBusy: "queue",
|
|
335
|
+
idempotencyKey: `wake:${w.id}@${w.fireAt}`,
|
|
336
|
+
};
|
|
337
|
+
// Counted until the message is accepted: from then on the message itself is the work in flight.
|
|
227
338
|
yield* Effect.acquireUseRelease(Effect.sync(beginWork), () => Effect.gen(function* () {
|
|
228
|
-
const
|
|
229
|
-
const
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
339
|
+
const accepted = yield* Deferred.make();
|
|
340
|
+
const delivered = () => void Deferred.doneUnsafe(accepted, Effect.void);
|
|
341
|
+
const followed = runTurn(agent, label, request, delivered).pipe(Effect.tap((r) =>
|
|
342
|
+
// Queued, it is refused only while the process or its session's queue is full: for now. A recurring
|
|
343
|
+
// wake-up has its next occurrence; a one-shot one goes back in the store, for the next poll.
|
|
344
|
+
r.busy && w.cron === undefined
|
|
345
|
+
? Effect.try({
|
|
346
|
+
try: () => {
|
|
347
|
+
requeueWakeup(stateRoot, w, new Date(now().getTime() + WAKEUP_POLL_MS));
|
|
348
|
+
log.info(`[schedule] ${label}: refused for now — retrying next poll (${r.failed})`);
|
|
349
|
+
},
|
|
234
350
|
catch: (cause) => new PortFailure(cause),
|
|
235
|
-
})
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
log.error(`[schedule] ${label}: dropped after too many busy retries`);
|
|
240
|
-
}
|
|
241
|
-
else if (r.busy && w.cron) {
|
|
242
|
-
// `runTurn` already said it was skipped for a busy session; this adds the one thing it cannot know —
|
|
243
|
-
// that a recurring wake has a next occurrence, so nothing is deferred and nothing is lost.
|
|
244
|
-
log.info(`[schedule] ${label}: next fires per cron`);
|
|
245
|
-
}
|
|
246
|
-
// A wake-up has no claim to settle (it is removed from the store before the turn starts), so its whole
|
|
247
|
-
// record is the log lines above and `runTurn`'s — deferred, dropped, failed, completed.
|
|
351
|
+
}).pipe(Effect.catchTag("PortFailure", (error) => Effect.sync(() => log.error(`[schedule] ${label}: refused, and could not be kept for a retry, so it is gone: ${String(error.cause)}`))))
|
|
352
|
+
: Effect.void), Effect.ensuring(Effect.sync(delivered)));
|
|
353
|
+
launch(label, Effect.asVoid(followed));
|
|
354
|
+
yield* Deferred.await(accepted);
|
|
248
355
|
}), (done) => Effect.sync(done));
|
|
249
356
|
return true;
|
|
250
357
|
}).pipe(
|
|
@@ -261,6 +368,16 @@ export function createScheduler(options) {
|
|
|
261
368
|
yield* Effect.sleep(WAKEUP_POLL_MS);
|
|
262
369
|
}
|
|
263
370
|
});
|
|
371
|
+
/** Arm `s` to fire next at `due`, or say it never will. */
|
|
372
|
+
const arm = (s, due) => {
|
|
373
|
+
if (!localClock)
|
|
374
|
+
return;
|
|
375
|
+
if (!due) {
|
|
376
|
+
log.warn(`[schedule] ${s.name}: cron "${s.cron}" will never fire again — not armed`);
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
379
|
+
launch(s.name, cronLoop(s, due));
|
|
380
|
+
};
|
|
264
381
|
return {
|
|
265
382
|
start() {
|
|
266
383
|
stopped = false;
|
|
@@ -270,34 +387,31 @@ export function createScheduler(options) {
|
|
|
270
387
|
// one to run blind, it is a boot failure. The rest of the window is history, and history does not gate a
|
|
271
388
|
// boot (`latestFire`).
|
|
272
389
|
const lastFires = new Map();
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
390
|
+
for (const s of schedules) {
|
|
391
|
+
const fire = latestFire(stateRoot, s.name);
|
|
392
|
+
if (fire === undefined)
|
|
393
|
+
continue;
|
|
394
|
+
lastFires.set(s.name, fire.firedAt);
|
|
395
|
+
if (fire.outcome !== undefined)
|
|
396
|
+
continue;
|
|
397
|
+
const accounted = accountForUnreportedFire(agent, stateRoot, s.name, fire).pipe(Effect.catchTag("PortFailure", (error) => Effect.sync(() =>
|
|
398
|
+
// Unsettled still, so the next boot reads it again.
|
|
399
|
+
log.error(`[schedule] ${s.name}: could not account for the fire claimed at ${fire.firedAt}: ${String(error.cause)}`))));
|
|
400
|
+
launch(s.name, accounted);
|
|
280
401
|
}
|
|
281
402
|
const current = now();
|
|
282
|
-
for (const s of
|
|
403
|
+
for (const s of schedules) {
|
|
283
404
|
if (stopped)
|
|
284
405
|
break;
|
|
285
|
-
//
|
|
286
|
-
// (`POST /run`, `fastagent routine run`), so there is nothing here to arm and nothing to warn about.
|
|
287
|
-
if (s.cron === undefined)
|
|
288
|
-
continue;
|
|
289
|
-
// FROM THE LAST FIRE, or from now when there was none. A routine that has never fired does not catch
|
|
406
|
+
// FROM THE LAST FIRE, or from now when there was none. A schedule that has never fired does not catch
|
|
290
407
|
// up the occurrence it missed while the process was down: nothing recorded that it was ever armed then,
|
|
291
408
|
// so "missed" is not a fact this process has. Every boot after the first resumes from the claim.
|
|
292
409
|
const last = lastFires.get(s.name);
|
|
293
410
|
const due = nextRun(s.cron, s.tz, last ? new Date(last) : current);
|
|
294
|
-
if (
|
|
295
|
-
log.warn(`[schedule] ${s.name}: cron "${s.cron}" will never fire again — not armed`);
|
|
296
|
-
continue;
|
|
297
|
-
}
|
|
298
|
-
if (due.getTime() <= current.getTime())
|
|
411
|
+
if (localClock && due && due.getTime() <= current.getTime()) {
|
|
299
412
|
log.info(`[schedule] ${s.name}: catching up a missed run`);
|
|
300
|
-
|
|
413
|
+
}
|
|
414
|
+
arm(s, due);
|
|
301
415
|
}
|
|
302
416
|
if (!stopped)
|
|
303
417
|
launch("wake-up poll", wakeLoop);
|
package/dist/schedule/state.d.ts
CHANGED
|
@@ -10,7 +10,7 @@ export declare function writeScheduleFile(path: string, value: unknown): void;
|
|
|
10
10
|
* How a claimed occurrence ended.
|
|
11
11
|
*
|
|
12
12
|
* `skipped` is NOT a failure and is the reason this is an enum rather than a boolean: a schedule's turns share one
|
|
13
|
-
* `
|
|
13
|
+
* `schedule:<name>` session, so an occurrence arriving while the previous one is still running is refused by that
|
|
14
14
|
* session, and recording it as `failed` made "the last run was still going" indistinguishable from "the model call
|
|
15
15
|
* died" — two things an operator reacts to completely differently. Every scheduler names this: Kubernetes calls it
|
|
16
16
|
* `concurrencyPolicy: Forbid`, Temporal calls it the `Skip` overlap policy, and neither treats it as an error.
|
|
@@ -25,16 +25,6 @@ export interface Fire {
|
|
|
25
25
|
outcome?: FireOutcome;
|
|
26
26
|
ms?: number;
|
|
27
27
|
}
|
|
28
|
-
/**
|
|
29
|
-
* A schedule name becomes a path segment (its claims live in `claims/<name>/`), so the rule is exactly the safety
|
|
30
|
-
* boundary and nothing more: a name may not leave that directory. The name comes from a filename under `routines/`,
|
|
31
|
-
* which already cannot contain a separator — so what a stricter rule would actually reject is legal filenames
|
|
32
|
-
* (`每日简报`, `my schedule`), whose only symptom would be a schedule that silently never fires again.
|
|
33
|
-
*
|
|
34
|
-
* One definition, two enforcers: `discover.ts` refuses an illegal one where the author can see which file is wrong,
|
|
35
|
-
* and `claimDir` asserts it again for a programmatic caller.
|
|
36
|
-
*/
|
|
37
|
-
export declare function isSafeScheduleName(name: string): boolean;
|
|
38
28
|
/**
|
|
39
29
|
* Why a slot is not ours. `duplicate` is ordinary (the same delivery arrived twice, or another scheduler took it and
|
|
40
30
|
* is running it now); `stale` means the schedule has moved past this instant and the slot will never run — a turn
|
|
@@ -75,9 +65,9 @@ export declare function claimSlot(stateRoot: string, name: string, slot: Date, f
|
|
|
75
65
|
* the history. Nothing here grows: the pruning that keeps the claim gate cheap keeps the history's size fixed too,
|
|
76
66
|
* which is why there is no separate audit file to rotate (what the turn SAID is in its session, not here).
|
|
77
67
|
*
|
|
78
|
-
* THE HISTORY read, and its only caller is the read-only `
|
|
79
|
-
* one-line refusal.
|
|
80
|
-
* for an operator to look at has no business
|
|
68
|
+
* THE HISTORY read, and its only caller is the read-only `schedules list --json` — which translates a read fault into a
|
|
69
|
+
* one-line refusal. Its text mode and the serving boot read `latestFire` instead: each asks about ONE claim, and a
|
|
70
|
+
* window of files kept for an operator to look at has no business failing either.
|
|
81
71
|
*/
|
|
82
72
|
export declare function readFires(stateRoot: string, name: string): Fire[];
|
|
83
73
|
/**
|
package/dist/schedule/state.js
CHANGED
|
@@ -32,8 +32,8 @@ export function writeScheduleFile(path, value) {
|
|
|
32
32
|
}
|
|
33
33
|
// ── claims/: one file per fired slot — the DECISION to fire, made atomically ──
|
|
34
34
|
/**
|
|
35
|
-
* How many claims to keep per schedule — and therefore how far back
|
|
36
|
-
* the claims ARE the history. The gate below reads only the newest, so this number answers the history's question,
|
|
35
|
+
* How many claims to keep per schedule — and therefore how far back the fire history under
|
|
36
|
+
* `<stateRoot>/schedule/claims/<name>/` reaches, since the claims ARE the history. The gate below reads only the newest, so this number answers the history's question,
|
|
37
37
|
* "did last night's run silently fail?": at 512 a minute cron keeps ~8.5 hours, an hourly one ~3 weeks.
|
|
38
38
|
*
|
|
39
39
|
* Raise it by FILE COUNT, not by byte count: 512 short lines are ~25 KB of content but ~2 MiB of blocks and 512
|
|
@@ -151,14 +151,14 @@ function readClaim(dir, name) {
|
|
|
151
151
|
}
|
|
152
152
|
/**
|
|
153
153
|
* A schedule name becomes a path segment (its claims live in `claims/<name>/`), so the rule is exactly the safety
|
|
154
|
-
* boundary and nothing more: a name may not leave that directory. The name comes from a filename under `
|
|
154
|
+
* boundary and nothing more: a name may not leave that directory. The name comes from a filename under `schedules/`,
|
|
155
155
|
* which already cannot contain a separator — so what a stricter rule would actually reject is legal filenames
|
|
156
156
|
* (`每日简报`, `my schedule`), whose only symptom would be a schedule that silently never fires again.
|
|
157
157
|
*
|
|
158
158
|
* One definition, two enforcers: `discover.ts` refuses an illegal one where the author can see which file is wrong,
|
|
159
159
|
* and `claimDir` asserts it again for a programmatic caller.
|
|
160
160
|
*/
|
|
161
|
-
|
|
161
|
+
function isSafeScheduleName(name) {
|
|
162
162
|
return name !== "" && !/[/\\]/.test(name) && !name.includes("\u0000") && name !== "." && name !== "..";
|
|
163
163
|
}
|
|
164
164
|
/** This function builds a path from it, so a name that can leave `claims/<name>/` is a bug, not an input. */
|
|
@@ -226,9 +226,9 @@ export function claimSlot(stateRoot, name, slot, firedAt) {
|
|
|
226
226
|
* the history. Nothing here grows: the pruning that keeps the claim gate cheap keeps the history's size fixed too,
|
|
227
227
|
* which is why there is no separate audit file to rotate (what the turn SAID is in its session, not here).
|
|
228
228
|
*
|
|
229
|
-
* THE HISTORY read, and its only caller is the read-only `
|
|
230
|
-
* one-line refusal.
|
|
231
|
-
* for an operator to look at has no business
|
|
229
|
+
* THE HISTORY read, and its only caller is the read-only `schedules list --json` — which translates a read fault into a
|
|
230
|
+
* one-line refusal. Its text mode and the serving boot read `latestFire` instead: each asks about ONE claim, and a
|
|
231
|
+
* window of files kept for an operator to look at has no business failing either.
|
|
232
232
|
*/
|
|
233
233
|
export function readFires(stateRoot, name) {
|
|
234
234
|
const dir = claimDir(stateRoot, name);
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import * as Effect from "effect/Effect";
|
|
2
|
-
import { type WakeAlarm } from "../channels/agentcore-protocol.ts";
|
|
2
|
+
import { type RecurringSchedule, type WakeAlarm } from "../channels/agentcore-protocol.ts";
|
|
3
3
|
import { type Wakeup } from "./wakeups.ts";
|
|
4
|
+
import type { Schedule } from "./schedule.ts";
|
|
4
5
|
/**
|
|
5
6
|
* Persist the forwarder URL the adapter saw in an envelope (write-if-changed — envelopes arrive on every turn, the
|
|
6
7
|
* file should not churn).
|
|
@@ -13,14 +14,33 @@ export declare function readWakeAlarmUrl(stateRoot: string): string | undefined;
|
|
|
13
14
|
* the pending store is the durable desired state — so "give up" means "until the next mirror", never "lost").
|
|
14
15
|
*/
|
|
15
16
|
export declare const MAX_SYNC_ATTEMPTS = 5;
|
|
16
|
-
/**
|
|
17
|
+
/**
|
|
18
|
+
* After a whole sync gives up, how long until it is tried again, for as long as this process lives. Outside the busy
|
|
19
|
+
* count, so a container that is only waiting to retry can still be reclaimed; its next start mirrors again.
|
|
20
|
+
*/
|
|
21
|
+
export declare const HEAL_MS: number;
|
|
22
|
+
/**
|
|
23
|
+
* Pending wake-ups → the desired one-shot alarms. A wake-up already due is left out (see {@link DUE_MARGIN_MS}): the
|
|
24
|
+
* container is awake for it, and its wake pump fires it.
|
|
25
|
+
*
|
|
26
|
+
* ONE ID PER INSTANT, never per wake-up. An alarm deletes itself once it has fired, and a recurring wake-up's claim
|
|
27
|
+
* mirrors its next instant right after: an id reused for it would update the alarm EventBridge is about to delete,
|
|
28
|
+
* and the next instant would be deleted with it. With one id per instant, mirroring again is idempotent.
|
|
29
|
+
*/
|
|
17
30
|
export declare function toAlarms(pending: Wakeup[], now: Date): WakeAlarm[];
|
|
31
|
+
/**
|
|
32
|
+
* Each armed schedule → the recurring EventBridge schedule that fires it. Discovery admits only a cron that
|
|
33
|
+
* translates (schedule/discover.ts), so one that does not here is a defect, and throws.
|
|
34
|
+
*/
|
|
35
|
+
export declare function toRecurring(schedules: readonly Schedule[]): RecurringSchedule[];
|
|
18
36
|
/**
|
|
19
37
|
* Build the wakeups sink for an AgentCore deployment (registered via `setWakeupsSink` by `start` when
|
|
20
38
|
* `FASTAGENT_AGENTCORE=1` + `FASTAGENT_WAKE_SECRET` are present).
|
|
21
39
|
*/
|
|
22
40
|
export declare function createWakeAlarmSink(options: {
|
|
23
41
|
secret: string;
|
|
42
|
+
/** The schedules armed now, mirrored as recurring schedules beside the wake-ups' alarms. */
|
|
43
|
+
schedules?: () => readonly Schedule[];
|
|
24
44
|
fetchImpl?: typeof fetch;
|
|
25
45
|
/** Injectable clock (tests); defaults to the wall clock. */
|
|
26
46
|
now?: () => Date;
|