experimental-ash 0.17.0 → 0.18.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/CHANGELOG.md +75 -0
- package/dist/docs/internals/schedule.md +37 -26
- package/dist/docs/public/schedules.md +61 -68
- package/dist/src/channel/schedule.d.ts +45 -32
- package/dist/src/channel/schedule.js +57 -44
- package/dist/src/chunks/authored-module-loader-XcFLnl49.js +2 -0
- package/dist/src/chunks/client-CKsU8Li3.js +4 -0
- package/dist/src/chunks/{dev-authored-source-watcher-B5J6JK7p.js → dev-authored-source-watcher-DtLxnrXI.js} +1 -1
- package/dist/src/chunks/{host-ByUKG--q.js → host-Dor4C8jo.js} +6 -6
- package/dist/src/chunks/paths-AVYgVLR3.js +88 -0
- package/dist/src/chunks/{prewarm-BLFoNX7E.js → prewarm-DsMkM8wg.js} +2 -2
- package/dist/src/cli/commands/info.js +1 -1
- package/dist/src/cli/dev/repl.js +1 -1
- package/dist/src/cli/run.js +1 -1
- package/dist/src/client/client.js +2 -1
- package/dist/src/client/index.d.ts +3 -0
- package/dist/src/client/index.js +1 -0
- package/dist/src/client/message-reducer-types.d.ts +130 -0
- package/dist/src/client/message-reducer-types.js +1 -0
- package/dist/src/client/message-reducer.d.ts +14 -0
- package/dist/src/client/message-reducer.js +462 -0
- package/dist/src/client/open-stream.js +2 -4
- package/dist/src/client/reducer.d.ts +63 -0
- package/dist/src/client/reducer.js +1 -0
- package/dist/src/client/session.js +3 -5
- package/dist/src/client/url.d.ts +8 -0
- package/dist/src/client/url.js +34 -0
- package/dist/src/compiler/manifest.d.ts +6 -24
- package/dist/src/compiler/manifest.js +2 -8
- package/dist/src/compiler/module-map.js +12 -0
- package/dist/src/compiler/normalize-channel.d.ts +0 -8
- package/dist/src/compiler/normalize-channel.js +0 -27
- package/dist/src/compiler/normalize-manifest.js +2 -10
- package/dist/src/compiler/normalize-schedule.d.ts +6 -12
- package/dist/src/compiler/normalize-schedule.js +9 -32
- package/dist/src/evals/cli/eval.js +1 -1
- package/dist/src/evals/runner/discover.js +1 -1
- package/dist/src/internal/application/package.js +1 -1
- package/dist/src/internal/authored-definition/core.d.ts +3 -2
- package/dist/src/internal/authored-definition/core.js +20 -10
- package/dist/src/internal/authored-module-loader.d.ts +0 -6
- package/dist/src/internal/authored-module-loader.js +11 -72
- package/dist/src/internal/nitro/routes/agent-info/build-agent-info-response.js +3 -1
- package/dist/src/internal/nitro/routes/runtime-stack.d.ts +0 -11
- package/dist/src/internal/nitro/routes/runtime-stack.js +0 -25
- package/dist/src/internal/nitro/routes/schedule-task.d.ts +3 -3
- package/dist/src/internal/nitro/routes/schedule-task.js +41 -11
- package/dist/src/public/definitions/schedule.d.ts +47 -36
- package/dist/src/public/definitions/schedule.js +10 -25
- package/dist/src/public/helpers/markdown.d.ts +6 -6
- package/dist/src/public/helpers/markdown.js +8 -8
- package/dist/src/public/schedules/index.d.ts +1 -1
- package/dist/src/public/schedules/index.js +1 -1
- package/dist/src/react/index.d.ts +3 -0
- package/dist/src/react/index.js +3 -0
- package/dist/src/react/use-ash-agent.d.ts +79 -0
- package/dist/src/react/use-ash-agent.js +330 -0
- package/dist/src/runtime/schedules/resolve-schedule.js +5 -5
- package/dist/src/runtime/types.d.ts +7 -10
- package/package.json +15 -2
- package/dist/src/chunks/authored-module-loader-Pt_g8xX2.js +0 -3
- package/dist/src/chunks/client-BeZ_W7vl.js +0 -4
- package/dist/src/chunks/paths-CFoo44rU.js +0 -88
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,80 @@
|
|
|
1
1
|
# experimental-ash
|
|
2
2
|
|
|
3
|
+
## 0.18.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 0d710b6: Fix `ResolveAgentError: Missing compiled module namespace for schedule
|
|
8
|
+
source ...` when a Nitro cron fires a TypeScript schedule that declares
|
|
9
|
+
an `async run({ ... })` handler.
|
|
10
|
+
|
|
11
|
+
The compiler's runtime module map (`.ash/compile/module-map.mjs`) lists
|
|
12
|
+
every module-backed authored source the runtime needs to load lazily.
|
|
13
|
+
It already collected channels, connections, tools, hooks, the sandbox,
|
|
14
|
+
the agent config, and the model — but it silently dropped schedules.
|
|
15
|
+
|
|
16
|
+
Markdown schedules (and TypeScript schedules whose only body is a
|
|
17
|
+
`markdown` string) execute via the pre-compiled `markdown` captured in
|
|
18
|
+
the manifest, so they never tripped the gap. Schedules with a `run`
|
|
19
|
+
handler, on the other hand, must load their default export at dispatch
|
|
20
|
+
time — and that load went straight to `ResolveAgentError` because the
|
|
21
|
+
module map had no entry for `schedules/<name>.ts`.
|
|
22
|
+
|
|
23
|
+
Module-sourced schedules with `hasRun: true` are now included in the
|
|
24
|
+
compiled module map. Markdown schedules and `markdown`-only module
|
|
25
|
+
schedules are still omitted (their body lives in the manifest and the
|
|
26
|
+
dispatcher never loads the source).
|
|
27
|
+
|
|
28
|
+
## 0.18.0
|
|
29
|
+
|
|
30
|
+
### Minor Changes
|
|
31
|
+
|
|
32
|
+
- 7304b4d: Replace the schedule `channel: receive(slack, args)` declarative form with
|
|
33
|
+
a `run` handler that receives the same `args.receive(channel, …)` route
|
|
34
|
+
handlers got in the previous release. Schedules now author one of:
|
|
35
|
+
|
|
36
|
+
- `markdown: "..."` — fire-and-forget agent invocation (channel-less,
|
|
37
|
+
output discarded). Markdown frontmatter form (`.md`) maps here.
|
|
38
|
+
- `async run({ receive, waitUntil, appAuth }) { ... }` — full handler.
|
|
39
|
+
`receive(channel, ...)` is the same cross-channel surface route
|
|
40
|
+
handlers use; `waitUntil(promise)` extends the cron task lifetime.
|
|
41
|
+
|
|
42
|
+
Migration:
|
|
43
|
+
|
|
44
|
+
```ts
|
|
45
|
+
// before
|
|
46
|
+
export default defineSchedule({
|
|
47
|
+
cron: "0 9 * * 1-5",
|
|
48
|
+
markdown: "Post the digest.",
|
|
49
|
+
channel: receive(slack, { channelId: "C0123ABC" }),
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
// after
|
|
53
|
+
export default defineSchedule({
|
|
54
|
+
cron: "0 9 * * 1-5",
|
|
55
|
+
async run({ receive, waitUntil, appAuth }) {
|
|
56
|
+
waitUntil(
|
|
57
|
+
receive(slack, {
|
|
58
|
+
message: "Post the digest.",
|
|
59
|
+
args: { channelId: "C0123ABC" },
|
|
60
|
+
auth: appAuth,
|
|
61
|
+
})
|
|
62
|
+
);
|
|
63
|
+
},
|
|
64
|
+
});
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Pure-markdown schedules (both `.md` form and `.ts` form with `markdown`)
|
|
68
|
+
are unchanged.
|
|
69
|
+
|
|
70
|
+
Removed: the `receive(channel, args)` schedule helper, the
|
|
71
|
+
`ChannelReceiveRef` type, the `ChannelRouteIdentityMap` compile-time
|
|
72
|
+
plumbing, the `ash-channel-identity` Rolldown plugin, and the
|
|
73
|
+
`channel`/`schedule.channel` manifest entry. Authored schedule
|
|
74
|
+
identity is now derived entirely at runtime from the channel value the
|
|
75
|
+
handler imports, matching how `args.receive(channel, …)` already works
|
|
76
|
+
in route handlers.
|
|
77
|
+
|
|
3
78
|
## 0.17.0
|
|
4
79
|
|
|
5
80
|
### Minor Changes
|
|
@@ -1,16 +1,18 @@
|
|
|
1
1
|
# Schedules
|
|
2
2
|
|
|
3
3
|
Schedules let an authored agent run on a cron cadence. The runtime owns the
|
|
4
|
-
dispatch contract;
|
|
4
|
+
dispatch contract; channels (when an author hands work off to one) own
|
|
5
|
+
delivery.
|
|
5
6
|
|
|
6
7
|
## Authoring shape
|
|
7
8
|
|
|
8
9
|
Each schedule is a single file under `schedules/`:
|
|
9
10
|
|
|
10
11
|
- `schedules/<name>.{ts,cts,mts,js,cjs,mjs}` — TypeScript module with a default
|
|
11
|
-
export of `defineSchedule({ cron, markdown
|
|
12
|
+
export of `defineSchedule({ cron, markdown? | run? })`. Exactly one of
|
|
13
|
+
`markdown` (fire-and-forget) or `run` (handler) is provided.
|
|
12
14
|
- `schedules/<name>.md` — markdown with frontmatter (`cron:`) and the body as
|
|
13
|
-
the prompt.
|
|
15
|
+
the prompt. Always fire-and-forget; markdown form cannot declare `run`.
|
|
14
16
|
|
|
15
17
|
Recursive nesting is supported. The schedule name is derived from the relative
|
|
16
18
|
path under `schedules/` minus the file extension
|
|
@@ -28,40 +30,45 @@ to the unified `discoverNamedSourceDirectory` walker with
|
|
|
28
30
|
hydrates each `ScheduleSourceRef` into a `CompiledScheduleDefinition`:
|
|
29
31
|
|
|
30
32
|
- TypeScript modules go through `loadModuleBackedDefinition` and
|
|
31
|
-
`normalizeScheduleDefinition` (rejects unknown keys
|
|
33
|
+
`normalizeScheduleDefinition` (rejects unknown keys; enforces the
|
|
34
|
+
`markdown` / `run` exclusivity rule).
|
|
32
35
|
- Markdown sources have already been lowered at discovery time; the compiler
|
|
33
36
|
re-runs `normalizeScheduleDefinition` for type safety.
|
|
34
|
-
-
|
|
35
|
-
|
|
36
|
-
loaded
|
|
37
|
-
|
|
38
|
-
- The compiled `markdown` field is `definition.markdown.trim()`.
|
|
37
|
+
- The compiled entry carries `hasRun: boolean` plus an optional `markdown`
|
|
38
|
+
string. The schedule's TypeScript module (for `hasRun` schedules) is
|
|
39
|
+
loaded at dispatch time via the standard module map; the compiler does
|
|
40
|
+
not bake the handler into the manifest.
|
|
39
41
|
- The schedule name is derived from the logical path
|
|
40
42
|
(`stripLogicalPathExtension(...).replace(/^schedules\//, "")`).
|
|
41
43
|
|
|
42
44
|
## Runtime
|
|
43
45
|
|
|
44
|
-
`ScheduleDispatcher.trigger` (`packages/ash/src/channel/schedule.ts`)
|
|
45
|
-
|
|
46
|
+
`ScheduleDispatcher.trigger` (`packages/ash/src/channel/schedule.ts`) builds
|
|
47
|
+
`ScheduleHandlerArgs` (a subset of `RouteHandlerArgs`) against the
|
|
48
|
+
request-scoped channel bundle and routes on the schedule shape:
|
|
46
49
|
|
|
47
|
-
- **
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
50
|
+
- **Handler (`run`):** invoke `definition.run(args)`. The handler decides
|
|
51
|
+
control flow; `args.receive(channel, …)` hands the work off to a channel,
|
|
52
|
+
`args.waitUntil(promise)` keeps the Nitro task open until the promise
|
|
53
|
+
settles. `appAuth` is the framework app principal pre-built for
|
|
54
|
+
convenience.
|
|
55
|
+
- **Markdown:** the dispatcher synthesizes a channel-less run by calling
|
|
56
|
+
`runtime.run({ adapter: SCHEDULE_ADAPTER, mode: "task", auth: APP_AUTH,
|
|
57
|
+
input: { message: markdown } })`. Output is discarded; the agent can still
|
|
58
|
+
call tools, log, or hit backends.
|
|
54
59
|
|
|
55
60
|
Both branches use the framework app principal: `principalId: "ash:app"`,
|
|
56
61
|
`principalType: "runtime"`.
|
|
57
62
|
|
|
58
|
-
|
|
63
|
+
`SCHEDULE_ADAPTER` is registered in `FRAMEWORK_ADAPTERS`
|
|
64
|
+
(`runtime/channels/registry.ts`) so the adapter registry can rehydrate
|
|
65
|
+
`{ kind: "schedule" }` at every workflow step boundary.
|
|
59
66
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
67
|
+
## Why markdown form omits `run`
|
|
68
|
+
|
|
69
|
+
Markdown frontmatter has no JavaScript scope; declaring a handler requires
|
|
70
|
+
TypeScript anyway. Markdown stays the path for fire-and-forget prompts;
|
|
71
|
+
`.ts` schedules cover everything else.
|
|
65
72
|
|
|
66
73
|
## Nitro task wiring
|
|
67
74
|
|
|
@@ -73,7 +80,8 @@ event name plus a baked-in artifacts config.
|
|
|
73
80
|
|
|
74
81
|
## Tests
|
|
75
82
|
|
|
76
|
-
- `src/channel/schedule.test.ts` — dispatcher behavior
|
|
83
|
+
- `src/channel/schedule.test.ts` — dispatcher behavior for both handler and
|
|
84
|
+
markdown branches plus the `waitUntil` accumulator.
|
|
77
85
|
- `src/discover/agent.integration.test.ts` — discovery shapes including
|
|
78
86
|
recursive nesting, markdown form, .md/.ts collision, legacy migration
|
|
79
87
|
diagnostic, and unsupported leaves.
|
|
@@ -83,6 +91,9 @@ event name plus a baked-in artifacts config.
|
|
|
83
91
|
— runtime resolution and Nitro task registration with mixed module/markdown
|
|
84
92
|
schedules.
|
|
85
93
|
- `test/scenarios/compile-agent.scenario.test.ts` — end-to-end compile with
|
|
86
|
-
module + markdown schedules
|
|
94
|
+
module + markdown schedules in both forms.
|
|
95
|
+
- `test/scenarios/schedule-trigger.scenario.test.ts` — markdown schedule
|
|
96
|
+
end-to-end through the real dispatcher, including the SCHEDULE_ADAPTER
|
|
97
|
+
step-boundary rehydration.
|
|
87
98
|
- `test/scenarios/runtime-loaders.scenario.test.ts` — runtime artifact
|
|
88
99
|
loaders round-trip.
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
---
|
|
2
2
|
title: "Schedules"
|
|
3
|
-
description: "Define recurring cron jobs that run an agent and optionally
|
|
3
|
+
description: "Define recurring cron jobs that run an agent and optionally hand off to a channel."
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
Schedules let an agent initiate recurring work — digests, syncs, maintenance, heartbeat checks — on a cron cadence.
|
|
6
|
+
Schedules let an agent initiate recurring work — digests, syncs, maintenance, heartbeat checks — on a cron cadence. Each schedule is either a fire-and-forget markdown prompt or a TypeScript handler that decides what to do (typically by handing the work off to a channel).
|
|
7
7
|
|
|
8
8
|
Schedules live under `schedules/` at the root agent package.
|
|
9
9
|
|
|
@@ -11,62 +11,77 @@ Schedules live under `schedules/` at the root agent package.
|
|
|
11
11
|
|
|
12
12
|
- schedules are root-only — local subagents cannot declare `schedules/`
|
|
13
13
|
- a schedule is a single file: `<name>.ts` or `<name>.md`
|
|
14
|
-
-
|
|
15
|
-
-
|
|
14
|
+
- every schedule provides exactly one of `markdown` (fire-and-forget) or `run` (handler)
|
|
15
|
+
- markdown-form (`.md`) schedules cannot declare a handler — use `.ts` for that
|
|
16
16
|
|
|
17
17
|
## Authoring shapes
|
|
18
18
|
|
|
19
|
-
### TypeScript
|
|
19
|
+
### TypeScript handler (`run`)
|
|
20
|
+
|
|
21
|
+
Use a handler when the schedule should deliver to a channel, branch on conditions, or compute args at fire time. The handler receives a tight subset of `RouteHandlerArgs`:
|
|
20
22
|
|
|
21
23
|
```ts
|
|
22
24
|
// agent/schedules/daily-digest.ts
|
|
23
|
-
import { defineSchedule
|
|
25
|
+
import { defineSchedule } from "experimental-ash/schedules";
|
|
24
26
|
|
|
25
27
|
import slack from "../channels/slack.js";
|
|
26
28
|
|
|
27
29
|
export default defineSchedule({
|
|
28
30
|
cron: "0 9 * * 1-5",
|
|
29
|
-
|
|
30
|
-
|
|
31
|
+
async run({ receive, waitUntil, appAuth }) {
|
|
32
|
+
waitUntil(
|
|
33
|
+
receive(slack, {
|
|
34
|
+
message: "Summarize yesterday's activity and post the digest.",
|
|
35
|
+
args: { channelId: "C0123ABC" },
|
|
36
|
+
auth: appAuth,
|
|
37
|
+
}),
|
|
38
|
+
);
|
|
39
|
+
},
|
|
31
40
|
});
|
|
32
41
|
```
|
|
33
42
|
|
|
43
|
+
`ScheduleHandlerArgs`:
|
|
44
|
+
|
|
45
|
+
- `receive(channel, { message, args, auth })` — same contract as a route handler's [`args.receive`](./channels/README.md#cross-channel-hand-off). Hands the work off to a channel's authored `receive` hook.
|
|
46
|
+
- `waitUntil(promise)` — extends the cron task's lifetime past handler return so in-flight work settles before the Nitro task completes.
|
|
47
|
+
- `appAuth` — pre-built APP auth context (`{ authenticator: "app", principalId: "ash:app", principalType: "runtime" }`). Pass to `receive(..., { auth: appAuth })` for schedules that run on behalf of the agent itself.
|
|
48
|
+
|
|
34
49
|
#### Announce-then-deliver
|
|
35
50
|
|
|
36
|
-
A
|
|
37
|
-
"what this is about" up front; the agent's reply then lands threaded under
|
|
38
|
-
that card. Same `initialPost` field [Slack's `receive`](./channels/README.md#cross-channel-hand-off)
|
|
39
|
-
accepts on the route-handler side:
|
|
51
|
+
A handler that hands off to Slack can post an anchor card before the agent runs so the thread shows "what this is about" up front; the agent's reply then lands threaded under that card. Same `initialPost` field [Slack's `receive`](./channels/README.md#cross-channel-hand-off) accepts from a route handler:
|
|
40
52
|
|
|
41
53
|
```ts
|
|
42
54
|
// agent/schedules/deploy-digest.ts
|
|
43
|
-
import { defineSchedule
|
|
55
|
+
import { defineSchedule } from "experimental-ash/schedules";
|
|
44
56
|
import { Card, CardText } from "experimental-ash/channels/slack";
|
|
45
57
|
|
|
46
58
|
import slack from "../channels/slack.js";
|
|
47
59
|
|
|
48
60
|
export default defineSchedule({
|
|
49
61
|
cron: "0 17 * * 1-5",
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
62
|
+
async run({ receive, waitUntil, appAuth }) {
|
|
63
|
+
waitUntil(
|
|
64
|
+
receive(slack, {
|
|
65
|
+
message: "Summarize today's production deploys.",
|
|
66
|
+
args: {
|
|
67
|
+
channelId: "C0123ABC",
|
|
68
|
+
initialPost: {
|
|
69
|
+
card: Card({ children: [CardText("Daily Deploy Digest")] }),
|
|
70
|
+
fallbackText: "Daily Deploy Digest",
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
auth: appAuth,
|
|
74
|
+
}),
|
|
75
|
+
);
|
|
76
|
+
},
|
|
58
77
|
});
|
|
59
78
|
```
|
|
60
79
|
|
|
61
|
-
`threadTs` and `initialPost` are mutually exclusive
|
|
62
|
-
an existing thread, or `initialPost` to anchor a new one.
|
|
80
|
+
`threadTs` and `initialPost` are mutually exclusive on Slack receive args. The anchor card's `ts` becomes the session's continuation token, so any later `@mention` reply in that thread resumes the same session — a digest that sparks a question can be answered in-thread without spinning up a new session.
|
|
63
81
|
|
|
64
|
-
|
|
65
|
-
reply in that thread resumes the same session. A nightly digest schedule whose digest
|
|
66
|
-
sparks a question can be answered in-thread without spinning up a new session — the
|
|
67
|
-
schedule's run and the operator's follow-up share one history.
|
|
82
|
+
### Markdown (`markdown`)
|
|
68
83
|
|
|
69
|
-
|
|
84
|
+
Fire-and-forget: the agent runs the prompt and the output is discarded. The agent is still free to call tools, write to backends, log, etc.
|
|
70
85
|
|
|
71
86
|
```ts
|
|
72
87
|
// agent/schedules/heartbeat.ts
|
|
@@ -78,7 +93,7 @@ export default defineSchedule({
|
|
|
78
93
|
});
|
|
79
94
|
```
|
|
80
95
|
|
|
81
|
-
|
|
96
|
+
Markdown frontmatter form:
|
|
82
97
|
|
|
83
98
|
```md
|
|
84
99
|
## <!-- agent/schedules/cleanup.md -->
|
|
@@ -88,7 +103,7 @@ export default defineSchedule({
|
|
|
88
103
|
Sweep stale workflow state.
|
|
89
104
|
```
|
|
90
105
|
|
|
91
|
-
The body is the prompt. The frontmatter accepts `cron` only
|
|
106
|
+
The body is the prompt. The frontmatter accepts `cron` only.
|
|
92
107
|
|
|
93
108
|
## Recursive nesting
|
|
94
109
|
|
|
@@ -108,62 +123,40 @@ agent/schedules/
|
|
|
108
123
|
```ts
|
|
109
124
|
interface ScheduleDefinition {
|
|
110
125
|
cron: string;
|
|
111
|
-
markdown
|
|
112
|
-
|
|
126
|
+
markdown?: string;
|
|
127
|
+
run?: (args: ScheduleHandlerArgs) => Promise<void> | void;
|
|
113
128
|
}
|
|
114
|
-
```
|
|
115
|
-
|
|
116
|
-
- `cron` (required) — a cron expression for when the schedule fires
|
|
117
|
-
- `markdown` (required) — the prompt the agent receives when the schedule runs
|
|
118
|
-
- `channel` (optional) — a typed channel reference produced by `receive(channel, args)`
|
|
119
|
-
|
|
120
|
-
The `markdown` field is the prompt body — the same convention used by `defineInstructions({ markdown })` and `defineSkill({ markdown })`.
|
|
121
|
-
|
|
122
|
-
## Channel targeting
|
|
123
|
-
|
|
124
|
-
When a schedule should deliver its output through a channel, use the `receive(channel, args)` helper from `experimental-ash/schedules`:
|
|
125
129
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
130
|
+
interface ScheduleHandlerArgs {
|
|
131
|
+
receive: CrossChannelReceiveFn;
|
|
132
|
+
waitUntil: (task: Promise<unknown>) => void;
|
|
133
|
+
appAuth: SessionAuthContext;
|
|
134
|
+
}
|
|
131
135
|
```
|
|
132
136
|
|
|
133
|
-
|
|
137
|
+
Exactly one of `markdown` or `run` must be provided. `defineSchedule` is a pass-through that exists to attach TypeScript types — it does not validate at authoring time; the compiler enforces the exclusivity rule.
|
|
134
138
|
|
|
135
139
|
## Runtime behavior
|
|
136
140
|
|
|
137
141
|
When a schedule fires, the dispatcher takes one of two paths:
|
|
138
142
|
|
|
139
|
-
###
|
|
143
|
+
### Handler form (`run`)
|
|
140
144
|
|
|
141
|
-
1. The dispatcher
|
|
142
|
-
2. It
|
|
143
|
-
3. The
|
|
145
|
+
1. The dispatcher loads the schedule's compiled module and reads `definition.run`.
|
|
146
|
+
2. It builds `ScheduleHandlerArgs` against the request-scoped channel bundle and invokes `run(args)`.
|
|
147
|
+
3. The handler decides everything: which channels to target, what auth to use, whether to short-circuit on holidays, etc.
|
|
148
|
+
4. Any promises the handler passes to `waitUntil` are awaited before the Nitro task settles.
|
|
144
149
|
|
|
145
|
-
###
|
|
150
|
+
### Markdown form
|
|
146
151
|
|
|
147
|
-
1. The dispatcher starts a session through the runtime directly with
|
|
152
|
+
1. The dispatcher starts a session through the runtime directly with the framework-owned `SCHEDULE_ADAPTER`.
|
|
148
153
|
2. The agent runs in **task mode** with the app principal.
|
|
149
|
-
3. Output is discarded
|
|
154
|
+
3. Output is discarded.
|
|
150
155
|
|
|
151
156
|
In both cases, the schedule session uses the **app principal**: `getSession().auth.current` and `getSession().auth.initiator` are set to `"ash:app"` with `principalType: "runtime"`.
|
|
152
157
|
|
|
153
158
|
The session goes through the same durable runtime engine as any other Ash session.
|
|
154
159
|
|
|
155
|
-
## Migrating from the legacy directory form
|
|
156
|
-
|
|
157
|
-
The directory form (`schedules/<name>/schedule.ts` + `schedules/<name>/prompt.md`) is no longer supported. To migrate, collapse each directory into a single file:
|
|
158
|
-
|
|
159
|
-
```text
|
|
160
|
-
agent/schedules/daily-digest/schedule.ts → agent/schedules/daily-digest.ts
|
|
161
|
-
agent/schedules/daily-digest/prompt.md → inlined as `markdown:` on
|
|
162
|
-
the `defineSchedule({...})` call
|
|
163
|
-
```
|
|
164
|
-
|
|
165
|
-
Discovery emits a clear migration diagnostic when it sees a legacy directory layout.
|
|
166
|
-
|
|
167
160
|
## When to use a schedule
|
|
168
161
|
|
|
169
162
|
Use a schedule when the agent should initiate work on its own cadence:
|
|
@@ -1,53 +1,66 @@
|
|
|
1
1
|
import type { ChannelAdapter } from "#channel/adapter.js";
|
|
2
2
|
import { type Session } from "#channel/session.js";
|
|
3
|
-
import type { Runtime } from "#channel/types.js";
|
|
3
|
+
import type { Runtime, SessionAuthContext } from "#channel/types.js";
|
|
4
|
+
import type { ScheduleRunHandler } from "#public/definitions/schedule.js";
|
|
4
5
|
import type { ResolvedChannelDefinition } from "#runtime/types.js";
|
|
5
6
|
/**
|
|
6
|
-
*
|
|
7
|
-
* not
|
|
7
|
+
* Pre-built application auth context handed to schedules. Schedules
|
|
8
|
+
* run on behalf of the agent itself, not a downstream user.
|
|
9
|
+
*/
|
|
10
|
+
export declare const SCHEDULE_APP_AUTH: SessionAuthContext;
|
|
11
|
+
/**
|
|
12
|
+
* Durable adapter kind used when a schedule fires without targeting a
|
|
13
|
+
* channel — the markdown form, and the synthesized run the dispatcher
|
|
14
|
+
* builds for it.
|
|
8
15
|
*
|
|
9
16
|
* Framework-owned — authored code never constructs a schedule adapter
|
|
10
|
-
* directly.
|
|
11
|
-
* authored schedule has no `channel` field (the only path available to
|
|
12
|
-
* markdown schedules, which are forbidden from declaring a channel).
|
|
17
|
+
* directly. Registered in `FRAMEWORK_ADAPTERS`.
|
|
13
18
|
*/
|
|
14
19
|
export declare const SCHEDULE_ADAPTER_KIND = "schedule";
|
|
20
|
+
export declare const SCHEDULE_ADAPTER: ChannelAdapter;
|
|
15
21
|
/**
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
* Carries no behavior — it is a bare discriminator so the runtime adapter
|
|
19
|
-
* registry can rehydrate `{ kind: "schedule" }` at every workflow step
|
|
20
|
-
* boundary. Registered in `FRAMEWORK_ADAPTERS`
|
|
21
|
-
* (`runtime/channels/registry.ts`).
|
|
22
|
+
* Loaded shape of one schedule for the dispatcher. Either `run` is
|
|
23
|
+
* defined (authored handler) or `markdown` is defined (fire-and-forget).
|
|
22
24
|
*/
|
|
23
|
-
export
|
|
24
|
-
type ScheduleChannel = Pick<ResolvedChannelDefinition, "receive" | "adapter">;
|
|
25
|
-
export interface ScheduleTriggerInput {
|
|
26
|
-
readonly channel?: {
|
|
27
|
-
readonly name: string;
|
|
28
|
-
readonly args: Readonly<Record<string, unknown>>;
|
|
29
|
-
};
|
|
30
|
-
readonly markdown: string;
|
|
25
|
+
export interface ScheduleDispatchInput {
|
|
31
26
|
readonly scheduleId: string;
|
|
27
|
+
readonly run?: ScheduleRunHandler;
|
|
28
|
+
readonly markdown?: string;
|
|
32
29
|
}
|
|
33
30
|
/**
|
|
34
|
-
*
|
|
31
|
+
* Dispatches scheduled task execution.
|
|
35
32
|
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
33
|
+
* For handler schedules: builds {@link ScheduleHandlerArgs} against the
|
|
34
|
+
* request-scoped channel bundle and invokes the author's `run`. The
|
|
35
|
+
* author owns control flow — `args.receive(channel, …)` hands work off
|
|
36
|
+
* to a channel; `args.waitUntil(promise)` extends the task lifetime
|
|
37
|
+
* so the dispatcher awaits in-flight work before settling.
|
|
38
|
+
*
|
|
39
|
+
* For markdown schedules: synthesizes a channel-less run that starts a
|
|
40
|
+
* session with {@link SCHEDULE_ADAPTER} in task mode and the markdown
|
|
41
|
+
* body as the message.
|
|
42
|
+
*
|
|
43
|
+
* Returns a {@link ScheduleDispatchResult} carrying any sessions the
|
|
44
|
+
* handler started (for telemetry / task-result observability) and the
|
|
45
|
+
* `waitUntil` promises the handler registered.
|
|
43
46
|
*/
|
|
47
|
+
export interface ScheduleDispatchResult {
|
|
48
|
+
readonly sessions: readonly Session[];
|
|
49
|
+
readonly waitUntilTasks: readonly Promise<unknown>[];
|
|
50
|
+
}
|
|
44
51
|
export declare class ScheduleDispatcher {
|
|
45
52
|
private readonly runtime;
|
|
46
|
-
private readonly
|
|
53
|
+
private readonly channels;
|
|
47
54
|
constructor(config: {
|
|
48
55
|
readonly runtime: Runtime;
|
|
49
|
-
readonly
|
|
56
|
+
readonly channels: readonly ResolvedChannelDefinition[];
|
|
50
57
|
});
|
|
51
|
-
trigger(input:
|
|
58
|
+
trigger(input: ScheduleDispatchInput): Promise<ScheduleDispatchResult>;
|
|
59
|
+
private runMarkdown;
|
|
52
60
|
}
|
|
53
|
-
|
|
61
|
+
/**
|
|
62
|
+
* Convenience: extract a `run` function from one loaded schedule module
|
|
63
|
+
* value, or throw with the file path so misconfigured modules fail
|
|
64
|
+
* obviously instead of crashing deep inside the dispatcher.
|
|
65
|
+
*/
|
|
66
|
+
export declare function expectScheduleRun(value: unknown, logicalPath: string, exportName: string | undefined): ScheduleRunHandler;
|
|
@@ -1,68 +1,81 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { createCrossChannelReceiveFn } from "#channel/cross-channel-receive.js";
|
|
2
2
|
import { createSession } from "#channel/session.js";
|
|
3
|
-
|
|
3
|
+
import { expectFunction } from "#internal/authored-module.js";
|
|
4
|
+
/**
|
|
5
|
+
* Pre-built application auth context handed to schedules. Schedules
|
|
6
|
+
* run on behalf of the agent itself, not a downstream user.
|
|
7
|
+
*/
|
|
8
|
+
export const SCHEDULE_APP_AUTH = {
|
|
4
9
|
attributes: {},
|
|
5
10
|
authenticator: "app",
|
|
6
11
|
principalId: "ash:app",
|
|
7
12
|
principalType: "runtime",
|
|
8
13
|
};
|
|
9
14
|
/**
|
|
10
|
-
* Durable adapter kind used when a schedule
|
|
11
|
-
*
|
|
15
|
+
* Durable adapter kind used when a schedule fires without targeting a
|
|
16
|
+
* channel — the markdown form, and the synthesized run the dispatcher
|
|
17
|
+
* builds for it.
|
|
12
18
|
*
|
|
13
19
|
* Framework-owned — authored code never constructs a schedule adapter
|
|
14
|
-
* directly.
|
|
15
|
-
* authored schedule has no `channel` field (the only path available to
|
|
16
|
-
* markdown schedules, which are forbidden from declaring a channel).
|
|
20
|
+
* directly. Registered in `FRAMEWORK_ADAPTERS`.
|
|
17
21
|
*/
|
|
18
22
|
export const SCHEDULE_ADAPTER_KIND = "schedule";
|
|
19
|
-
/**
|
|
20
|
-
* Framework adapter installed for channel-less schedules.
|
|
21
|
-
*
|
|
22
|
-
* Carries no behavior — it is a bare discriminator so the runtime adapter
|
|
23
|
-
* registry can rehydrate `{ kind: "schedule" }` at every workflow step
|
|
24
|
-
* boundary. Registered in `FRAMEWORK_ADAPTERS`
|
|
25
|
-
* (`runtime/channels/registry.ts`).
|
|
26
|
-
*/
|
|
27
23
|
export const SCHEDULE_ADAPTER = {
|
|
28
24
|
kind: SCHEDULE_ADAPTER_KIND,
|
|
29
25
|
};
|
|
30
|
-
/**
|
|
31
|
-
* Dispatcher for scheduled task execution.
|
|
32
|
-
*
|
|
33
|
-
* When the schedule targets a channel, hands the message off to
|
|
34
|
-
* `route.receive(input, { send })` — the channel author's normal entry
|
|
35
|
-
* point. When the schedule has no channel, starts the session through
|
|
36
|
-
* the runtime directly with a minimal schedule adapter. Both branches
|
|
37
|
-
* return a `Session` and let the workflow runtime own terminal
|
|
38
|
-
* completion, matching the fire-and-forget semantics HTTP routes already
|
|
39
|
-
* use.
|
|
40
|
-
*/
|
|
41
26
|
export class ScheduleDispatcher {
|
|
42
27
|
runtime;
|
|
43
|
-
|
|
28
|
+
channels;
|
|
44
29
|
constructor(config) {
|
|
45
30
|
this.runtime = config.runtime;
|
|
46
|
-
this.
|
|
31
|
+
this.channels = config.channels;
|
|
47
32
|
}
|
|
48
33
|
async trigger(input) {
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
34
|
+
const sessions = [];
|
|
35
|
+
const waitUntilTasks = [];
|
|
36
|
+
const receive = createCrossChannelReceiveFn(this.runtime, this.channels);
|
|
37
|
+
const args = {
|
|
38
|
+
appAuth: SCHEDULE_APP_AUTH,
|
|
39
|
+
receive: async (channel, options) => {
|
|
40
|
+
const session = await receive(channel, options);
|
|
41
|
+
sessions.push(session);
|
|
42
|
+
return session;
|
|
43
|
+
},
|
|
44
|
+
waitUntil(task) {
|
|
45
|
+
waitUntilTasks.push(task);
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
if (input.run) {
|
|
49
|
+
await input.run(args);
|
|
57
50
|
}
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
51
|
+
else if (input.markdown !== undefined) {
|
|
52
|
+
const session = await this.runMarkdown(input.markdown);
|
|
53
|
+
sessions.push(session);
|
|
54
|
+
}
|
|
55
|
+
else {
|
|
56
|
+
throw new Error(`Schedule "${input.scheduleId}" has neither "run" nor "markdown" — at least one must be set.`);
|
|
57
|
+
}
|
|
58
|
+
return { sessions, waitUntilTasks };
|
|
59
|
+
}
|
|
60
|
+
async runMarkdown(markdown) {
|
|
61
|
+
const handle = await this.runtime.run({
|
|
62
|
+
adapter: SCHEDULE_ADAPTER,
|
|
63
|
+
auth: SCHEDULE_APP_AUTH,
|
|
64
|
+
input: { message: markdown },
|
|
65
|
+
mode: "task",
|
|
66
66
|
});
|
|
67
|
+
return createSession(handle.sessionId, handle.continuationToken, this.runtime);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Convenience: extract a `run` function from one loaded schedule module
|
|
72
|
+
* value, or throw with the file path so misconfigured modules fail
|
|
73
|
+
* obviously instead of crashing deep inside the dispatcher.
|
|
74
|
+
*/
|
|
75
|
+
export function expectScheduleRun(value, logicalPath, exportName) {
|
|
76
|
+
const definition = value;
|
|
77
|
+
if (definition === null || typeof definition !== "object") {
|
|
78
|
+
throw new Error(`Schedule export "${exportName ?? "default"}" from "${logicalPath}" must be an object.`);
|
|
67
79
|
}
|
|
80
|
+
return expectFunction(definition.run, `Expected the schedule export "${exportName ?? "default"}" from "${logicalPath}" to export a \`run\` handler function.`);
|
|
68
81
|
}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import{createRequire as e}from"node:module";import{dirname as t,join as n,resolve as r,sep as i}from"node:path";import{createHash as a}from"node:crypto";import{existsSync as o,mkdirSync as s,readFileSync as c,writeFileSync as l}from"node:fs";import{pathToFileURL as u}from"node:url";function d(e,t){return e[t.exportName??`default`]}async function f(e){return typeof e==`function`?await e():e}function p(e,t){if(typeof e!=`object`||!e||Array.isArray(e))throw Error(t);return e}function m(e,t){if(typeof e!=`string`)throw Error(t);return e}function h(e,t){if(typeof e!=`function`)throw Error(t);return e}function g(e,t){let n=p(e,t),r={};for(let[e,i]of Object.entries(n))r[e]=p(i,t);return r}function _(e,t,n){let r=new Set(t);for(let t of Object.keys(e))if(!r.has(t))throw Error(`${n} Unknown key "${t}".`)}function v(e,t,n){let r=e[t];if(r===void 0)return;let i=p(r,n),a={};for(let[e,t]of Object.entries(i))a[e]=m(t,n);return a}let y,b;function x(){return y??=(async()=>await import(u(e(e(import.meta.url).resolve(`nitro/package.json`)).resolve(`rolldown`)).href))(),y}function S(){return b??=(async()=>await import(u(e(e(import.meta.url).resolve(`nitro/package.json`)).resolve(`rolldown/parseAst`)).href))(),b}async function C(e){let{build:t}=await x();return await t(e)}function w(e,t){let n=e.output.filter(e=>e.type===`chunk`),r=n[0];if(r===void 0||n.length!==1)throw Error(`Expected one bundled ${t}.`);return r}const T=[{importLine:`import { fileURLToPath as __ashFileURLToPath } from "node:url";`,declarationLine:`const __filename = __ashFileURLToPath(import.meta.url);`,bindingPattern:/^(?:const|let|var)\s+__filename\b/m},{importLine:`import { dirname as __ashDirname } from "node:path";`,declarationLine:`const __dirname = __ashDirname(__filename);`,bindingPattern:/^(?:const|let|var)\s+__dirname\b/m}],E={importLine:`import { createRequire as __ashCreateRequire } from "node:module";`,declarationLine:`const require = __ashCreateRequire(import.meta.url);`,bindingPattern:/^(?:const|let|var)\s+require\b/m};function D(e,t={}){let n=[...T];t.includeRequire===!0&&n.push(E);let r=[],i=[];for(let t of n)t.bindingPattern.test(e)||(r.push(t.importLine),i.push(t.declarationLine));return i.length===0?``:[...r,...i].join(`
|
|
2
|
+
`)}function O(e={}){return{name:`ash-node-esm-compat-banner`,renderChunk(t){let n=D(t,e);return n===``?null:{code:`${n}\n${t}`}}}}const k=/\.[cm]?[jt]sx?$/,A=n(`node_modules`,`.cache`,`experimental-ash`,`authored-modules`),j=[`.ts`,`.tsx`,`.mts`,`.cts`,`.js`,`.jsx`,`.mjs`,`.cjs`,`.json`],M=new Map;function N(e){let t=r(e),n=M.get(t);if(n!==void 0)return n;let i=(async()=>{try{return await P(e)}finally{M.delete(t)}})();return M.set(t,i),i}async function P(e){return p(k.test(e)?await I(e):await import(F(e)),`Expected "${e}" to export a module namespace object.`)}function F(e){let t=e.replaceAll(`\\`,`/`);return/^[A-Za-z]:\//.test(t)?`file:///${encodeURI(t)}`:t.startsWith(`/`)?`file://${encodeURI(t)}`:t}async function I(e){let t=q(e),r=n(t,`tsconfig.json`),i=R({tsconfigPath:r}),c=w(await C({cwd:t,input:e,platform:`node`,plugins:[O({includeRequire:!0}),L(t),i].filter(e=>e!==null),resolve:{extensions:[...j]},tsconfig:o(r)?r:!1,write:!1,output:{comments:!1,format:`esm`,sourcemap:`inline`}}),`authored module for "${e}"`),u=a(`sha1`).update(e).update(`\0`).update(c.code).digest(`hex`),d=n(t,A),f=n(d,`${u}.mjs`);return o(f)||(s(d,{recursive:!0}),l(f,c.code)),await import(`${F(f)}?v=${u}`)}function L(e){return{name:`ash-package-boundary`,async resolveId(t,n,i){if(!U(t))return;if(W(t))return{external:!0,id:t};let a=n===void 0||n.startsWith(`\0`)?void 0:r(n);if(a!==void 0&&K(a,e)){let e=await this.resolve(t,n,{kind:i.kind,skipSelf:!0});if(e===null||typeof e.id!=`string`||G(e.id))return{external:!0,id:t}}}}}function R(e){let t=z(e);return t.length===0?null:{name:`ash-tsconfig-path-alias`,resolveId(e){for(let n of t){let t=B(n.pattern,e);if(t!==null)for(let e of n.targets){let n=V(e,t);if(n!==null)return n}}}}}function z(e){if(!o(e.tsconfigPath))return[];try{let n=JSON.parse(c(e.tsconfigPath,`utf8`)),i=n.compilerOptions?.paths;if(typeof i!=`object`||!i||Array.isArray(i))return[];let a=typeof n.compilerOptions?.baseUrl==`string`?n.compilerOptions.baseUrl:`.`,o=r(t(e.tsconfigPath),a);return Object.entries(i).flatMap(([e,t])=>{if(!Array.isArray(t))return[];let n=t.flatMap(e=>typeof e==`string`?[r(o,e)]:[]);return n.length===0?[]:[{pattern:e,targets:n}]})}catch{return[]}}function B(e,t){let n=e.indexOf(`*`);if(n===-1)return e===t?``:null;let r=e.slice(0,n),i=e.slice(n+1);return!t.startsWith(r)||!t.endsWith(i)?null:t.slice(r.length,t.length-i.length)}function V(e,t){let r=e.replace(`*`,t),i=H(r);if(i!==null)return i;for(let e of j){let t=H(`${r}${e}`);if(t!==null)return t}for(let e of j){let t=H(n(r,`index${e}`));if(t!==null)return t}return null}function H(e){return o(e)?e:null}function U(e){return!(e.startsWith(`.`)||e.startsWith(`/`)||/^[A-Za-z]:[\\/]/.test(e)||/^(?:node|data|file):/.test(e)||e.startsWith(`@/`))}function W(e){return e===`experimental-ash`||e.startsWith(`experimental-ash/`)}function G(e){return e.replaceAll(`\\`,`/`).includes(`/node_modules/`)}function K(e,t){let n=r(e),a=r(t);return n===a||n.startsWith(`${a}${i}`)}function q(e){let r=t(e);for(;;){if(o(n(r,`package.json`)))return r;let i=t(r);if(i===r)throw Error(`Failed to resolve the authored package root for "${e}".`);r=i}}export{S as a,_ as c,d,v as f,w as i,g as l,O as n,h as o,f as p,C as r,p as s,N as t,m as u};
|