@fastagent-sh/fastagent 0.21.1 → 0.22.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -12
- package/dist/agent.d.ts +24 -37
- package/dist/agent.js +7 -15
- package/dist/atomic-write.d.ts +13 -17
- package/dist/atomic-write.js +18 -29
- package/dist/bind.d.ts +12 -22
- package/dist/bind.js +18 -27
- package/dist/channel.d.ts +11 -33
- package/dist/channels/agentcore-limits.d.ts +2 -13
- package/dist/channels/agentcore-limits.js +2 -13
- package/dist/channels/agentcore-protocol.d.ts +67 -58
- package/dist/channels/agentcore-protocol.js +23 -15
- package/dist/channels/agentcore-service.d.ts +19 -33
- package/dist/channels/agentcore-service.js +176 -59
- package/dist/channels/agentcore.d.ts +39 -33
- package/dist/channels/agentcore.js +90 -224
- package/dist/channels/body.d.ts +15 -2
- package/dist/channels/body.js +24 -2
- package/dist/channels/busy.d.ts +1 -21
- package/dist/channels/busy.js +2 -39
- package/dist/channels/control.d.ts +17 -62
- package/dist/channels/control.js +74 -182
- package/dist/channels/define-channel.d.ts +39 -0
- package/dist/channels/define-channel.js +9 -0
- package/dist/channels/discover.d.ts +10 -31
- package/dist/channels/discover.js +37 -31
- package/dist/channels/feishu/bootstrap-token.d.ts +1 -8
- package/dist/channels/feishu/bootstrap-token.js +7 -23
- package/dist/channels/feishu/card.d.ts +14 -28
- package/dist/channels/feishu/card.js +16 -30
- package/dist/channels/feishu/cloud.d.ts +1 -5
- package/dist/channels/feishu/context-buffer.d.ts +4 -23
- package/dist/channels/feishu/context-buffer.js +8 -31
- package/dist/channels/feishu/crypto.d.ts +10 -5
- package/dist/channels/feishu/crypto.js +12 -19
- package/dist/channels/feishu/feishu-api.d.ts +2 -1
- package/dist/channels/feishu/feishu-api.js +5 -4
- package/dist/channels/feishu/feishu.d.ts +4 -10
- package/dist/channels/feishu/feishu.js +113 -227
- package/dist/channels/feishu/invoke-turn.d.ts +5 -4
- package/dist/channels/feishu/invoke-turn.js +36 -54
- package/dist/channels/feishu/model.d.ts +4 -21
- package/dist/channels/feishu/model.js +1 -8
- package/dist/channels/feishu/normalize.d.ts +2 -10
- package/dist/channels/feishu/normalize.js +7 -42
- package/dist/channels/feishu/parse.d.ts +9 -29
- package/dist/channels/feishu/parse.js +8 -26
- package/dist/channels/feishu/preview.d.ts +12 -14
- package/dist/channels/feishu/preview.js +108 -207
- package/dist/channels/feishu/register-app.d.ts +8 -23
- package/dist/channels/feishu/register-app.js +7 -20
- package/dist/channels/feishu/register-webhook.d.ts +2 -9
- package/dist/channels/feishu/register-webhook.js +9 -33
- package/dist/channels/feishu/scaffold/channel.ts +27 -19
- package/dist/channels/feishu/scaffold/channel.websocket.ts +31 -0
- package/dist/channels/feishu/setup-mode.d.ts +11 -36
- package/dist/channels/feishu/setup-mode.js +7 -26
- package/dist/channels/feishu/ws-ingress.d.ts +1 -2
- package/dist/channels/feishu/ws-ingress.js +6 -8
- package/dist/channels/http.d.ts +15 -15
- package/dist/channels/http.js +19 -9
- package/dist/channels/kit/attachment-path.d.ts +4 -5
- package/dist/channels/kit/attachment-path.js +10 -34
- package/dist/channels/kit/context-buffer.d.ts +14 -21
- package/dist/channels/kit/context-buffer.js +25 -38
- package/dist/channels/kit/delivery.d.ts +35 -0
- package/dist/channels/kit/delivery.js +129 -0
- package/dist/channels/kit/event-stream.d.ts +5 -0
- package/dist/channels/kit/event-stream.js +29 -0
- package/dist/channels/kit/invoke-turn-kit.d.ts +53 -26
- package/dist/channels/kit/invoke-turn-kit.js +89 -32
- package/dist/channels/kit/preview-kit.d.ts +18 -72
- package/dist/channels/kit/preview-kit.js +22 -88
- package/dist/channels/kit/signature.d.ts +3 -9
- package/dist/channels/kit/signature.js +3 -9
- package/dist/channels/kit/state.d.ts +1 -2
- package/dist/channels/kit/state.js +5 -14
- package/dist/channels/kit/stop-command.d.ts +1 -2
- package/dist/channels/kit/stop-command.js +6 -9
- package/dist/channels/kit/tasks.d.ts +1 -1
- package/dist/channels/kit/tasks.js +8 -21
- package/dist/channels/kit/text.d.ts +2 -16
- package/dist/channels/kit/text.js +23 -31
- package/dist/channels/kit/thread-participants.d.ts +4 -17
- package/dist/channels/kit/thread-participants.js +8 -55
- package/dist/channels/kit/transport.d.ts +26 -0
- package/dist/channels/kit/transport.js +20 -0
- package/dist/channels/kit/turn-queue.d.ts +7 -11
- package/dist/channels/kit/turn-queue.js +21 -39
- package/dist/channels/kit/turn-runner.d.ts +32 -18
- package/dist/channels/kit/turn-runner.js +98 -53
- package/dist/channels/kit/turn-store.d.ts +32 -32
- package/dist/channels/kit/turn-store.js +45 -88
- package/dist/channels/lark/lark.d.ts +1 -5
- package/dist/channels/lark/onboard.d.ts +3 -12
- package/dist/channels/lark/onboard.js +3 -13
- package/dist/channels/lark/scaffold/channel.ts +27 -19
- package/dist/channels/lark/scaffold/channel.websocket.ts +31 -0
- package/dist/channels/registration.d.ts +9 -35
- package/dist/channels/registration.js +6 -37
- package/dist/channels/secret.js +2 -5
- package/dist/channels/serve.d.ts +47 -44
- package/dist/channels/serve.js +165 -101
- package/dist/channels/slack/config-api.d.ts +4 -27
- package/dist/channels/slack/config-api.js +5 -34
- package/dist/channels/slack/context-buffer.d.ts +2 -3
- package/dist/channels/slack/context-buffer.js +5 -5
- package/dist/channels/slack/invoke-turn.d.ts +3 -1
- package/dist/channels/slack/invoke-turn.js +25 -34
- package/dist/channels/slack/manifest.js +1 -3
- package/dist/channels/slack/model.d.ts +1 -2
- package/dist/channels/slack/model.js +1 -1
- package/dist/channels/slack/onboard.d.ts +4 -3
- package/dist/channels/slack/onboard.js +8 -11
- package/dist/channels/slack/onboarding-state.d.ts +3 -5
- package/dist/channels/slack/onboarding-state.js +9 -6
- package/dist/channels/slack/parse.d.ts +4 -13
- package/dist/channels/slack/parse.js +7 -28
- package/dist/channels/slack/preview.d.ts +12 -2
- package/dist/channels/slack/preview.js +292 -269
- package/dist/channels/slack/reaction.d.ts +11 -3
- package/dist/channels/slack/reaction.js +19 -8
- package/dist/channels/slack/register-webhook.js +4 -7
- package/dist/channels/slack/scaffold/channel.ts +20 -13
- package/dist/channels/slack/scaffold/slack-send.ts +2 -2
- package/dist/channels/slack/setup-server.d.ts +1 -5
- package/dist/channels/slack/setup-server.js +1 -5
- package/dist/channels/slack/shared-api.d.ts +1 -4
- package/dist/channels/slack/shared-api.js +2 -13
- package/dist/channels/slack/slack-api.d.ts +2 -1
- package/dist/channels/slack/slack-api.js +6 -4
- package/dist/channels/slack/slack.d.ts +11 -14
- package/dist/channels/slack/slack.js +121 -122
- package/dist/channels/sse.d.ts +1 -1
- package/dist/channels/sse.js +1 -1
- package/dist/channels/telegram/context-buffer.d.ts +7 -22
- package/dist/channels/telegram/context-buffer.js +6 -23
- package/dist/channels/telegram/invoke-turn.d.ts +7 -7
- package/dist/channels/telegram/invoke-turn.js +24 -43
- package/dist/channels/telegram/parse.d.ts +24 -51
- package/dist/channels/telegram/parse.js +32 -84
- package/dist/channels/telegram/preview.d.ts +10 -1
- package/dist/channels/telegram/preview.js +79 -100
- package/dist/channels/telegram/register-webhook.js +5 -23
- package/dist/channels/telegram/scaffold/channel.ts +25 -18
- package/dist/channels/telegram/scaffold/telegram-send.ts +6 -4
- package/dist/channels/telegram/telegram-api.d.ts +4 -3
- package/dist/channels/telegram/telegram-api.js +9 -7
- package/dist/channels/telegram/telegram.d.ts +4 -18
- package/dist/channels/telegram/telegram.js +38 -94
- package/dist/channels/telegram/turn-store.d.ts +11 -14
- package/dist/channels/telegram/turn-store.js +8 -6
- package/dist/channels/wait-health.d.ts +4 -6
- package/dist/channels/wait-health.js +10 -14
- package/dist/cli/add-feishu.d.ts +2 -12
- package/dist/cli/add-feishu.js +24 -71
- package/dist/cli/add-slack.d.ts +5 -3
- package/dist/cli/add-slack.js +53 -76
- package/dist/cli/auth-view.d.ts +2 -16
- package/dist/cli/auth-view.js +2 -16
- package/dist/cli/commands/add.js +25 -40
- package/dist/cli/commands/chat.d.ts +0 -1
- package/dist/cli/commands/chat.js +5 -6
- package/dist/cli/commands/deploy/agentcore.js +60 -65
- package/dist/cli/commands/deploy/docker.js +50 -23
- package/dist/cli/commands/deploy/fly.js +23 -61
- package/dist/cli/commands/deploy/railway.js +23 -18
- package/dist/cli/commands/deploy/shared.d.ts +38 -57
- package/dist/cli/commands/deploy/shared.js +40 -59
- package/dist/cli/commands/deploy.d.ts +10 -27
- package/dist/cli/commands/deploy.js +38 -60
- package/dist/cli/commands/destroy.d.ts +4 -0
- package/dist/cli/commands/destroy.js +56 -0
- package/dist/cli/commands/dev.d.ts +2 -1
- package/dist/cli/commands/dev.js +20 -23
- package/dist/cli/commands/info.d.ts +0 -2
- package/dist/cli/commands/info.js +83 -49
- package/dist/cli/commands/init.d.ts +1 -2
- package/dist/cli/commands/init.js +16 -55
- package/dist/cli/commands/invoke.d.ts +0 -1
- package/dist/cli/commands/invoke.js +5 -7
- package/dist/cli/commands/login.d.ts +2 -1
- package/dist/cli/commands/login.js +33 -38
- package/dist/cli/commands/logs.js +3 -4
- package/dist/cli/commands/models.js +1 -5
- package/dist/cli/commands/routine-run.d.ts +6 -0
- package/dist/cli/commands/routine-run.js +47 -0
- package/dist/cli/commands/routine.d.ts +25 -0
- package/dist/cli/commands/routine.js +122 -0
- package/dist/cli/commands/start.d.ts +31 -3
- package/dist/cli/commands/start.js +131 -81
- package/dist/cli/commands/tool.js +26 -12
- package/dist/cli/fail.d.ts +13 -23
- package/dist/cli/fail.js +20 -23
- package/dist/cli/invoke-stream.d.ts +1 -8
- package/dist/cli/kernel.d.ts +6 -38
- package/dist/cli/kernel.js +14 -77
- package/dist/cli/models-view.d.ts +1 -9
- package/dist/cli/models-view.js +11 -22
- package/dist/cli/program.d.ts +5 -6
- package/dist/cli/program.js +120 -157
- package/dist/cli/serve.d.ts +57 -41
- package/dist/cli/serve.js +94 -121
- package/dist/cli/shared.d.ts +15 -61
- package/dist/cli/shared.js +55 -181
- package/dist/cli.js +1 -8
- package/dist/collect.d.ts +6 -0
- package/dist/collect.js +6 -0
- package/dist/core.d.ts +4 -1
- package/dist/core.js +9 -7
- package/dist/declared-secrets.d.ts +55 -0
- package/dist/declared-secrets.js +88 -0
- package/dist/deploy/agentcore/aws-cli.d.ts +91 -0
- package/dist/deploy/agentcore/aws-cli.js +64 -0
- package/dist/deploy/agentcore/destroy.d.ts +51 -0
- package/dist/deploy/agentcore/destroy.js +273 -0
- package/dist/deploy/agentcore/forwarder.js +28 -79
- package/dist/deploy/agentcore/logs.d.ts +2 -18
- package/dist/deploy/agentcore/logs.js +27 -51
- package/dist/deploy/agentcore/plan.d.ts +85 -105
- package/dist/deploy/agentcore/plan.js +152 -314
- package/dist/deploy/agentcore/run.d.ts +28 -28
- package/dist/deploy/agentcore/run.js +227 -306
- package/dist/deploy/agentcore/zip.d.ts +2 -15
- package/dist/deploy/agentcore/zip.js +3 -17
- package/dist/deploy/build-context.d.ts +21 -0
- package/dist/deploy/build-context.js +137 -0
- package/dist/deploy/channel-ingress.d.ts +26 -39
- package/dist/deploy/channel-ingress.js +57 -28
- package/dist/deploy/container.d.ts +20 -34
- package/dist/deploy/container.js +49 -100
- package/dist/deploy/docker/plan.d.ts +12 -11
- package/dist/deploy/docker/plan.js +43 -24
- package/dist/deploy/docker/run.d.ts +22 -25
- package/dist/deploy/docker/run.js +39 -37
- package/dist/deploy/fly/plan.d.ts +15 -44
- package/dist/deploy/fly/plan.js +41 -73
- package/dist/deploy/fly/run.d.ts +16 -51
- package/dist/deploy/fly/run.js +42 -95
- package/dist/deploy/hosts.d.ts +24 -3
- package/dist/deploy/hosts.js +24 -3
- package/dist/deploy/preflight.d.ts +47 -41
- package/dist/deploy/preflight.js +282 -383
- package/dist/deploy/railway/plan.d.ts +16 -42
- package/dist/deploy/railway/plan.js +42 -60
- package/dist/deploy/railway/run.d.ts +30 -68
- package/dist/deploy/railway/run.js +86 -100
- package/dist/deploy/registration-gate.d.ts +6 -13
- package/dist/deploy/residency.d.ts +37 -0
- package/dist/deploy/residency.js +24 -0
- package/dist/deploy/runner.d.ts +10 -9
- package/dist/deploy/runner.js +7 -18
- package/dist/deploy/secrets.d.ts +40 -41
- package/dist/deploy/secrets.js +117 -103
- package/dist/deploy/workspace.d.ts +50 -0
- package/dist/deploy/workspace.js +197 -0
- package/dist/dev-supervisor.d.ts +1 -9
- package/dist/dev-supervisor.js +21 -64
- package/dist/effect-port.d.ts +50 -0
- package/dist/effect-port.js +77 -0
- package/dist/engines/pi/agent-session-factory.d.ts +50 -69
- package/dist/engines/pi/agent-session-factory.js +249 -146
- package/dist/engines/pi/auth.d.ts +15 -11
- package/dist/engines/pi/auth.js +96 -99
- package/dist/engines/pi/chat.d.ts +1 -5
- package/dist/engines/pi/chat.js +2 -14
- package/dist/engines/pi/config.d.ts +53 -51
- package/dist/engines/pi/config.js +83 -92
- package/dist/engines/pi/create.d.ts +60 -75
- package/dist/engines/pi/create.js +108 -141
- package/dist/engines/pi/definition.d.ts +14 -58
- package/dist/engines/pi/definition.js +21 -85
- package/dist/engines/pi/invoke-session.d.ts +5 -25
- package/dist/engines/pi/invoke-session.js +233 -244
- package/dist/engines/pi/login.d.ts +95 -35
- package/dist/engines/pi/login.js +259 -107
- package/dist/engines/pi/machine.d.ts +31 -0
- package/dist/engines/pi/machine.js +106 -0
- package/dist/engines/pi/models.d.ts +70 -89
- package/dist/engines/pi/models.js +188 -81
- package/dist/engines/pi/open.d.ts +65 -53
- package/dist/engines/pi/open.js +129 -69
- package/dist/engines/pi/report.d.ts +3 -12
- package/dist/engines/pi/report.js +2 -16
- package/dist/engines/pi/search-tools.d.ts +1 -9
- package/dist/engines/pi/search-tools.js +26 -57
- package/dist/engines/pi/service.d.ts +2 -16
- package/dist/engines/pi/service.js +2 -16
- package/dist/engines/pi/session-builder.d.ts +5 -9
- package/dist/engines/pi/session-builder.js +44 -82
- package/dist/engines/pi/session-control.js +286 -269
- package/dist/engines/pi/session-effects.d.ts +14 -0
- package/dist/engines/pi/session-effects.js +30 -0
- package/dist/engines/pi/session-inheritance.d.ts +9 -18
- package/dist/engines/pi/session-inheritance.js +103 -160
- package/dist/engines/pi/session-markers.d.ts +31 -30
- package/dist/engines/pi/session-markers.js +26 -25
- package/dist/engines/pi/session-settings.d.ts +6 -22
- package/dist/engines/pi/session-settings.js +10 -21
- package/dist/engines/pi/session-store.d.ts +26 -92
- package/dist/engines/pi/session-store.js +96 -310
- package/dist/engines/pi/tool-context.d.ts +17 -45
- package/dist/engines/pi/tool-context.js +7 -26
- package/dist/engines/pi/tool.d.ts +45 -36
- package/dist/engines/pi/tool.js +43 -57
- package/dist/engines/pi/turn-kit.d.ts +14 -52
- package/dist/engines/pi/turn-kit.js +18 -67
- package/dist/engines/pi/wake-tool.d.ts +2 -10
- package/dist/engines/pi/wake-tool.js +7 -27
- package/dist/env.d.ts +27 -28
- package/dist/env.js +45 -50
- package/dist/index.js +0 -2
- package/dist/lark.d.ts +1 -2
- package/dist/lark.js +1 -2
- package/dist/loader.d.ts +14 -55
- package/dist/loader.js +24 -43
- package/dist/log.d.ts +2 -12
- package/dist/log.js +3 -24
- package/dist/node.d.ts +2 -15
- package/dist/node.js +2 -18
- package/dist/observe.d.ts +2 -8
- package/dist/open-url.d.ts +1 -1
- package/dist/open-url.js +1 -1
- package/dist/paths.d.ts +70 -134
- package/dist/paths.js +165 -232
- package/dist/pi.d.ts +4 -6
- package/dist/pi.js +3 -3
- package/dist/proxy.d.ts +16 -6
- package/dist/proxy.js +52 -16
- package/dist/runtime.d.ts +18 -12
- package/dist/runtime.js +18 -11
- package/dist/scaffold/add-channel.d.ts +14 -31
- package/dist/scaffold/add-channel.js +48 -112
- package/dist/scaffold/init.d.ts +9 -31
- package/dist/scaffold/init.js +45 -138
- package/dist/scaffold/templates/env.example +6 -4
- package/dist/scaffold/templates/fastagent.config.ts +36 -0
- package/dist/scaffold/templates.d.ts +3 -3
- package/dist/scaffold/templates.js +5 -8
- package/dist/scaffold/vendor-skill.d.ts +2 -5
- package/dist/scaffold/vendor-skill.js +19 -29
- package/dist/schedule/cron.d.ts +9 -5
- package/dist/schedule/cron.js +9 -11
- package/dist/schedule/discover.d.ts +12 -12
- package/dist/schedule/discover.js +43 -34
- package/dist/schedule/routine.d.ts +43 -0
- package/dist/schedule/routine.js +32 -0
- package/dist/schedule/run.d.ts +55 -0
- package/dist/schedule/run.js +146 -0
- package/dist/schedule/scheduler.d.ts +44 -55
- package/dist/schedule/scheduler.js +291 -265
- package/dist/schedule/state.d.ts +105 -10
- package/dist/schedule/state.js +293 -19
- package/dist/schedule/wake-alarm.d.ts +10 -23
- package/dist/schedule/wake-alarm.js +91 -121
- package/dist/schedule/wakeups.d.ts +23 -39
- package/dist/schedule/wakeups.js +33 -75
- package/dist/secrets-gate.d.ts +32 -0
- package/dist/secrets-gate.js +33 -0
- package/dist/service.d.ts +137 -98
- package/dist/service.js +250 -252
- package/dist/session-remote.d.ts +33 -44
- package/dist/session-remote.js +203 -192
- package/dist/session.d.ts +166 -229
- package/dist/session.js +33 -55
- package/dist/tunnel.d.ts +8 -36
- package/dist/tunnel.js +24 -65
- package/dist/version.d.ts +2 -2
- package/dist/version.js +2 -2
- package/package.json +10 -16
- package/dist/channels/agentcore-state.d.ts +0 -77
- package/dist/channels/agentcore-state.js +0 -276
- package/dist/channels/github/github.d.ts +0 -31
- package/dist/channels/github/github.js +0 -89
- package/dist/channels/github/scaffold/channel.ts +0 -25
- package/dist/cli/commands/attach.d.ts +0 -108
- package/dist/cli/commands/attach.js +0 -637
- package/dist/cli/commands/fire.d.ts +0 -7
- package/dist/cli/commands/fire.js +0 -38
- package/dist/cli/commands/schedule.d.ts +0 -12
- package/dist/cli/commands/schedule.js +0 -87
- package/dist/github.d.ts +0 -3
- package/dist/github.js +0 -2
- package/dist/scaffold/templates/fastagent.config.mjs +0 -23
- package/dist/schedule/audit.d.ts +0 -18
- package/dist/schedule/audit.js +0 -57
- package/dist/schedule/schedule.d.ts +0 -35
- package/dist/schedule/schedule.js +0 -23
|
@@ -1,52 +1,61 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
* named from its filename. This is the FILE producer of scheduled invocations (the author's, declarative,
|
|
5
|
-
* git-tracked, deploy-guaranteed); the agent's `wake` tool is the second producer.
|
|
2
|
+
* Routine discovery: an agent declares its named units of work by dropping files in `routines/`, mirroring
|
|
3
|
+
* `tools/` and `channels/`.
|
|
6
4
|
*/
|
|
7
5
|
import { join } from "node:path";
|
|
8
|
-
import { loadModuleDir
|
|
6
|
+
import { loadModuleDir } from "../loader.js";
|
|
7
|
+
import { readSecretDeclaration } from "../declared-secrets.js";
|
|
9
8
|
import { assertInsideAgentDir } from "../paths.js";
|
|
10
9
|
import { cronError } from "./cron.js";
|
|
11
|
-
|
|
12
|
-
* (deploy pre-flight's time-trigger detection; `fastagent info` uses {@link loadSchedules} instead,
|
|
13
|
-
* since it also reports broken files and next instants). */
|
|
14
|
-
export async function discoverScheduleFiles(dir) {
|
|
15
|
-
await assertInsideAgentDir(dir, "schedules");
|
|
16
|
-
const entries = await moduleInventory(join(dir, "schedules"));
|
|
17
|
-
return entries.map((entry) => entry.name);
|
|
18
|
-
}
|
|
10
|
+
import { isSafeScheduleName } from "./state.js";
|
|
19
11
|
/**
|
|
20
|
-
* Discover
|
|
21
|
-
*
|
|
22
|
-
* an invalid cron/tz — is ISOLATED into `failures` (skipped + reported, never a crash-loop, G2), the same
|
|
23
|
-
* way `loadTools`/`loadChannels` isolate theirs. A duplicate name (foo.ts + foo.js) keeps the first and
|
|
24
|
-
* reports the rest.
|
|
12
|
+
* Discover routines in `<dir>/routines/`: each file default-exports a `defineRoutine({...})`, named from its
|
|
13
|
+
* filename.
|
|
25
14
|
*/
|
|
26
|
-
export async function
|
|
27
|
-
await assertInsideAgentDir(dir, "
|
|
28
|
-
const { modules, failures } = await loadModuleDir(join(dir, "
|
|
15
|
+
export async function loadRoutines(dir) {
|
|
16
|
+
await assertInsideAgentDir(dir, "routines");
|
|
17
|
+
const { modules, failures } = await loadModuleDir(join(dir, "routines"));
|
|
29
18
|
const byName = new Map();
|
|
19
|
+
const secrets = new Map();
|
|
30
20
|
for (const { name, label, file, mod } of modules) {
|
|
31
21
|
try {
|
|
32
|
-
const
|
|
33
|
-
if (!
|
|
34
|
-
throw new Error(`${label} must default-export
|
|
22
|
+
const r = mod.default;
|
|
23
|
+
if (!r || typeof r.prompt !== "string") {
|
|
24
|
+
throw new Error(`${label} must default-export defineRoutine({ prompt, cron? })`);
|
|
25
|
+
}
|
|
26
|
+
const declaration = readSecretDeclaration(r, label);
|
|
27
|
+
if (declaration.error !== undefined)
|
|
28
|
+
throw new Error(declaration.error);
|
|
29
|
+
// NO CRON IS NOT AN ERROR: the routine is then reachable by name only (`POST /run`, the CLI, a scheduler
|
|
30
|
+
// that calls the route). A `tz` without one is, though — it would read as a time this routine does not have.
|
|
31
|
+
if (r.cron !== undefined && typeof r.cron !== "string")
|
|
32
|
+
throw new Error(`${label}: "cron" must be a string`);
|
|
33
|
+
if (r.cron === undefined && r.tz !== undefined) {
|
|
34
|
+
throw new Error(`${label}: "tz" means nothing without "cron" — remove it, or give this routine a cron`);
|
|
35
|
+
}
|
|
36
|
+
if (r.cron !== undefined) {
|
|
37
|
+
const err = cronError(r.cron, r.tz);
|
|
38
|
+
if (err)
|
|
39
|
+
throw new Error(`${label}: invalid cron/tz — ${err}`);
|
|
40
|
+
}
|
|
41
|
+
// The name becomes a path segment (the fired-slot claims live under `claims/<name>/`), so anything that could
|
|
42
|
+
// leave that directory is refused here — where the author sees which file is wrong — rather than deeper.
|
|
43
|
+
if (!isSafeScheduleName(name)) {
|
|
44
|
+
throw new Error(`${label}: a routine name cannot be ".", ".." or contain a path separator`);
|
|
35
45
|
}
|
|
36
|
-
const err = cronError(s.cron, s.tz);
|
|
37
|
-
if (err)
|
|
38
|
-
throw new Error(`${label}: invalid cron/tz — ${err}`);
|
|
39
|
-
// "wake" is reserved: the run audit records self-scheduled wake-ups under that name, so a schedule
|
|
40
|
-
// named wake would make `schedule history wake` an unreadable mix of two different things.
|
|
41
|
-
if (name === "wake")
|
|
42
|
-
throw new Error(`${label}: "wake" is a reserved schedule name (the self-scheduling audit uses it)`);
|
|
43
46
|
if (byName.has(name))
|
|
44
|
-
throw new Error(`${label}: duplicate
|
|
45
|
-
byName.set(name, {
|
|
47
|
+
throw new Error(`${label}: duplicate routine name "${name}" — kept the first`);
|
|
48
|
+
byName.set(name, {
|
|
49
|
+
name,
|
|
50
|
+
prompt: r.prompt,
|
|
51
|
+
...(r.cron !== undefined ? { cron: r.cron } : {}),
|
|
52
|
+
...(r.tz !== undefined ? { tz: r.tz } : {}),
|
|
53
|
+
});
|
|
54
|
+
secrets.set(name, declaration.secrets);
|
|
46
55
|
}
|
|
47
56
|
catch (error) {
|
|
48
57
|
failures.push({ label, file, message: error.message });
|
|
49
58
|
}
|
|
50
59
|
}
|
|
51
|
-
return {
|
|
60
|
+
return { routines: [...byName.values()], secrets, failures };
|
|
52
61
|
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WHICH conversation a routine's turns belong to: all of them, so a routine shares one continuing conversation —
|
|
3
|
+
* and without depending on engine session storage. The ONE spelling of it: the clock invokes through it,
|
|
4
|
+
* `fastagent routine run` reproduces a run with it, `POST /run` reports it as where to look, and
|
|
5
|
+
* `routine history` prints the command that opens it.
|
|
6
|
+
*/
|
|
7
|
+
export declare function routineSession(name: string): string;
|
|
8
|
+
/** A named unit of work the definition owns. With a `cron`, a clock fires it; without one, only its name reaches it. */
|
|
9
|
+
export interface Routine {
|
|
10
|
+
/**
|
|
11
|
+
* 5-field cron expression (`minute hour day-of-month month day-of-week`), or absent.
|
|
12
|
+
*
|
|
13
|
+
* ABSENT IS A REAL CHOICE, not an omission: the routine is then reachable only by name — `POST /run`, the CLI, a
|
|
14
|
+
* platform scheduler that calls the route. A time is one way to ask for work, not what the work is.
|
|
15
|
+
*/
|
|
16
|
+
cron?: string;
|
|
17
|
+
/** IANA timezone for the cron (default "UTC"). Meaningless without `cron`. */
|
|
18
|
+
tz?: string;
|
|
19
|
+
/** The turn's text = the routine's instruction. */
|
|
20
|
+
prompt: string;
|
|
21
|
+
/** Env vars this routine declared — carried by `deploy`, asserted before the clock starts. */
|
|
22
|
+
secrets?: readonly string[];
|
|
23
|
+
}
|
|
24
|
+
/** What an author writes. `prompt` may be built FROM the declared secrets, which is the only reason a
|
|
25
|
+
* routine file ever read the environment (a channel id, a target inbox). */
|
|
26
|
+
export interface DefineRoutineOptions<S extends readonly string[]> {
|
|
27
|
+
cron?: string;
|
|
28
|
+
tz?: string;
|
|
29
|
+
prompt: string | ((secrets: Record<S[number], string>) => string);
|
|
30
|
+
/**
|
|
31
|
+
* Env vars this routine needs (`["SLACK_DIGEST_CHANNEL"]`), typed into the `prompt` builder.
|
|
32
|
+
* `deploy` carries them and the clock refuses to start while one is unset, naming this file
|
|
33
|
+
* (see src/declared-secrets.ts) — the guarantees a bare `process.env` read cannot have.
|
|
34
|
+
*/
|
|
35
|
+
secrets?: S;
|
|
36
|
+
}
|
|
37
|
+
/** Type + completion for a routine file, and the ONE place a prompt builder is resolved (at load:
|
|
38
|
+
* what the loader stores is always a string, so every consumer sees one shape). */
|
|
39
|
+
export declare function defineRoutine<const S extends readonly string[] = readonly []>(routine: DefineRoutineOptions<S>): Routine;
|
|
40
|
+
/** A loaded routine: its authored fields plus the name derived from its filename (authoritative). */
|
|
41
|
+
export interface LoadedRoutine extends Routine {
|
|
42
|
+
name: string;
|
|
43
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Routine authoring: `defineRoutine`.
|
|
3
|
+
*
|
|
4
|
+
* A ROUTINE IS THE UNIT OF WORK, and the only named one in the system: a prompt the definition owns, addressed by
|
|
5
|
+
* name. `cron` is a FIELD of it, not a second concept — with one, a clock fires it; without one, it is reached by
|
|
6
|
+
* name alone (`POST /run`).
|
|
7
|
+
*
|
|
8
|
+
* WHAT IT IS NOT: a wake-up. The agent can schedule work for itself too (the `wake` tool, schedule/wakeups.ts), and
|
|
9
|
+
* conceptually that is the same idea — work to be done later. Every operational difference between them follows
|
|
10
|
+
* from ONE root: a routine is written in the DEFINITION (versioned, reviewed, shipped with the image, addressable
|
|
11
|
+
* by name) and a wake-up is written into the STATE by a running agent (minted id, cancellable, aimed back at the
|
|
12
|
+
* conversation it came from, erased by the next release if it were a definition file). Code and data.
|
|
13
|
+
*
|
|
14
|
+
* A `prompt` may be a function of the file's declared `secrets` — the only reason such a file ever read the
|
|
15
|
+
* environment (src/declared-secrets.ts).
|
|
16
|
+
*/
|
|
17
|
+
import { secretValues } from "../declared-secrets.js";
|
|
18
|
+
/**
|
|
19
|
+
* WHICH conversation a routine's turns belong to: all of them, so a routine shares one continuing conversation —
|
|
20
|
+
* and without depending on engine session storage. The ONE spelling of it: the clock invokes through it,
|
|
21
|
+
* `fastagent routine run` reproduces a run with it, `POST /run` reports it as where to look, and
|
|
22
|
+
* `routine history` prints the command that opens it.
|
|
23
|
+
*/
|
|
24
|
+
export function routineSession(name) {
|
|
25
|
+
return `routine:${name}`;
|
|
26
|
+
}
|
|
27
|
+
/** Type + completion for a routine file, and the ONE place a prompt builder is resolved (at load:
|
|
28
|
+
* what the loader stores is always a string, so every consumer sees one shape). */
|
|
29
|
+
export function defineRoutine(routine) {
|
|
30
|
+
const { prompt, ...rest } = routine;
|
|
31
|
+
return { ...rest, prompt: typeof prompt === "function" ? prompt(secretValues(routine.secrets)) : prompt };
|
|
32
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { Agent } from "../agent.ts";
|
|
2
|
+
import { type LoadedRoutine } from "./routine.ts";
|
|
3
|
+
/**
|
|
4
|
+
* The 404 for a name this deployment does not declare — drift: a caller outliving the thing it calls. The names are
|
|
5
|
+
* listed so an operator can see whether it is a typo or a stale job without shelling in. The caller's name is CLIPPED
|
|
6
|
+
* before it goes back out: listing this deployment's own names is deliberate, quoting an unauthenticated caller's
|
|
7
|
+
* 4 KiB of body is not (`refuseNonJsonBody` states the same rule for the same table).
|
|
8
|
+
*/
|
|
9
|
+
export declare function unknownRoutine(name: string, routines: readonly LoadedRoutine[]): Response;
|
|
10
|
+
/** The reply shape both doors answer with — one for the route, one for a host that has no route (AgentCore). */
|
|
11
|
+
export interface RoutineRunReply {
|
|
12
|
+
name: string;
|
|
13
|
+
/** WHERE TO LOOK: the turn's output lives in this session's journal (`fastagent routine history`, `/control/*`). */
|
|
14
|
+
session: string;
|
|
15
|
+
ran: boolean;
|
|
16
|
+
/** Present when `ran: false` — today, the previous turn still holding this routine's one session. */
|
|
17
|
+
reason?: string;
|
|
18
|
+
/** Present when the turn RAN and did not finish. */
|
|
19
|
+
failed?: string;
|
|
20
|
+
ms: number;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Run one routine and say what happened. THE one implementation, because there are two doors and only one
|
|
24
|
+
* contract: `POST /run` on any host that publishes routes, and AgentCore's IAM-gated `routine-run` envelope on the
|
|
25
|
+
* host that publishes none. A second copy is how the two would come to answer differently.
|
|
26
|
+
*/
|
|
27
|
+
export declare function runRoutineByName(agent: Agent, routine: LoadedRoutine): Promise<RoutineRunReply>;
|
|
28
|
+
/**
|
|
29
|
+
* Build the handler for `GET /routines` — what a caller may ask for by name.
|
|
30
|
+
*
|
|
31
|
+
* DISCOVERY, because the alternative was a 404. The names were already public: `POST /run` lists them when a caller
|
|
32
|
+
* gets one wrong, which is deliberate (an operator has to tell a typo from a stale caller) — so the only thing this
|
|
33
|
+
* adds is not having to guess wrong first. "Expose a declared prompt as an API" is half a sentence without it.
|
|
34
|
+
*
|
|
35
|
+
* NAMES AND SCHEDULES, NEVER PROMPTS. What a routine SAYS is the definition's content; handing it to an
|
|
36
|
+
* unauthenticated caller would publish the agent's behaviour, which is the one thing keeping the prompt out of the
|
|
37
|
+
* request body was for. `cron` is here because it answers "will this run on its own, or is my clock the only one?",
|
|
38
|
+
* which is a caller's question, not the definition's secret.
|
|
39
|
+
*
|
|
40
|
+
* Mounted exactly where `POST /run` is: it describes that route, so listing names a caller cannot use would be a
|
|
41
|
+
* catalogue of nothing.
|
|
42
|
+
*/
|
|
43
|
+
export declare function createRoutineListHandler(options: {
|
|
44
|
+
routines: readonly LoadedRoutine[];
|
|
45
|
+
}): ((req: Request) => Promise<Response>) | undefined;
|
|
46
|
+
/**
|
|
47
|
+
* Build the handler for `POST /run`, bound to what this serve loaded.
|
|
48
|
+
*
|
|
49
|
+
* `undefined` when the definition declares nothing runnable: a route that can only ever answer 404 is not a route,
|
|
50
|
+
* and the startup report names what is actually mounted.
|
|
51
|
+
*/
|
|
52
|
+
export declare function createRunHandler(options: {
|
|
53
|
+
agent: Agent;
|
|
54
|
+
routines: readonly LoadedRoutine[];
|
|
55
|
+
}): ((req: Request) => Promise<Response>) | undefined;
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `POST /run` — **an API**: run a unit of work this definition declares, by name.
|
|
3
|
+
*
|
|
4
|
+
* NOT A TIME TRIGGER, and the difference is the whole design. Occurrence semantics — a slot, a claim, a fire
|
|
5
|
+
* history, an overlap policy — exist exactly where fastagent OWNS the clock: the resident loop in `dev`/`start`,
|
|
6
|
+
* and AgentCore, where `deploy` writes the EventBridge rule, injects the instant and relays it through an ingress
|
|
7
|
+
* secret (channels/agentcore.ts). Both of those know which occurrence a run is for because they produced it.
|
|
8
|
+
*
|
|
9
|
+
* This route is the other case: somebody else's clock, CI job, button or script. It cannot be told which occurrence
|
|
10
|
+
* it means, and pretending otherwise was measurably worse than admitting it. The attempt cost four rounds — an
|
|
11
|
+
* instant dated ahead poisoned the claim gate permanently, one off the grid minted a claim no occurrence would ever
|
|
12
|
+
* match, an old one replayed history a turn at a time, and the grid arithmetic all three needed was ~6ms of
|
|
13
|
+
* synchronous CPU per request on an anonymous route. A platform cron drifting a few minutes then lost an occurrence
|
|
14
|
+
* outright (Railway documents "a few minutes" of variance; measured: 3 fires, 2 turns).
|
|
15
|
+
*
|
|
16
|
+
* So the contract is the one an API has:
|
|
17
|
+
*
|
|
18
|
+
* - the body names WHAT to run and nothing else — the prompt stays in the definition, which is the whole reason
|
|
19
|
+
* this exists beside `POST /invoke`, whose caller brings its own text and therefore its own behaviour
|
|
20
|
+
* - the reply says what happened and WHERE TO LOOK (the session the turn ran in). Retry policy is the caller's,
|
|
21
|
+
* because only the caller knows whether its work tolerates running twice
|
|
22
|
+
* - it is exactly as exposed as `POST /invoke`: unauthenticated, full tool authority, `http.invoke: false` and a
|
|
23
|
+
* gateway in front are the answers. The framework authenticates nothing (docs/design/session-control.md §14)
|
|
24
|
+
*
|
|
25
|
+
* AND NO IDEMPOTENCY KEY. A key would deduplicate the CALL, not the WORK: a turn that sent one message and then died
|
|
26
|
+
* would answer a keyed retry with "already ran" — safety exactly where it is absent, which is the same reason a failed
|
|
27
|
+
* fire is not retried (schedule/scheduler.ts). What makes a retry safe is idempotent WORK, which only the author can
|
|
28
|
+
* arrange.
|
|
29
|
+
*
|
|
30
|
+
* NAME IN THE BODY, not the path. A declared name is a filename (`每日简报`, `my schedule` are both legal), and a
|
|
31
|
+
* path segment would mean percent-encoding it — the cost the control plane pays for session ids and this route has
|
|
32
|
+
* no reason to. It also keeps the shape of `POST /invoke`, whose scope rides in the body for the same reason.
|
|
33
|
+
*/
|
|
34
|
+
import * as Effect from "effect/Effect";
|
|
35
|
+
import { readBodyCapped, refuseNonJsonBody } from "../channels/body.js";
|
|
36
|
+
import { text } from "../channels/respond.js";
|
|
37
|
+
import { routineSession } from "./routine.js";
|
|
38
|
+
import { runTurn } from "./scheduler.js";
|
|
39
|
+
/** A trigger body is a name; the cap only has to admit that. */
|
|
40
|
+
const MAX_RUN_BODY_BYTES = 4 * 1024;
|
|
41
|
+
/**
|
|
42
|
+
* How much of a caller's `name` a 404 will quote back — the ONE thing this route echoes, because telling a typo
|
|
43
|
+
* from a stale caller needs the name and nothing else does. Long enough to recognise one, short enough not to be a
|
|
44
|
+
* page.
|
|
45
|
+
*/
|
|
46
|
+
const MAX_ECHOED_NAME = 64;
|
|
47
|
+
/**
|
|
48
|
+
* The 404 for a name this deployment does not declare — drift: a caller outliving the thing it calls. The names are
|
|
49
|
+
* listed so an operator can see whether it is a typo or a stale job without shelling in. The caller's name is CLIPPED
|
|
50
|
+
* before it goes back out: listing this deployment's own names is deliberate, quoting an unauthenticated caller's
|
|
51
|
+
* 4 KiB of body is not (`refuseNonJsonBody` states the same rule for the same table).
|
|
52
|
+
*/
|
|
53
|
+
export function unknownRoutine(name, routines) {
|
|
54
|
+
return text(`no declared work named "${name.slice(0, MAX_ECHOED_NAME)}" (this deployment has: ${routines
|
|
55
|
+
.map((r) => r.name)
|
|
56
|
+
.join(", ")})\n`, 404);
|
|
57
|
+
}
|
|
58
|
+
/** The Effect boundary, in one place: the turn runs, and its outcome is what the caller reads. */
|
|
59
|
+
const runOnce = (agent, routine) => Effect.runPromise(runTurn(agent, routine.name, routineSession(routine.name), routine.prompt));
|
|
60
|
+
/**
|
|
61
|
+
* Run one routine and say what happened. THE one implementation, because there are two doors and only one
|
|
62
|
+
* contract: `POST /run` on any host that publishes routes, and AgentCore's IAM-gated `routine-run` envelope on the
|
|
63
|
+
* host that publishes none. A second copy is how the two would come to answer differently.
|
|
64
|
+
*/
|
|
65
|
+
export async function runRoutineByName(agent, routine) {
|
|
66
|
+
const session = routineSession(routine.name);
|
|
67
|
+
const { busy, failed, ms } = await runOnce(agent, routine);
|
|
68
|
+
// BUSY IS NOT A FAILURE and it is the one "did not run" this has: a declared unit of work has ONE session, so a
|
|
69
|
+
// call arriving while the previous turn holds it is refused by that session. Reported as itself, with what the
|
|
70
|
+
// caller needs in order to decide — try later — rather than as an error it would retry blindly.
|
|
71
|
+
if (busy) {
|
|
72
|
+
return {
|
|
73
|
+
name: routine.name,
|
|
74
|
+
session,
|
|
75
|
+
ran: false,
|
|
76
|
+
reason: `the previous turn of "${routine.name}" is still running`,
|
|
77
|
+
ms,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
return { name: routine.name, session, ran: true, ...(failed !== undefined ? { failed } : {}), ms };
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Build the handler for `GET /routines` — what a caller may ask for by name.
|
|
84
|
+
*
|
|
85
|
+
* DISCOVERY, because the alternative was a 404. The names were already public: `POST /run` lists them when a caller
|
|
86
|
+
* gets one wrong, which is deliberate (an operator has to tell a typo from a stale caller) — so the only thing this
|
|
87
|
+
* adds is not having to guess wrong first. "Expose a declared prompt as an API" is half a sentence without it.
|
|
88
|
+
*
|
|
89
|
+
* NAMES AND SCHEDULES, NEVER PROMPTS. What a routine SAYS is the definition's content; handing it to an
|
|
90
|
+
* unauthenticated caller would publish the agent's behaviour, which is the one thing keeping the prompt out of the
|
|
91
|
+
* request body was for. `cron` is here because it answers "will this run on its own, or is my clock the only one?",
|
|
92
|
+
* which is a caller's question, not the definition's secret.
|
|
93
|
+
*
|
|
94
|
+
* Mounted exactly where `POST /run` is: it describes that route, so listing names a caller cannot use would be a
|
|
95
|
+
* catalogue of nothing.
|
|
96
|
+
*/
|
|
97
|
+
export function createRoutineListHandler(options) {
|
|
98
|
+
const { routines } = options;
|
|
99
|
+
if (routines.length === 0)
|
|
100
|
+
return undefined;
|
|
101
|
+
const body = routines.map((r) => ({
|
|
102
|
+
name: r.name,
|
|
103
|
+
...(r.cron !== undefined ? { cron: r.cron } : {}),
|
|
104
|
+
...(r.tz !== undefined ? { tz: r.tz } : {}),
|
|
105
|
+
}));
|
|
106
|
+
// NO METHOD CHECK. The router dispatches `GET /routines` here and nothing else, so the only thing such a check
|
|
107
|
+
// could reach is HEAD — which `channels/serve.ts` deliberately answers with the GET route, the same way
|
|
108
|
+
// `GET /health` is HEAD-probeable. Checking would break that and buy nothing.
|
|
109
|
+
return async () => Response.json(body);
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Build the handler for `POST /run`, bound to what this serve loaded.
|
|
113
|
+
*
|
|
114
|
+
* `undefined` when the definition declares nothing runnable: a route that can only ever answer 404 is not a route,
|
|
115
|
+
* and the startup report names what is actually mounted.
|
|
116
|
+
*/
|
|
117
|
+
export function createRunHandler(options) {
|
|
118
|
+
const { agent, routines } = options;
|
|
119
|
+
if (routines.length === 0)
|
|
120
|
+
return undefined;
|
|
121
|
+
return async (req) => {
|
|
122
|
+
if (req.method !== "POST")
|
|
123
|
+
return text("POST only\n", 405);
|
|
124
|
+
const wrongType = refuseNonJsonBody(req);
|
|
125
|
+
if (wrongType)
|
|
126
|
+
return wrongType;
|
|
127
|
+
const body = await readBodyCapped(req, MAX_RUN_BODY_BYTES);
|
|
128
|
+
if ("tooLarge" in body)
|
|
129
|
+
return text("body too large\n", 413);
|
|
130
|
+
let payload;
|
|
131
|
+
try {
|
|
132
|
+
payload = JSON.parse(body.text);
|
|
133
|
+
}
|
|
134
|
+
catch {
|
|
135
|
+
return text("invalid json\n", 400);
|
|
136
|
+
}
|
|
137
|
+
const { name } = (payload ?? {});
|
|
138
|
+
if (typeof name !== "string" || name === "") {
|
|
139
|
+
return text('need { "name": string } — e.g. {"name":"daily"}\n', 400);
|
|
140
|
+
}
|
|
141
|
+
const routine = routines.find((r) => r.name === name);
|
|
142
|
+
if (!routine)
|
|
143
|
+
return unknownRoutine(name, routines);
|
|
144
|
+
return Response.json(await runRoutineByName(agent, routine));
|
|
145
|
+
};
|
|
146
|
+
}
|
|
@@ -1,75 +1,64 @@
|
|
|
1
|
-
|
|
2
|
-
* The scheduler: a time-trigger that fires the agent on each schedule's cron. SINGLE-PROCESS (like all
|
|
3
|
-
* fastagent state today) — a deployment with schedules must keep one machine running, since cron has no
|
|
4
|
-
* external wake-up (`deploy` enforces that). Started/stopped by the serve path (dev/start).
|
|
5
|
-
*
|
|
6
|
-
* Fire model:
|
|
7
|
-
* - **stable session per schedule** (`schedule:<name>`), so a schedule's turns share one continuing
|
|
8
|
-
* conversation persisted by the core session store; the scheduler is ZERO-touch on session storage.
|
|
9
|
-
* - **output is the agent's tools' job** — the scheduler only fires and logs the outcome.
|
|
10
|
-
* - **durability = catch up ONCE** (`state.ts`): on start, if the next instant after the last fire is
|
|
11
|
-
* already past (the process was down across it), fire once and advance — not once per missed slot.
|
|
12
|
-
* lastFired is claimed BEFORE the invoke (at-most-once per slot: a crash mid-turn won't re-fire it;
|
|
13
|
-
* "a digest late once" beats "twice"). Strict at-least-once (a per-turn WAL) is a later tier.
|
|
14
|
-
*/
|
|
1
|
+
import * as Effect from "effect/Effect";
|
|
15
2
|
import { type Agent } from "../agent.ts";
|
|
16
|
-
import
|
|
17
|
-
|
|
18
|
-
* per-chat session. Derived at RUNTIME from the name (never an authored field). */
|
|
19
|
-
export declare function scheduleSession(name: string): string;
|
|
3
|
+
import { PortFailure } from "../effect-port.ts";
|
|
4
|
+
import { type LoadedRoutine } from "./routine.ts";
|
|
20
5
|
export interface Scheduler {
|
|
21
|
-
/** Arm
|
|
6
|
+
/** Arm routines, account for a fire the previous run was killed in, and catch up overdue work once. */
|
|
22
7
|
start(): void;
|
|
23
|
-
/**
|
|
24
|
-
* interrupted turn is not re-fired — lastFired was already claimed). A catch-up fire in flight has NO
|
|
25
|
-
* timer entry to clear either (the overdue branch fires directly, without arming), so `stop()` simply
|
|
26
|
-
* lets it run out or be cut by process exit — same non-drain, and its claim is already persisted. */
|
|
8
|
+
/** Cancel pending waits. */
|
|
27
9
|
stop(): void;
|
|
28
10
|
}
|
|
29
11
|
export interface SchedulerOptions {
|
|
30
12
|
agent: Agent;
|
|
31
13
|
stateRoot: string;
|
|
32
|
-
|
|
33
|
-
|
|
14
|
+
/** Every routine this serve loaded. Only the ones WITH a cron are armed — the rest are reached by name. */
|
|
15
|
+
routines: LoadedRoutine[];
|
|
16
|
+
/** Override wall-clock dates; elapsed time and waits use the Effect clock. */
|
|
34
17
|
now?: () => Date;
|
|
35
|
-
/** External
|
|
36
|
-
* scheduler through the serving surface ({@link fireScheduleOnce} with a `slot`), so `start()`
|
|
37
|
-
* arms NO cron timers and does NO boot catch-up — delivery, including for instants that passed
|
|
38
|
-
* while this process was down, is the external clock's job; the adapter's slot-idempotent claim
|
|
39
|
-
* is the duplicate guard. The wake-up poll still runs (degraded: it only fires while the
|
|
40
|
-
* process happens to be awake — the deploy path warns about this). */
|
|
18
|
+
/** External slot delivery owns cron timers and catch-up; local wake polling still runs. */
|
|
41
19
|
externalClock?: boolean;
|
|
42
20
|
}
|
|
43
|
-
/**
|
|
44
|
-
*
|
|
21
|
+
/**
|
|
22
|
+
* The iterator is a Promise port inside an uninterruptible claimed occurrence, including its cleanup.
|
|
23
|
+
*
|
|
24
|
+
* WHAT THE TURN SAID IS NOT LOGGED. It is already durable: every fire runs in the session named in the line below
|
|
25
|
+
* (`routine:<name>` for a cron, the asking conversation for a wake-up), and a session is persisted under
|
|
26
|
+
* EXPORTED for the API path (`POST /run`), which runs the same turn without a claim: it has no occurrence to
|
|
27
|
+
* claim, because nobody's grid produced it (schedule/run.ts). The resident clock's own claim/run/settle is
|
|
28
|
+
* {@link fireRoutineOnce}.
|
|
29
|
+
*
|
|
30
|
+
* `<stateRoot>/sessions/` like any other. Copying the reply into the log would be a second store of the same text,
|
|
31
|
+
* unstructured, in a stream with a wider audience and a bound that differs per host.
|
|
32
|
+
*/
|
|
33
|
+
export declare function runTurn(agent: Agent, label: string, session: string, prompt: string): Effect.Effect<{
|
|
34
|
+
busy: boolean;
|
|
35
|
+
failed: string | undefined;
|
|
36
|
+
ms: number;
|
|
37
|
+
} | {
|
|
38
|
+
busy: boolean;
|
|
39
|
+
failed: string;
|
|
40
|
+
ms: number;
|
|
41
|
+
}, never, never>;
|
|
45
42
|
export interface ScheduleFireOutcome {
|
|
46
43
|
fired: boolean;
|
|
47
|
-
/**
|
|
44
|
+
/** A delivery whose slot was already claimed, or which is older than the newest claim (a stale replay). */
|
|
48
45
|
skippedReason?: string;
|
|
49
46
|
failed?: string;
|
|
47
|
+
/**
|
|
48
|
+
* The occurrence was claimed but the turn was refused because the previous one is still running — the overlap
|
|
49
|
+
* policy, not a fault. Reported apart from `failed` because an operator reacts to the two differently, and a
|
|
50
|
+
* clock that reads `failed` on a public route would be reading an error that is not one.
|
|
51
|
+
*/
|
|
52
|
+
skipped?: true;
|
|
50
53
|
ms: number;
|
|
51
54
|
}
|
|
52
|
-
/**
|
|
53
|
-
|
|
54
|
-
* re-fire on restart), run, audit. Shared by the resident scheduler's timers and the external-clock
|
|
55
|
-
* serving surface (the AgentCore adapter).
|
|
56
|
-
*
|
|
57
|
-
* `slot` is the external clock's idempotency key — the cron instant this fire is FOR. External
|
|
58
|
-
* delivery (EventBridge-style) is at-least-once, so a duplicate slot must not double-fire: when
|
|
59
|
-
* lastFired ≥ slot the fire is SKIPPED (at-most-once per slot, same trade as the resident claim —
|
|
60
|
-
* "a digest late once" beats "twice"). The resident scheduler omits `slot`: its timers fire each
|
|
61
|
-
* slot exactly once, so the unconditional claim is already correct.
|
|
62
|
-
*
|
|
63
|
-
* A state fault while claiming (loadFires/saveFires — both before the invoke, so nothing ran)
|
|
64
|
-
* THROWS: the resident path catches it at its single skip+audit boundary ({@link createScheduler}'s
|
|
65
|
-
* fireThenReArm — skipping rather than firing unclaimed also avoids an infinite catch-up loop on
|
|
66
|
-
* restart), and the external path lets it surface as a failed request (visible in the clock's logs).
|
|
67
|
-
*/
|
|
68
|
-
export declare function fireScheduleOnce(opts: {
|
|
55
|
+
/** Shared resident/external claim → run → settle. */
|
|
56
|
+
export declare function fireRoutineOnce(opts: {
|
|
69
57
|
agent: Agent;
|
|
70
58
|
stateRoot: string;
|
|
71
|
-
|
|
72
|
-
|
|
59
|
+
routine: LoadedRoutine;
|
|
60
|
+
/** The instant this fire is FOR. Two schedulers agree on it, which is what makes the claim exclude. */
|
|
61
|
+
slot: Date;
|
|
73
62
|
now?: () => Date;
|
|
74
|
-
}):
|
|
75
|
-
export declare function createScheduler(
|
|
63
|
+
}): Effect.Effect<ScheduleFireOutcome, PortFailure>;
|
|
64
|
+
export declare function createScheduler(options: SchedulerOptions): Effect.Effect<Scheduler>;
|