@sentry/junior-scheduler 0.89.0 → 0.91.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +198 -158
- package/dist/schedule-tools.d.ts +6 -19
- package/dist/tool-support.d.ts +275 -0
- package/dist/tools/create-task.d.ts +54 -0
- package/dist/tools/delete-task.d.ts +49 -0
- package/dist/tools/list-tasks.d.ts +48 -0
- package/dist/tools/run-task-now.d.ts +49 -0
- package/dist/tools/update-task.d.ts +55 -0
- package/package.json +2 -3
- package/src/plugin.ts +2 -2
- package/src/schedule-tools.ts +6 -692
- package/src/tool-support.ts +471 -0
- package/src/tools/create-task.ts +121 -0
- package/src/tools/delete-task.ts +48 -0
- package/src/tools/list-tasks.ts +42 -0
- package/src/tools/run-task-now.ts +53 -0
- package/src/tools/update-task.ts +121 -0
package/src/schedule-tools.ts
CHANGED
|
@@ -1,692 +1,6 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
type PluginCredentialSubject,
|
|
8
|
-
type PluginToolDefinition,
|
|
9
|
-
type SlackDestination,
|
|
10
|
-
type SlackRequester,
|
|
11
|
-
type SlackSource,
|
|
12
|
-
} from "@sentry/junior-plugin-api";
|
|
13
|
-
import { buildCalendarRecurrence, parseScheduleTimestamp } from "./cadence";
|
|
14
|
-
import { sanitizeScheduledTaskPrincipal } from "./identity";
|
|
15
|
-
import { type SchedulerStore } from "./store";
|
|
16
|
-
import { SCHEDULED_TASK_SYSTEM_ACTOR } from "./types";
|
|
17
|
-
import type {
|
|
18
|
-
ScheduledCalendarFrequency,
|
|
19
|
-
ScheduledTask,
|
|
20
|
-
ScheduledTaskConversationAccess,
|
|
21
|
-
ScheduledTaskPrincipal,
|
|
22
|
-
ScheduledTaskRecurrence,
|
|
23
|
-
ScheduledTaskStatus,
|
|
24
|
-
} from "./types";
|
|
25
|
-
|
|
26
|
-
export interface SchedulerToolContext {
|
|
27
|
-
credentialSubject?: PluginCredentialSubject;
|
|
28
|
-
requester?: SlackRequester;
|
|
29
|
-
source?: SlackSource;
|
|
30
|
-
store: SchedulerStore;
|
|
31
|
-
userText?: string;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
const TASK_ID_PREFIX = "sched";
|
|
35
|
-
const MAX_LISTED_TASKS = 50;
|
|
36
|
-
const DEFAULT_SCHEDULE_TIMEZONE = "America/Los_Angeles";
|
|
37
|
-
|
|
38
|
-
type SchemaIssue = {
|
|
39
|
-
code: string;
|
|
40
|
-
path: readonly PropertyKey[];
|
|
41
|
-
};
|
|
42
|
-
|
|
43
|
-
function throwToolInputError(error: string): never {
|
|
44
|
-
throw new PluginToolInputError(error);
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
function requireActiveConversation(
|
|
48
|
-
context: SchedulerToolContext,
|
|
49
|
-
): SlackDestination {
|
|
50
|
-
const parsed = sourceSchema.safeParse(context.source);
|
|
51
|
-
if (!parsed.success) {
|
|
52
|
-
const source = context.source as Partial<SlackSource> | undefined;
|
|
53
|
-
const issues = parsed.error.issues as readonly SchemaIssue[];
|
|
54
|
-
if (!source || source.platform !== "slack") {
|
|
55
|
-
throwToolInputError("No active Slack conversation is available.");
|
|
56
|
-
}
|
|
57
|
-
if (issues.some((issue) => issue.code === "unrecognized_keys")) {
|
|
58
|
-
throwToolInputError(
|
|
59
|
-
"Active Slack conversation must not include unknown fields.",
|
|
60
|
-
);
|
|
61
|
-
}
|
|
62
|
-
if (issues.some((issue) => issue.path[0] === "channelId")) {
|
|
63
|
-
throwToolInputError("Active Slack conversation channel is invalid.");
|
|
64
|
-
}
|
|
65
|
-
if (issues.some((issue) => issue.path[0] === "teamId")) {
|
|
66
|
-
throwToolInputError("Active Slack conversation workspace is invalid.");
|
|
67
|
-
}
|
|
68
|
-
throwToolInputError("No active Slack conversation is available.");
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
if (parsed.data.platform !== "slack") {
|
|
72
|
-
throwToolInputError("No active Slack conversation is available.");
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
return {
|
|
76
|
-
platform: "slack",
|
|
77
|
-
teamId: parsed.data.teamId,
|
|
78
|
-
channelId: parsed.data.channelId,
|
|
79
|
-
};
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
function requireRequester(
|
|
83
|
-
context: SchedulerToolContext,
|
|
84
|
-
): ScheduledTaskPrincipal {
|
|
85
|
-
if (context.requester?.platform !== "slack") {
|
|
86
|
-
throwToolInputError("No active Slack requester context is available.");
|
|
87
|
-
}
|
|
88
|
-
const userId = context.requester?.userId?.trim();
|
|
89
|
-
if (!userId || userId.toLowerCase() === "unknown") {
|
|
90
|
-
throwToolInputError("No active Slack requester context is available.");
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
return sanitizeScheduledTaskPrincipal({
|
|
94
|
-
slackUserId: userId,
|
|
95
|
-
...(context.requester?.userName
|
|
96
|
-
? { userName: context.requester.userName }
|
|
97
|
-
: {}),
|
|
98
|
-
...(context.requester?.fullName
|
|
99
|
-
? { fullName: context.requester.fullName }
|
|
100
|
-
: {}),
|
|
101
|
-
});
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
function tool<TInput = any>(
|
|
105
|
-
definition: PluginToolDefinition<TInput>,
|
|
106
|
-
): PluginToolDefinition<TInput> {
|
|
107
|
-
return definition;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
function isDmChannel(channelId: string): boolean {
|
|
111
|
-
return channelId.startsWith("D");
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
function getConversationAccess(
|
|
115
|
-
destination: SlackDestination,
|
|
116
|
-
): ScheduledTaskConversationAccess {
|
|
117
|
-
if (isDmChannel(destination.channelId)) {
|
|
118
|
-
return { audience: "direct", visibility: "private" };
|
|
119
|
-
}
|
|
120
|
-
if (destination.channelId.startsWith("G")) {
|
|
121
|
-
return { audience: "group", visibility: "private" };
|
|
122
|
-
}
|
|
123
|
-
if (destination.channelId.startsWith("C")) {
|
|
124
|
-
return { audience: "channel", visibility: "unknown" };
|
|
125
|
-
}
|
|
126
|
-
return { audience: "channel", visibility: "unknown" };
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
function getCredentialSubject(args: {
|
|
130
|
-
access: ScheduledTaskConversationAccess;
|
|
131
|
-
subject: PluginCredentialSubject | undefined;
|
|
132
|
-
}): PluginCredentialSubject | undefined {
|
|
133
|
-
if (
|
|
134
|
-
args.access.audience !== "direct" ||
|
|
135
|
-
args.access.visibility !== "private"
|
|
136
|
-
) {
|
|
137
|
-
return undefined;
|
|
138
|
-
}
|
|
139
|
-
if (!args.subject) {
|
|
140
|
-
return undefined;
|
|
141
|
-
}
|
|
142
|
-
const subject = pluginCredentialSubjectSchema.safeParse(args.subject);
|
|
143
|
-
if (!subject.success) {
|
|
144
|
-
throwToolInputError("Active Slack credential subject is invalid.");
|
|
145
|
-
}
|
|
146
|
-
return {
|
|
147
|
-
type: subject.data.type,
|
|
148
|
-
userId: subject.data.userId,
|
|
149
|
-
allowedWhen: subject.data.allowedWhen,
|
|
150
|
-
};
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
function sameDestination(
|
|
154
|
-
task: ScheduledTask,
|
|
155
|
-
destination: SlackDestination,
|
|
156
|
-
): boolean {
|
|
157
|
-
const taskDestination = task.destination;
|
|
158
|
-
return (
|
|
159
|
-
taskDestination.platform === "slack" &&
|
|
160
|
-
taskDestination.teamId === destination.teamId &&
|
|
161
|
-
taskDestination.channelId === destination.channelId
|
|
162
|
-
);
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
async function getWritableTask(args: {
|
|
166
|
-
context: SchedulerToolContext;
|
|
167
|
-
taskId: string;
|
|
168
|
-
}): Promise<ScheduledTask> {
|
|
169
|
-
const destination = requireActiveConversation(args.context);
|
|
170
|
-
|
|
171
|
-
const task = await schedulerStore(args.context).getTask(args.taskId);
|
|
172
|
-
if (!task || task.status === "deleted") {
|
|
173
|
-
throwToolInputError(
|
|
174
|
-
"Scheduled task was not found in the active Slack conversation.",
|
|
175
|
-
);
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
if (!sameDestination(task, destination)) {
|
|
179
|
-
throwToolInputError(
|
|
180
|
-
"Scheduled task can only be managed from the Slack destination where it was created.",
|
|
181
|
-
);
|
|
182
|
-
}
|
|
183
|
-
return task;
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
function compactTask(task: ScheduledTask): Record<string, unknown> {
|
|
187
|
-
return {
|
|
188
|
-
id: task.id,
|
|
189
|
-
status: task.status,
|
|
190
|
-
task: task.task.text,
|
|
191
|
-
schedule: task.schedule.description,
|
|
192
|
-
timezone: task.schedule.timezone,
|
|
193
|
-
recurrence: task.schedule.recurrence
|
|
194
|
-
? {
|
|
195
|
-
frequency: task.schedule.recurrence.frequency,
|
|
196
|
-
interval: task.schedule.recurrence.interval,
|
|
197
|
-
start_date: task.schedule.recurrence.startDate,
|
|
198
|
-
time: task.schedule.recurrence.time,
|
|
199
|
-
weekdays: task.schedule.recurrence.weekdays,
|
|
200
|
-
month: task.schedule.recurrence.month,
|
|
201
|
-
day_of_month: task.schedule.recurrence.dayOfMonth,
|
|
202
|
-
}
|
|
203
|
-
: null,
|
|
204
|
-
next_run_at: task.nextRunAtMs
|
|
205
|
-
? new Date(task.nextRunAtMs).toISOString()
|
|
206
|
-
: null,
|
|
207
|
-
conversation_access: task.conversationAccess ?? null,
|
|
208
|
-
credential_subject: task.credentialSubject
|
|
209
|
-
? {
|
|
210
|
-
type: task.credentialSubject.type,
|
|
211
|
-
allowed_when: task.credentialSubject.allowedWhen,
|
|
212
|
-
}
|
|
213
|
-
: null,
|
|
214
|
-
last_run_at: task.lastRunAtMs
|
|
215
|
-
? new Date(task.lastRunAtMs).toISOString()
|
|
216
|
-
: null,
|
|
217
|
-
run_now_at: task.runNowAtMs
|
|
218
|
-
? new Date(task.runNowAtMs).toISOString()
|
|
219
|
-
: null,
|
|
220
|
-
};
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
function buildTaskId(): string {
|
|
224
|
-
return `${TASK_ID_PREFIX}_${randomUUID()}`;
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
function schedulerStore(context: SchedulerToolContext): SchedulerStore {
|
|
228
|
-
return context.store;
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
function normalizeStatus(
|
|
232
|
-
value: string | undefined,
|
|
233
|
-
): ScheduledTaskStatus | undefined {
|
|
234
|
-
if (value === "active" || value === "paused" || value === "blocked") {
|
|
235
|
-
return value;
|
|
236
|
-
}
|
|
237
|
-
return undefined;
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
function normalizeFrequency(
|
|
241
|
-
value: unknown,
|
|
242
|
-
): ScheduledCalendarFrequency | undefined {
|
|
243
|
-
if (
|
|
244
|
-
value === "daily" ||
|
|
245
|
-
value === "weekly" ||
|
|
246
|
-
value === "monthly" ||
|
|
247
|
-
value === "yearly"
|
|
248
|
-
) {
|
|
249
|
-
return value;
|
|
250
|
-
}
|
|
251
|
-
return undefined;
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
function buildRecurrence(args: {
|
|
255
|
-
existing?: ScheduledTaskRecurrence;
|
|
256
|
-
input: {
|
|
257
|
-
recurrence?: unknown;
|
|
258
|
-
};
|
|
259
|
-
nextRunAtMs: number | undefined;
|
|
260
|
-
timezone: string;
|
|
261
|
-
}): ScheduledTaskRecurrence | undefined {
|
|
262
|
-
if (args.input.recurrence === null) {
|
|
263
|
-
return undefined;
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
const frequency =
|
|
267
|
-
normalizeFrequency(args.input.recurrence) ?? args.existing?.frequency;
|
|
268
|
-
if (!frequency) {
|
|
269
|
-
return undefined;
|
|
270
|
-
}
|
|
271
|
-
if (!args.nextRunAtMs) {
|
|
272
|
-
throwToolInputError("Recurring scheduled tasks require next_run_at.");
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
try {
|
|
276
|
-
return buildCalendarRecurrence({
|
|
277
|
-
frequency,
|
|
278
|
-
interval: args.existing?.interval,
|
|
279
|
-
nextRunAtMs: args.nextRunAtMs,
|
|
280
|
-
timezone: args.timezone,
|
|
281
|
-
weekdays: frequency === "weekly" ? args.existing?.weekdays : undefined,
|
|
282
|
-
});
|
|
283
|
-
} catch (error) {
|
|
284
|
-
throwToolInputError(
|
|
285
|
-
error instanceof RangeError
|
|
286
|
-
? "timezone must be a valid IANA time zone."
|
|
287
|
-
: error instanceof Error
|
|
288
|
-
? error.message
|
|
289
|
-
: String(error),
|
|
290
|
-
);
|
|
291
|
-
}
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
function validateRecurringFrequencyLimit(input: { recurrence?: unknown }) {
|
|
295
|
-
if (
|
|
296
|
-
input.recurrence !== undefined &&
|
|
297
|
-
input.recurrence !== null &&
|
|
298
|
-
!normalizeFrequency(input.recurrence)
|
|
299
|
-
) {
|
|
300
|
-
throwToolInputError(
|
|
301
|
-
"Recurring scheduled tasks can run at most once per day.",
|
|
302
|
-
);
|
|
303
|
-
}
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
function validateCreateScheduleKind(input: {
|
|
307
|
-
recurrence?: unknown;
|
|
308
|
-
schedule_kind?: unknown;
|
|
309
|
-
}) {
|
|
310
|
-
if (input.schedule_kind === undefined) {
|
|
311
|
-
throwToolInputError("Provide schedule_kind as one_off or recurring.");
|
|
312
|
-
}
|
|
313
|
-
if (
|
|
314
|
-
input.schedule_kind !== "one_off" &&
|
|
315
|
-
input.schedule_kind !== "recurring"
|
|
316
|
-
) {
|
|
317
|
-
throwToolInputError("schedule_kind must be one_off or recurring.");
|
|
318
|
-
}
|
|
319
|
-
if (input.schedule_kind === "one_off" && input.recurrence !== undefined) {
|
|
320
|
-
throwToolInputError("Omit recurrence when schedule_kind is one_off.");
|
|
321
|
-
}
|
|
322
|
-
if (
|
|
323
|
-
input.schedule_kind === "recurring" &&
|
|
324
|
-
(input.recurrence === undefined || input.recurrence === null)
|
|
325
|
-
) {
|
|
326
|
-
throwToolInputError("Provide recurrence when schedule_kind is recurring.");
|
|
327
|
-
}
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
function shouldRebuildRecurrence(input: {
|
|
331
|
-
next_run_at?: string;
|
|
332
|
-
recurrence?: unknown;
|
|
333
|
-
timezone?: string;
|
|
334
|
-
}): boolean {
|
|
335
|
-
return (
|
|
336
|
-
input.next_run_at !== undefined ||
|
|
337
|
-
input.recurrence !== undefined ||
|
|
338
|
-
input.timezone !== undefined
|
|
339
|
-
);
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
function getDefaultScheduleTimezone(): string {
|
|
343
|
-
return process.env.JUNIOR_TIMEZONE?.trim() || DEFAULT_SCHEDULE_TIMEZONE;
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
function isValidTimeZone(timezone: string): boolean {
|
|
347
|
-
try {
|
|
348
|
-
new Intl.DateTimeFormat("en-US", { timeZone: timezone }).format();
|
|
349
|
-
return true;
|
|
350
|
-
} catch {
|
|
351
|
-
return false;
|
|
352
|
-
}
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
function parseNextRunAtMs(
|
|
356
|
-
nextRunAtIso: string | undefined,
|
|
357
|
-
): number | undefined {
|
|
358
|
-
try {
|
|
359
|
-
if (nextRunAtIso) {
|
|
360
|
-
return parseScheduleTimestamp(nextRunAtIso);
|
|
361
|
-
}
|
|
362
|
-
} catch {
|
|
363
|
-
return undefined;
|
|
364
|
-
}
|
|
365
|
-
return undefined;
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
/** Create a tool that stores a scheduled task for the active Slack context. */
|
|
369
|
-
export function createSlackScheduleCreateTaskTool(
|
|
370
|
-
context: SchedulerToolContext,
|
|
371
|
-
) {
|
|
372
|
-
return tool({
|
|
373
|
-
description:
|
|
374
|
-
"Create a one-time or recurring Junior task in the active Slack conversation. For one-time reminders or one-time scheduled work, omit recurrence entirely; never choose a default recurrence. Use only when the user explicitly asks Junior to do work later or on a recurring cadence. Only manage tasks for the active Slack DM or channel; never target threads, other channels, or another user's DM. When the task, schedule, and destination are clear, create it without asking for confirmation; ask only when one of those is ambiguous.",
|
|
375
|
-
executionMode: "sequential",
|
|
376
|
-
inputSchema: Type.Object({
|
|
377
|
-
task: Type.String({ minLength: 1, maxLength: 4000 }),
|
|
378
|
-
schedule: Type.String({ minLength: 1, maxLength: 300 }),
|
|
379
|
-
schedule_kind: Type.Union(
|
|
380
|
-
[Type.Literal("one_off"), Type.Literal("recurring")],
|
|
381
|
-
{
|
|
382
|
-
description:
|
|
383
|
-
"Required schedule classification. Use one_off for one-time reminders or one-time scheduled work. Use recurring only when the user explicitly asks for a repeating schedule.",
|
|
384
|
-
},
|
|
385
|
-
),
|
|
386
|
-
timezone: Type.Optional(
|
|
387
|
-
Type.String({
|
|
388
|
-
minLength: 1,
|
|
389
|
-
maxLength: 80,
|
|
390
|
-
description:
|
|
391
|
-
"IANA timezone, e.g. 'America/Los_Angeles'. Defaults to the channel's configured timezone.",
|
|
392
|
-
}),
|
|
393
|
-
),
|
|
394
|
-
next_run_at: Type.Optional(
|
|
395
|
-
Type.String({
|
|
396
|
-
minLength: 1,
|
|
397
|
-
description:
|
|
398
|
-
"Exact next run time as an ISO timestamp, computed from the user's requested schedule.",
|
|
399
|
-
}),
|
|
400
|
-
),
|
|
401
|
-
recurrence: Type.Optional(
|
|
402
|
-
Type.Union(
|
|
403
|
-
[
|
|
404
|
-
Type.Literal("daily"),
|
|
405
|
-
Type.Literal("weekly"),
|
|
406
|
-
Type.Literal("monthly"),
|
|
407
|
-
Type.Literal("yearly"),
|
|
408
|
-
],
|
|
409
|
-
{
|
|
410
|
-
description:
|
|
411
|
-
"Required when schedule_kind is recurring. Omit when schedule_kind is one_off. Recurring tasks run at most once per day: use daily, weekly, monthly, or yearly only.",
|
|
412
|
-
},
|
|
413
|
-
),
|
|
414
|
-
),
|
|
415
|
-
}),
|
|
416
|
-
execute: async (input) => {
|
|
417
|
-
const destination = requireActiveConversation(context);
|
|
418
|
-
const requester = requireRequester(context);
|
|
419
|
-
|
|
420
|
-
const nowMs = Date.now();
|
|
421
|
-
const timezone = input.timezone ?? getDefaultScheduleTimezone();
|
|
422
|
-
validateCreateScheduleKind(input);
|
|
423
|
-
validateRecurringFrequencyLimit(input);
|
|
424
|
-
if (!isValidTimeZone(timezone)) {
|
|
425
|
-
throwToolInputError("timezone must be a valid IANA time zone.");
|
|
426
|
-
}
|
|
427
|
-
const nextRunAtMs = parseNextRunAtMs(input.next_run_at);
|
|
428
|
-
if (!nextRunAtMs) {
|
|
429
|
-
throwToolInputError("Provide next_run_at as a valid ISO timestamp.");
|
|
430
|
-
}
|
|
431
|
-
const recurrence = buildRecurrence({
|
|
432
|
-
input,
|
|
433
|
-
nextRunAtMs,
|
|
434
|
-
timezone,
|
|
435
|
-
});
|
|
436
|
-
const conversationAccess = getConversationAccess(destination);
|
|
437
|
-
const credentialSubject = getCredentialSubject({
|
|
438
|
-
access: conversationAccess,
|
|
439
|
-
subject: context.credentialSubject,
|
|
440
|
-
});
|
|
441
|
-
|
|
442
|
-
const task: ScheduledTask = {
|
|
443
|
-
id: buildTaskId(),
|
|
444
|
-
createdAtMs: nowMs,
|
|
445
|
-
updatedAtMs: nowMs,
|
|
446
|
-
createdBy: requester,
|
|
447
|
-
conversationAccess,
|
|
448
|
-
...(credentialSubject ? { credentialSubject } : {}),
|
|
449
|
-
destination,
|
|
450
|
-
executionActor: SCHEDULED_TASK_SYSTEM_ACTOR,
|
|
451
|
-
nextRunAtMs,
|
|
452
|
-
originalRequest: context.userText,
|
|
453
|
-
schedule: {
|
|
454
|
-
description: input.schedule,
|
|
455
|
-
timezone,
|
|
456
|
-
kind: recurrence ? "recurring" : "one_off",
|
|
457
|
-
recurrence,
|
|
458
|
-
},
|
|
459
|
-
status: "active",
|
|
460
|
-
task: {
|
|
461
|
-
text: input.task,
|
|
462
|
-
},
|
|
463
|
-
};
|
|
464
|
-
|
|
465
|
-
await schedulerStore(context).saveTask(task);
|
|
466
|
-
return {
|
|
467
|
-
ok: true,
|
|
468
|
-
task: compactTask(task),
|
|
469
|
-
};
|
|
470
|
-
},
|
|
471
|
-
});
|
|
472
|
-
}
|
|
473
|
-
|
|
474
|
-
/** Create a tool that lists scheduled tasks for the active Slack conversation. */
|
|
475
|
-
export function createSlackScheduleListTasksTool(
|
|
476
|
-
context: SchedulerToolContext,
|
|
477
|
-
) {
|
|
478
|
-
return tool({
|
|
479
|
-
description:
|
|
480
|
-
"List scheduled Junior tasks for the active Slack conversation. Use when the user asks what is scheduled here, or when task IDs are needed before editing, deleting, or running schedules. Only manages tasks for the active Slack DM or channel.",
|
|
481
|
-
annotations: { readOnlyHint: true, destructiveHint: false },
|
|
482
|
-
inputSchema: Type.Object({}),
|
|
483
|
-
execute: async () => {
|
|
484
|
-
const destination = requireActiveConversation(context);
|
|
485
|
-
|
|
486
|
-
const tasks = await schedulerStore(context).listTasksForTeam(
|
|
487
|
-
destination.teamId,
|
|
488
|
-
);
|
|
489
|
-
const matching = tasks.filter((task) =>
|
|
490
|
-
sameDestination(task, destination),
|
|
491
|
-
);
|
|
492
|
-
const visible = matching.slice(0, MAX_LISTED_TASKS).map(compactTask);
|
|
493
|
-
|
|
494
|
-
return {
|
|
495
|
-
ok: true,
|
|
496
|
-
tasks: visible,
|
|
497
|
-
truncated: matching.length > visible.length,
|
|
498
|
-
};
|
|
499
|
-
},
|
|
500
|
-
});
|
|
501
|
-
}
|
|
502
|
-
|
|
503
|
-
/** Create a tool that edits a scheduled task in the active Slack conversation. */
|
|
504
|
-
export function createSlackScheduleUpdateTaskTool(
|
|
505
|
-
context: SchedulerToolContext,
|
|
506
|
-
) {
|
|
507
|
-
return tool({
|
|
508
|
-
description:
|
|
509
|
-
"Edit, pause, resume, or reschedule an existing Junior scheduled task in the active Slack conversation. Use only task IDs returned for this conversation. Do not move scheduled tasks across conversations.",
|
|
510
|
-
executionMode: "sequential",
|
|
511
|
-
inputSchema: Type.Object({
|
|
512
|
-
task_id: Type.String({
|
|
513
|
-
minLength: 1,
|
|
514
|
-
description:
|
|
515
|
-
"ID of the task to update. Must be from this active Slack conversation.",
|
|
516
|
-
}),
|
|
517
|
-
task: Type.Optional(Type.String({ minLength: 1, maxLength: 4000 })),
|
|
518
|
-
schedule: Type.Optional(Type.String({ minLength: 1, maxLength: 300 })),
|
|
519
|
-
timezone: Type.Optional(Type.String({ minLength: 1, maxLength: 80 })),
|
|
520
|
-
next_run_at: Type.Optional(
|
|
521
|
-
Type.String({
|
|
522
|
-
minLength: 1,
|
|
523
|
-
description: "Exact ISO timestamp when changing the next run time.",
|
|
524
|
-
}),
|
|
525
|
-
),
|
|
526
|
-
recurrence: Type.Optional(
|
|
527
|
-
Type.Union(
|
|
528
|
-
[
|
|
529
|
-
Type.Literal("daily"),
|
|
530
|
-
Type.Literal("weekly"),
|
|
531
|
-
Type.Literal("monthly"),
|
|
532
|
-
Type.Literal("yearly"),
|
|
533
|
-
Type.Null(),
|
|
534
|
-
],
|
|
535
|
-
{
|
|
536
|
-
description:
|
|
537
|
-
"Provide only for repeating schedules. Omit for one-time requests. Set to null to convert a recurring task to one-time.",
|
|
538
|
-
},
|
|
539
|
-
),
|
|
540
|
-
),
|
|
541
|
-
status: Type.Optional(
|
|
542
|
-
Type.Union(
|
|
543
|
-
[
|
|
544
|
-
Type.Literal("active"),
|
|
545
|
-
Type.Literal("paused"),
|
|
546
|
-
Type.Literal("blocked"),
|
|
547
|
-
],
|
|
548
|
-
{
|
|
549
|
-
description:
|
|
550
|
-
"Set to active, paused, or blocked to resume, pause, or block the task.",
|
|
551
|
-
},
|
|
552
|
-
),
|
|
553
|
-
),
|
|
554
|
-
}),
|
|
555
|
-
execute: async (input) => {
|
|
556
|
-
const lookup = await getWritableTask({
|
|
557
|
-
context,
|
|
558
|
-
taskId: input.task_id,
|
|
559
|
-
});
|
|
560
|
-
|
|
561
|
-
const timezone = input.timezone ?? lookup.schedule.timezone;
|
|
562
|
-
validateRecurringFrequencyLimit(input);
|
|
563
|
-
if (!isValidTimeZone(timezone)) {
|
|
564
|
-
throwToolInputError("timezone must be a valid IANA time zone.");
|
|
565
|
-
}
|
|
566
|
-
const parsedNextRunAtMs = parseNextRunAtMs(input.next_run_at);
|
|
567
|
-
const nextRunAtMs = input.next_run_at
|
|
568
|
-
? parsedNextRunAtMs
|
|
569
|
-
: lookup.nextRunAtMs;
|
|
570
|
-
if (input.next_run_at && !nextRunAtMs) {
|
|
571
|
-
throwToolInputError("Provide next_run_at as a valid ISO timestamp.");
|
|
572
|
-
}
|
|
573
|
-
|
|
574
|
-
const status = normalizeStatus(input.status);
|
|
575
|
-
if (input.status && !status) {
|
|
576
|
-
throwToolInputError("status must be active, paused, or blocked.");
|
|
577
|
-
}
|
|
578
|
-
if (status === "active" && !nextRunAtMs) {
|
|
579
|
-
throwToolInputError(
|
|
580
|
-
"Active scheduled tasks require next_run_at when no next run is stored.",
|
|
581
|
-
);
|
|
582
|
-
}
|
|
583
|
-
const recurrence = shouldRebuildRecurrence(input)
|
|
584
|
-
? buildRecurrence({
|
|
585
|
-
existing: lookup.schedule.recurrence,
|
|
586
|
-
input,
|
|
587
|
-
nextRunAtMs,
|
|
588
|
-
timezone,
|
|
589
|
-
})
|
|
590
|
-
: lookup.schedule.recurrence;
|
|
591
|
-
const nextStatus = status ?? lookup.status;
|
|
592
|
-
|
|
593
|
-
const next: ScheduledTask = {
|
|
594
|
-
...lookup,
|
|
595
|
-
updatedAtMs: Date.now(),
|
|
596
|
-
nextRunAtMs,
|
|
597
|
-
runNowAtMs: nextStatus === "active" ? lookup.runNowAtMs : undefined,
|
|
598
|
-
status: nextStatus,
|
|
599
|
-
statusReason:
|
|
600
|
-
nextStatus === "blocked" ? lookup.statusReason : undefined,
|
|
601
|
-
schedule: {
|
|
602
|
-
...lookup.schedule,
|
|
603
|
-
description: input.schedule ?? lookup.schedule.description,
|
|
604
|
-
timezone,
|
|
605
|
-
kind: recurrence ? "recurring" : "one_off",
|
|
606
|
-
recurrence,
|
|
607
|
-
},
|
|
608
|
-
task: input.task ? { text: input.task } : lookup.task,
|
|
609
|
-
};
|
|
610
|
-
|
|
611
|
-
await schedulerStore(context).saveTask(next);
|
|
612
|
-
return {
|
|
613
|
-
ok: true,
|
|
614
|
-
task: compactTask(next),
|
|
615
|
-
};
|
|
616
|
-
},
|
|
617
|
-
});
|
|
618
|
-
}
|
|
619
|
-
|
|
620
|
-
/** Create a tool that removes a scheduled task from the active Slack conversation. */
|
|
621
|
-
export function createSlackScheduleDeleteTaskTool(
|
|
622
|
-
context: SchedulerToolContext,
|
|
623
|
-
) {
|
|
624
|
-
return tool({
|
|
625
|
-
description:
|
|
626
|
-
"Delete one scheduled Junior task from the active Slack conversation. Use only task IDs returned for this conversation. Do not delete schedules from threads, other channels, or another user's DM.",
|
|
627
|
-
executionMode: "sequential",
|
|
628
|
-
inputSchema: Type.Object({
|
|
629
|
-
task_id: Type.String({
|
|
630
|
-
minLength: 1,
|
|
631
|
-
description:
|
|
632
|
-
"ID of the task to delete. Must be from this active Slack conversation.",
|
|
633
|
-
}),
|
|
634
|
-
}),
|
|
635
|
-
execute: async ({ task_id }) => {
|
|
636
|
-
const lookup = await getWritableTask({ context, taskId: task_id });
|
|
637
|
-
|
|
638
|
-
const next: ScheduledTask = {
|
|
639
|
-
...lookup,
|
|
640
|
-
updatedAtMs: Date.now(),
|
|
641
|
-
status: "deleted",
|
|
642
|
-
nextRunAtMs: undefined,
|
|
643
|
-
runNowAtMs: undefined,
|
|
644
|
-
};
|
|
645
|
-
|
|
646
|
-
await schedulerStore(context).saveTask(next);
|
|
647
|
-
return {
|
|
648
|
-
ok: true,
|
|
649
|
-
task: compactTask(next),
|
|
650
|
-
};
|
|
651
|
-
},
|
|
652
|
-
});
|
|
653
|
-
}
|
|
654
|
-
|
|
655
|
-
/** Create a tool that marks an existing scheduled task due immediately. */
|
|
656
|
-
export function createSlackScheduleRunTaskNowTool(
|
|
657
|
-
context: SchedulerToolContext,
|
|
658
|
-
) {
|
|
659
|
-
return tool({
|
|
660
|
-
description:
|
|
661
|
-
"Queue an existing active scheduled Junior task to run as soon as possible, without changing its cadence. Use when the user asks to run an existing scheduled task now. Use only task IDs returned for this conversation.",
|
|
662
|
-
executionMode: "sequential",
|
|
663
|
-
inputSchema: Type.Object({
|
|
664
|
-
task_id: Type.String({
|
|
665
|
-
minLength: 1,
|
|
666
|
-
description:
|
|
667
|
-
"ID of the active task to run now. Must be from this active Slack conversation.",
|
|
668
|
-
}),
|
|
669
|
-
}),
|
|
670
|
-
execute: async ({ task_id }) => {
|
|
671
|
-
const lookup = await getWritableTask({ context, taskId: task_id });
|
|
672
|
-
if (lookup.status !== "active") {
|
|
673
|
-
throwToolInputError(
|
|
674
|
-
"Scheduled task must be active before it can be run now. Resume the task first if you want it to run.",
|
|
675
|
-
);
|
|
676
|
-
}
|
|
677
|
-
|
|
678
|
-
const nowMs = Date.now();
|
|
679
|
-
const next: ScheduledTask = {
|
|
680
|
-
...lookup,
|
|
681
|
-
updatedAtMs: nowMs,
|
|
682
|
-
runNowAtMs: nowMs,
|
|
683
|
-
};
|
|
684
|
-
|
|
685
|
-
await schedulerStore(context).saveTask(next);
|
|
686
|
-
return {
|
|
687
|
-
ok: true,
|
|
688
|
-
task: compactTask(next),
|
|
689
|
-
};
|
|
690
|
-
},
|
|
691
|
-
});
|
|
692
|
-
}
|
|
1
|
+
export { createSlackScheduleCreateTaskTool } from "./tools/create-task";
|
|
2
|
+
export { createSlackScheduleDeleteTaskTool } from "./tools/delete-task";
|
|
3
|
+
export { createSlackScheduleListTasksTool } from "./tools/list-tasks";
|
|
4
|
+
export { createSlackScheduleRunTaskNowTool } from "./tools/run-task-now";
|
|
5
|
+
export { createSlackScheduleUpdateTaskTool } from "./tools/update-task";
|
|
6
|
+
export type { SchedulerToolContext } from "./tool-support";
|