wolli 0.0.3 → 0.0.4
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 +23 -24
- package/built-in/plugins/discord/discord-chat.ts +47 -84
- package/built-in/plugins/discord/index.ts +139 -97
- package/built-in/plugins/discord/package.json +2 -2
- package/built-in/plugins/scheduler/cron.ts +131 -0
- package/built-in/plugins/scheduler/index.ts +147 -151
- package/built-in/plugins/scheduler/package.json +3 -2
- package/built-in/plugins/scheduler/scheduler-due.ts +22 -0
- package/built-in/plugins/telegram/README.md +3 -3
- package/built-in/plugins/telegram/index.ts +177 -139
- package/built-in/plugins/telegram/package.json +2 -2
- package/built-in/plugins/telegram/telegram-chat.ts +79 -155
- package/dist/cli.js +5696 -5841
- package/docs/hooks.md +103 -0
- package/docs/index.md +10 -8
- package/docs/integrations.md +78 -663
- package/docs/introduction.md +95 -0
- package/docs/plugins.md +43 -36
- package/docs/providers.md +63 -0
- package/docs/sdk.md +42 -47
- package/docs/tools.md +81 -0
- package/docs/workflows.md +170 -0
- package/package.json +1 -1
- package/built-in/plugins/scheduler/scheduler-chat.ts +0 -164
- package/docs/extensions.md +0 -2331
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cron tool — lets the agent schedule its own prompts (add / list / update / remove / run)
|
|
3
|
+
* by driving the scheduler integration's CRUD actions. `add` snapshots the calling
|
|
4
|
+
* session's tags onto the job (`originTags`), so when the job fires, `scheduler-due.ts`
|
|
5
|
+
* wakes the session that surface belongs to and its reply returns there with no
|
|
6
|
+
* scheduler-side special-casing.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { defineTool } from "wolli";
|
|
10
|
+
import { Type } from "typebox";
|
|
11
|
+
import scheduler from "./index.ts";
|
|
12
|
+
|
|
13
|
+
const CronParams = Type.Object({
|
|
14
|
+
action: Type.Union([
|
|
15
|
+
Type.Literal("add"),
|
|
16
|
+
Type.Literal("list"),
|
|
17
|
+
Type.Literal("update"),
|
|
18
|
+
Type.Literal("remove"),
|
|
19
|
+
Type.Literal("run"),
|
|
20
|
+
]),
|
|
21
|
+
prompt: Type.Optional(Type.String({ description: "What to run (the woken session's first message)." })),
|
|
22
|
+
name: Type.Optional(Type.String({ description: "Human label for the job." })),
|
|
23
|
+
at: Type.Optional(Type.Number({ description: "One-shot run time, epoch ms." })),
|
|
24
|
+
everyMs: Type.Optional(Type.Number({ description: "Fixed interval in ms." })),
|
|
25
|
+
cron: Type.Optional(Type.String({ description: "Cron expression (5/6-field)." })),
|
|
26
|
+
tz: Type.Optional(Type.String({ description: "Timezone for the cron expression (host local if omitted)." })),
|
|
27
|
+
id: Type.Optional(Type.String({ description: "Job id, for update / remove / run." })),
|
|
28
|
+
enabled: Type.Optional(Type.Boolean({ description: "Enable or disable the job (update)." })),
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
/** The fields of a job this tool reads back from `listJobs` (the integration owns the full shape). */
|
|
32
|
+
interface Job {
|
|
33
|
+
id: string;
|
|
34
|
+
name?: string;
|
|
35
|
+
schedule: { kind: "at"; at: number } | { kind: "every"; everyMs: number } | { kind: "cron"; expr: string; tz?: string };
|
|
36
|
+
enabled: boolean;
|
|
37
|
+
nextRunAt: number;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Map the tool's flat `at`/`everyMs`/`cron` fields onto the integration's `Schedule` union. */
|
|
41
|
+
function buildSchedule(p: { at?: number; everyMs?: number; cron?: string; tz?: string }): Job["schedule"] | undefined {
|
|
42
|
+
if (p.at !== undefined) return { kind: "at", at: p.at };
|
|
43
|
+
if (p.everyMs !== undefined) return { kind: "every", everyMs: p.everyMs };
|
|
44
|
+
if (p.cron !== undefined) return { kind: "cron", expr: p.cron, tz: p.tz };
|
|
45
|
+
return undefined;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function describeSchedule(schedule: Job["schedule"]): string {
|
|
49
|
+
switch (schedule.kind) {
|
|
50
|
+
case "at":
|
|
51
|
+
return `at ${new Date(schedule.at).toISOString()}`;
|
|
52
|
+
case "every":
|
|
53
|
+
return `every ${schedule.everyMs}ms`;
|
|
54
|
+
case "cron":
|
|
55
|
+
return `cron "${schedule.expr}"${schedule.tz ? ` (${schedule.tz})` : ""}`;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function text(message: string, details: unknown) {
|
|
60
|
+
return { content: [{ type: "text" as const, text: message }], details };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export default defineTool({
|
|
64
|
+
name: "cron",
|
|
65
|
+
label: "Cron",
|
|
66
|
+
description:
|
|
67
|
+
"Schedule prompts to run later. Actions: add (prompt + at/everyMs/cron), list, update (id), remove (id), run (id).",
|
|
68
|
+
parameters: CronParams,
|
|
69
|
+
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
70
|
+
const sched = ctx.integration(scheduler);
|
|
71
|
+
try {
|
|
72
|
+
switch (params.action) {
|
|
73
|
+
case "add": {
|
|
74
|
+
if (!params.prompt) return text("Error: prompt is required to add a job.", { error: "prompt required" });
|
|
75
|
+
const schedule = buildSchedule(params);
|
|
76
|
+
if (!schedule) {
|
|
77
|
+
return text("Error: provide one of at, everyMs, or cron.", { error: "schedule required" });
|
|
78
|
+
}
|
|
79
|
+
// Snapshot the scheduling session's tags so the fired result returns to this surface.
|
|
80
|
+
const result = (await sched.addJob({
|
|
81
|
+
prompt: params.prompt,
|
|
82
|
+
name: params.name,
|
|
83
|
+
schedule,
|
|
84
|
+
originTags: ctx.session.getTags(),
|
|
85
|
+
})) as { id: string; nextRunAt: number };
|
|
86
|
+
return text(
|
|
87
|
+
`Scheduled job ${result.id} — ${describeSchedule(schedule)} — next ${new Date(result.nextRunAt).toISOString()}.`,
|
|
88
|
+
result,
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
case "list": {
|
|
92
|
+
const result = (await sched.listJobs({})) as { jobs: Job[] };
|
|
93
|
+
const body = result.jobs.length
|
|
94
|
+
? result.jobs
|
|
95
|
+
.map((j) => {
|
|
96
|
+
const label = j.name ? `${j.name} ` : "";
|
|
97
|
+
const state = j.enabled ? `next ${new Date(j.nextRunAt).toISOString()}` : "disabled";
|
|
98
|
+
return `${j.id} ${label}— ${describeSchedule(j.schedule)} — ${state}`;
|
|
99
|
+
})
|
|
100
|
+
.join("\n")
|
|
101
|
+
: "No scheduled jobs.";
|
|
102
|
+
return text(body, result);
|
|
103
|
+
}
|
|
104
|
+
case "update": {
|
|
105
|
+
if (!params.id) return text("Error: id is required to update a job.", { error: "id required" });
|
|
106
|
+
const result = await sched.updateJob({
|
|
107
|
+
id: params.id,
|
|
108
|
+
prompt: params.prompt,
|
|
109
|
+
name: params.name,
|
|
110
|
+
schedule: buildSchedule(params),
|
|
111
|
+
enabled: params.enabled,
|
|
112
|
+
});
|
|
113
|
+
return text(`Updated job ${params.id}.`, result);
|
|
114
|
+
}
|
|
115
|
+
case "remove": {
|
|
116
|
+
if (!params.id) return text("Error: id is required to remove a job.", { error: "id required" });
|
|
117
|
+
const result = (await sched.removeJob({ id: params.id })) as { removed: boolean };
|
|
118
|
+
return text(result.removed ? `Removed job ${params.id}.` : `No job ${params.id}.`, result);
|
|
119
|
+
}
|
|
120
|
+
case "run": {
|
|
121
|
+
if (!params.id) return text("Error: id is required to run a job.", { error: "id required" });
|
|
122
|
+
const result = await sched.runJob({ id: params.id });
|
|
123
|
+
return text(`Job ${params.id} will run on the next tick.`, result);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
} catch (err) {
|
|
127
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
128
|
+
return text(`Error: ${message}`, { error: message });
|
|
129
|
+
}
|
|
130
|
+
},
|
|
131
|
+
});
|
|
@@ -4,13 +4,12 @@
|
|
|
4
4
|
* This integration owns the jobs and the wake loop: it persists jobs in `ctx.store`
|
|
5
5
|
* (one file at `~/.wolli/agents/<name>/store/scheduler.json`), ticks a coarse timer,
|
|
6
6
|
* and emits a `due` event when a job's time arrives. It does not touch sessions or the
|
|
7
|
-
* agent; the
|
|
8
|
-
*
|
|
9
|
-
* split.
|
|
7
|
+
* agent; the `cron` tool (`cron.ts`) drives the CRUD actions and the `scheduler-due.ts`
|
|
8
|
+
* workflow, on `due`, wakes the session the job was scheduled from.
|
|
10
9
|
*
|
|
11
10
|
* Jobs are scheduled by the agent through the `cron` tool, which calls the CRUD actions
|
|
12
|
-
* below. The scheduler has no secret — onboarding just writes an empty `scheduler
|
|
13
|
-
* account so `run()` starts.
|
|
11
|
+
* below. The scheduler has no secret — onboarding just writes an empty `scheduler`
|
|
12
|
+
* account record so `run()` starts.
|
|
14
13
|
*
|
|
15
14
|
* ## Guarantees
|
|
16
15
|
* - At-most-once: a tick advances a job's `nextRunAt` (or disables a one-shot) and
|
|
@@ -21,7 +20,7 @@
|
|
|
21
20
|
*/
|
|
22
21
|
|
|
23
22
|
import { randomUUID } from "node:crypto";
|
|
24
|
-
import
|
|
23
|
+
import { defineIntegration, type IntegrationOnboardContext, type KeyValueStore } from "wolli";
|
|
25
24
|
import { Cron } from "croner";
|
|
26
25
|
import { type Static, Type } from "typebox";
|
|
27
26
|
|
|
@@ -80,165 +79,162 @@ async function onboard(ctx: IntegrationOnboardContext): Promise<Record<string, u
|
|
|
80
79
|
return {};
|
|
81
80
|
}
|
|
82
81
|
|
|
83
|
-
export default
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
82
|
+
export default defineIntegration({
|
|
83
|
+
account: Type.Object({
|
|
84
|
+
/** Wake interval in ms; defaults to 60s. */
|
|
85
|
+
tickMs: Type.Optional(Type.Number()),
|
|
86
|
+
}),
|
|
87
|
+
events: {
|
|
88
|
+
due: Type.Object({
|
|
89
|
+
id: Type.String(),
|
|
90
|
+
prompt: Type.String(),
|
|
91
|
+
originTags: Type.Optional(Type.Record(Type.String(), Type.String())),
|
|
92
|
+
name: Type.Optional(Type.String()),
|
|
89
93
|
}),
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
94
|
+
},
|
|
95
|
+
onboard,
|
|
96
|
+
actions: {
|
|
97
|
+
addJob: {
|
|
98
|
+
description: "Schedule a new job from a prompt and a schedule (at / every / cron).",
|
|
99
|
+
parameters: Type.Object({
|
|
93
100
|
prompt: Type.String(),
|
|
94
|
-
originTags: Type.Optional(Type.Record(Type.String(), Type.String())),
|
|
95
101
|
name: Type.Optional(Type.String()),
|
|
102
|
+
schedule: Schedule,
|
|
103
|
+
originTags: Type.Optional(Type.Record(Type.String(), Type.String())),
|
|
96
104
|
}),
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
name: p.name,
|
|
120
|
-
prompt: p.prompt,
|
|
121
|
-
schedule: p.schedule,
|
|
122
|
-
enabled: seeded !== null,
|
|
123
|
-
originTags: p.originTags,
|
|
124
|
-
nextRunAt: seeded ?? 0,
|
|
125
|
-
};
|
|
126
|
-
const jobs = loadJobs(ctx.store);
|
|
127
|
-
jobs[job.id] = job;
|
|
128
|
-
saveJobs(ctx.store, jobs);
|
|
129
|
-
return { id: job.id, nextRunAt: job.nextRunAt };
|
|
130
|
-
},
|
|
105
|
+
execute: async (params, ctx) => {
|
|
106
|
+
const p = params as {
|
|
107
|
+
prompt: string;
|
|
108
|
+
name?: string;
|
|
109
|
+
schedule: Schedule;
|
|
110
|
+
originTags?: Record<string, string>;
|
|
111
|
+
};
|
|
112
|
+
const now = Date.now();
|
|
113
|
+
const seeded = computeNextRunAt(p.schedule, now);
|
|
114
|
+
const job: Job = {
|
|
115
|
+
id: randomUUID(),
|
|
116
|
+
name: p.name,
|
|
117
|
+
prompt: p.prompt,
|
|
118
|
+
schedule: p.schedule,
|
|
119
|
+
enabled: seeded !== null,
|
|
120
|
+
originTags: p.originTags,
|
|
121
|
+
nextRunAt: seeded ?? 0,
|
|
122
|
+
};
|
|
123
|
+
const jobs = loadJobs(ctx.store);
|
|
124
|
+
jobs[job.id] = job;
|
|
125
|
+
saveJobs(ctx.store, jobs);
|
|
126
|
+
return { id: job.id, nextRunAt: job.nextRunAt };
|
|
131
127
|
},
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
}
|
|
128
|
+
},
|
|
129
|
+
listJobs: {
|
|
130
|
+
description: "List all scheduled jobs.",
|
|
131
|
+
parameters: Type.Object({}),
|
|
132
|
+
execute: async (_params, ctx) => {
|
|
133
|
+
return { jobs: Object.values(loadJobs(ctx.store)) };
|
|
138
134
|
},
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
135
|
+
},
|
|
136
|
+
updateJob: {
|
|
137
|
+
description: "Update a job by id; recomputes the next run when the schedule changes.",
|
|
138
|
+
parameters: Type.Object({
|
|
139
|
+
id: Type.String(),
|
|
140
|
+
prompt: Type.Optional(Type.String()),
|
|
141
|
+
name: Type.Optional(Type.String()),
|
|
142
|
+
schedule: Type.Optional(Schedule),
|
|
143
|
+
enabled: Type.Optional(Type.Boolean()),
|
|
144
|
+
}),
|
|
145
|
+
execute: async (params, ctx) => {
|
|
146
|
+
const p = params as {
|
|
147
|
+
id: string;
|
|
148
|
+
prompt?: string;
|
|
149
|
+
name?: string;
|
|
150
|
+
schedule?: Schedule;
|
|
151
|
+
enabled?: boolean;
|
|
152
|
+
};
|
|
153
|
+
const jobs = loadJobs(ctx.store);
|
|
154
|
+
const job = jobs[p.id];
|
|
155
|
+
if (!job) throw new Error(`unknown job '${p.id}'`);
|
|
159
156
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
157
|
+
if (p.prompt !== undefined) job.prompt = p.prompt;
|
|
158
|
+
if (p.name !== undefined) job.name = p.name;
|
|
159
|
+
if (p.enabled !== undefined) job.enabled = p.enabled;
|
|
160
|
+
if (p.schedule !== undefined) {
|
|
161
|
+
job.schedule = p.schedule;
|
|
162
|
+
const next = computeNextRunAt(p.schedule, Date.now());
|
|
163
|
+
job.nextRunAt = next ?? 0;
|
|
164
|
+
if (next === null) job.enabled = false;
|
|
165
|
+
}
|
|
169
166
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
},
|
|
167
|
+
saveJobs(ctx.store, jobs);
|
|
168
|
+
return { job };
|
|
173
169
|
},
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
}
|
|
170
|
+
},
|
|
171
|
+
removeJob: {
|
|
172
|
+
description: "Delete a job by id.",
|
|
173
|
+
parameters: Type.Object({ id: Type.String() }),
|
|
174
|
+
execute: async (params, ctx) => {
|
|
175
|
+
const { id } = params as { id: string };
|
|
176
|
+
const jobs = loadJobs(ctx.store);
|
|
177
|
+
const removed = id in jobs;
|
|
178
|
+
delete jobs[id];
|
|
179
|
+
saveJobs(ctx.store, jobs);
|
|
180
|
+
return { removed };
|
|
185
181
|
},
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
}
|
|
182
|
+
},
|
|
183
|
+
runJob: {
|
|
184
|
+
description: "Run a job on the next tick (sets it due immediately).",
|
|
185
|
+
parameters: Type.Object({ id: Type.String() }),
|
|
186
|
+
execute: async (params, ctx) => {
|
|
187
|
+
const { id } = params as { id: string };
|
|
188
|
+
const jobs = loadJobs(ctx.store);
|
|
189
|
+
const job = jobs[id];
|
|
190
|
+
if (!job) throw new Error(`unknown job '${id}'`);
|
|
191
|
+
job.enabled = true;
|
|
192
|
+
job.nextRunAt = 0;
|
|
193
|
+
saveJobs(ctx.store, jobs);
|
|
194
|
+
return { id, nextRunAt: job.nextRunAt };
|
|
199
195
|
},
|
|
200
196
|
},
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
197
|
+
},
|
|
198
|
+
run(ctx) {
|
|
199
|
+
const account = ctx.account as SchedulerAccount;
|
|
200
|
+
const tickMs = account.tickMs ?? DEFAULT_TICK_MS;
|
|
204
201
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
}
|
|
219
|
-
due.push(job);
|
|
202
|
+
const tick = (): void => {
|
|
203
|
+
const now = Date.now();
|
|
204
|
+
const jobs = loadJobs(ctx.store);
|
|
205
|
+
const due: Job[] = [];
|
|
206
|
+
for (const job of Object.values(jobs)) {
|
|
207
|
+
if (!job.enabled || job.nextRunAt > now) continue;
|
|
208
|
+
job.lastRunAt = now;
|
|
209
|
+
if (job.schedule.kind === "at") {
|
|
210
|
+
job.enabled = false; // one-shot
|
|
211
|
+
} else {
|
|
212
|
+
const next = computeNextRunAt(job.schedule, now);
|
|
213
|
+
if (next === null) job.enabled = false;
|
|
214
|
+
else job.nextRunAt = next;
|
|
220
215
|
}
|
|
221
|
-
|
|
216
|
+
due.push(job);
|
|
217
|
+
}
|
|
218
|
+
if (due.length === 0) return;
|
|
222
219
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
220
|
+
// Persist the advanced state before emitting so a crash right after an emit never re-fires.
|
|
221
|
+
saveJobs(ctx.store, jobs);
|
|
222
|
+
for (const job of due) {
|
|
223
|
+
ctx.emit("due", {
|
|
224
|
+
id: job.id,
|
|
225
|
+
prompt: job.prompt,
|
|
226
|
+
originTags: job.originTags,
|
|
227
|
+
name: job.name,
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
};
|
|
234
231
|
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
232
|
+
// One catch-up tick on start: each overdue job fires once (recompute-from-now), not N replays.
|
|
233
|
+
tick();
|
|
234
|
+
const timer = setInterval(tick, tickMs);
|
|
238
235
|
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
}
|
|
236
|
+
const dispose = () => clearInterval(timer);
|
|
237
|
+
ctx.signal.addEventListener("abort", dispose);
|
|
238
|
+
return dispose;
|
|
239
|
+
},
|
|
240
|
+
});
|
|
@@ -5,12 +5,13 @@
|
|
|
5
5
|
"type": "module",
|
|
6
6
|
"wolli": {
|
|
7
7
|
"integrations": ["./index.ts"],
|
|
8
|
-
"
|
|
8
|
+
"workflows": ["./scheduler-due.ts"],
|
|
9
|
+
"tools": ["./cron.ts"]
|
|
9
10
|
},
|
|
10
11
|
"dependencies": {
|
|
11
12
|
"croner": "10.0.1"
|
|
12
13
|
},
|
|
13
14
|
"peerDependencies": {
|
|
14
|
-
"
|
|
15
|
+
"wolli": "*"
|
|
15
16
|
}
|
|
16
17
|
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Scheduler due routing — on a `due` event, runs the job's prompt as a turn in the session
|
|
3
|
+
* it was scheduled from (the newest match for the job's origin tags). A telegram-tagged
|
|
4
|
+
* origin means telegram's own `agent_end` ships the reply back to that chat; no
|
|
5
|
+
* scheduler-side channel handling. If no session matches (the origin was pruned), a fresh
|
|
6
|
+
* one is created carrying the SAME origin tags so it stays bound to that surface — never
|
|
7
|
+
* an untagged session, which would deliver nowhere.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import scheduler from "./index.ts";
|
|
11
|
+
|
|
12
|
+
export const due = scheduler.on("due", async (job, ctx) => {
|
|
13
|
+
const originTags = job.originTags ?? {};
|
|
14
|
+
const [match] = await ctx.agent.findSessions(originTags);
|
|
15
|
+
const session = match
|
|
16
|
+
? await ctx.agent.openSession(match.id)
|
|
17
|
+
: await ctx.agent.createSession({
|
|
18
|
+
setup: (s) => s.appendTags(originTags),
|
|
19
|
+
});
|
|
20
|
+
// followUp queues cleanly if a turn is in flight.
|
|
21
|
+
await session.sendUserMessage(job.prompt, { deliverAs: "followUp" });
|
|
22
|
+
});
|
|
@@ -33,13 +33,13 @@ response.
|
|
|
33
33
|
|
|
34
34
|
## Commands
|
|
35
35
|
|
|
36
|
-
These commands are registered as the bot's command menu
|
|
37
|
-
|
|
36
|
+
These commands are registered as the bot's command menu at producer startup and are
|
|
37
|
+
handled locally by the inbound routing workflow, not forwarded to the model:
|
|
38
38
|
|
|
39
39
|
| Command | Action |
|
|
40
40
|
|---------|--------|
|
|
41
41
|
| `/new` | Start a fresh session for this chat. The new session becomes the active one; the previous session stays addressable but new messages route to the new one. |
|
|
42
|
-
| `/status` | Show the current session name and
|
|
42
|
+
| `/status` | Show the current session name and its current model. |
|
|
43
43
|
| `/help` | List the available commands. |
|
|
44
44
|
|
|
45
45
|
Any other `/command` returns `Unknown command: /<name>. Try /help.`
|