@sentry/junior-scheduler 0.74.1 → 0.76.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/db/schema.d.ts +223 -0
- package/dist/index.d.ts +1 -2
- package/dist/index.js +624 -455
- package/dist/plugin.d.ts +2 -2
- package/dist/schedule-tools.d.ts +10 -9
- package/dist/store.d.ts +21 -3
- package/dist/types.d.ts +2 -5
- package/migrations/0001_scheduler.sql +35 -0
- package/package.json +6 -2
- package/src/db/schema.ts +53 -0
- package/src/index.ts +6 -2
- package/src/plugin.ts +89 -30
- package/src/schedule-tools.ts +68 -36
- package/src/store.ts +842 -47
- package/src/types.ts +2 -5
- package/dist/prompt.d.ts +0 -7
- package/src/prompt.ts +0 -90
package/src/schedule-tools.ts
CHANGED
|
@@ -1,19 +1,18 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import { Type } from "@sinclair/typebox";
|
|
3
3
|
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
type
|
|
9
|
-
type AgentPluginState,
|
|
10
|
-
type AgentPluginToolDefinition,
|
|
4
|
+
PluginToolInputError,
|
|
5
|
+
pluginCredentialSubjectSchema,
|
|
6
|
+
sourceSchema,
|
|
7
|
+
type PluginCredentialSubject,
|
|
8
|
+
type PluginToolDefinition,
|
|
11
9
|
type SlackDestination,
|
|
12
10
|
type SlackRequester,
|
|
11
|
+
type SlackSource,
|
|
13
12
|
} from "@sentry/junior-plugin-api";
|
|
14
13
|
import { buildCalendarRecurrence, parseScheduleTimestamp } from "./cadence";
|
|
15
14
|
import { sanitizeScheduledTaskPrincipal } from "./identity";
|
|
16
|
-
import {
|
|
15
|
+
import { type SchedulerStore } from "./store";
|
|
17
16
|
import { SCHEDULED_TASK_SYSTEM_ACTOR } from "./types";
|
|
18
17
|
import type {
|
|
19
18
|
ScheduledCalendarFrequency,
|
|
@@ -25,10 +24,10 @@ import type {
|
|
|
25
24
|
} from "./types";
|
|
26
25
|
|
|
27
26
|
export interface SchedulerToolContext {
|
|
28
|
-
credentialSubject?:
|
|
27
|
+
credentialSubject?: PluginCredentialSubject;
|
|
29
28
|
requester?: SlackRequester;
|
|
30
|
-
source?:
|
|
31
|
-
|
|
29
|
+
source?: SlackSource;
|
|
30
|
+
store: SchedulerStore;
|
|
32
31
|
userText?: string;
|
|
33
32
|
}
|
|
34
33
|
|
|
@@ -42,15 +41,15 @@ type SchemaIssue = {
|
|
|
42
41
|
};
|
|
43
42
|
|
|
44
43
|
function throwToolInputError(error: string): never {
|
|
45
|
-
throw new
|
|
44
|
+
throw new PluginToolInputError(error);
|
|
46
45
|
}
|
|
47
46
|
|
|
48
47
|
function requireActiveConversation(
|
|
49
48
|
context: SchedulerToolContext,
|
|
50
49
|
): SlackDestination {
|
|
51
|
-
const parsed =
|
|
50
|
+
const parsed = sourceSchema.safeParse(context.source);
|
|
52
51
|
if (!parsed.success) {
|
|
53
|
-
const source = context.source as Partial<
|
|
52
|
+
const source = context.source as Partial<SlackSource> | undefined;
|
|
54
53
|
const issues = parsed.error.issues as readonly SchemaIssue[];
|
|
55
54
|
if (!source || source.platform !== "slack") {
|
|
56
55
|
throwToolInputError("No active Slack conversation is available.");
|
|
@@ -69,11 +68,15 @@ function requireActiveConversation(
|
|
|
69
68
|
throwToolInputError("No active Slack conversation is available.");
|
|
70
69
|
}
|
|
71
70
|
|
|
72
|
-
if (
|
|
71
|
+
if (parsed.data.platform !== "slack") {
|
|
73
72
|
throwToolInputError("No active Slack conversation is available.");
|
|
74
73
|
}
|
|
75
74
|
|
|
76
|
-
return
|
|
75
|
+
return {
|
|
76
|
+
platform: "slack",
|
|
77
|
+
teamId: parsed.data.teamId,
|
|
78
|
+
channelId: parsed.data.channelId,
|
|
79
|
+
};
|
|
77
80
|
}
|
|
78
81
|
|
|
79
82
|
function requireRequester(
|
|
@@ -99,8 +102,8 @@ function requireRequester(
|
|
|
99
102
|
}
|
|
100
103
|
|
|
101
104
|
function tool<TInput = any>(
|
|
102
|
-
definition:
|
|
103
|
-
):
|
|
105
|
+
definition: PluginToolDefinition<TInput>,
|
|
106
|
+
): PluginToolDefinition<TInput> {
|
|
104
107
|
return definition;
|
|
105
108
|
}
|
|
106
109
|
|
|
@@ -125,8 +128,8 @@ function getConversationAccess(
|
|
|
125
128
|
|
|
126
129
|
function getCredentialSubject(args: {
|
|
127
130
|
access: ScheduledTaskConversationAccess;
|
|
128
|
-
subject:
|
|
129
|
-
}):
|
|
131
|
+
subject: PluginCredentialSubject | undefined;
|
|
132
|
+
}): PluginCredentialSubject | undefined {
|
|
130
133
|
if (
|
|
131
134
|
args.access.audience !== "direct" ||
|
|
132
135
|
args.access.visibility !== "private"
|
|
@@ -136,7 +139,7 @@ function getCredentialSubject(args: {
|
|
|
136
139
|
if (!args.subject) {
|
|
137
140
|
return undefined;
|
|
138
141
|
}
|
|
139
|
-
const subject =
|
|
142
|
+
const subject = pluginCredentialSubjectSchema.safeParse(args.subject);
|
|
140
143
|
if (!subject.success) {
|
|
141
144
|
throwToolInputError("Active Slack credential subject is invalid.");
|
|
142
145
|
}
|
|
@@ -165,9 +168,7 @@ async function getWritableTask(args: {
|
|
|
165
168
|
}): Promise<ScheduledTask> {
|
|
166
169
|
const destination = requireActiveConversation(args.context);
|
|
167
170
|
|
|
168
|
-
const task = await
|
|
169
|
-
args.taskId,
|
|
170
|
-
);
|
|
171
|
+
const task = await schedulerStore(args.context).getTask(args.taskId);
|
|
171
172
|
if (!task || task.status === "deleted") {
|
|
172
173
|
throwToolInputError(
|
|
173
174
|
"Scheduled task was not found in the active Slack conversation.",
|
|
@@ -216,7 +217,6 @@ function compactTask(task: ScheduledTask): Record<string, unknown> {
|
|
|
216
217
|
run_now_at: task.runNowAtMs
|
|
217
218
|
? new Date(task.runNowAtMs).toISOString()
|
|
218
219
|
: null,
|
|
219
|
-
version: task.version,
|
|
220
220
|
};
|
|
221
221
|
}
|
|
222
222
|
|
|
@@ -224,6 +224,10 @@ function buildTaskId(): string {
|
|
|
224
224
|
return `${TASK_ID_PREFIX}_${randomUUID()}`;
|
|
225
225
|
}
|
|
226
226
|
|
|
227
|
+
function schedulerStore(context: SchedulerToolContext): SchedulerStore {
|
|
228
|
+
return context.store;
|
|
229
|
+
}
|
|
230
|
+
|
|
227
231
|
function normalizeStatus(
|
|
228
232
|
value: string | undefined,
|
|
229
233
|
): ScheduledTaskStatus | undefined {
|
|
@@ -299,6 +303,30 @@ function validateRecurringFrequencyLimit(input: { recurrence?: unknown }) {
|
|
|
299
303
|
}
|
|
300
304
|
}
|
|
301
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
|
+
|
|
302
330
|
function shouldRebuildRecurrence(input: {
|
|
303
331
|
next_run_at?: string;
|
|
304
332
|
recurrence?: unknown;
|
|
@@ -343,11 +371,18 @@ export function createSlackScheduleCreateTaskTool(
|
|
|
343
371
|
) {
|
|
344
372
|
return tool({
|
|
345
373
|
description:
|
|
346
|
-
"Create a
|
|
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.",
|
|
347
375
|
executionMode: "sequential",
|
|
348
376
|
inputSchema: Type.Object({
|
|
349
377
|
task: Type.String({ minLength: 1, maxLength: 4000 }),
|
|
350
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
|
+
),
|
|
351
386
|
timezone: Type.Optional(
|
|
352
387
|
Type.String({
|
|
353
388
|
minLength: 1,
|
|
@@ -373,7 +408,7 @@ export function createSlackScheduleCreateTaskTool(
|
|
|
373
408
|
],
|
|
374
409
|
{
|
|
375
410
|
description:
|
|
376
|
-
"
|
|
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.",
|
|
377
412
|
},
|
|
378
413
|
),
|
|
379
414
|
),
|
|
@@ -384,6 +419,7 @@ export function createSlackScheduleCreateTaskTool(
|
|
|
384
419
|
|
|
385
420
|
const nowMs = Date.now();
|
|
386
421
|
const timezone = input.timezone ?? getDefaultScheduleTimezone();
|
|
422
|
+
validateCreateScheduleKind(input);
|
|
387
423
|
validateRecurringFrequencyLimit(input);
|
|
388
424
|
if (!isValidTimeZone(timezone)) {
|
|
389
425
|
throwToolInputError("timezone must be a valid IANA time zone.");
|
|
@@ -424,10 +460,9 @@ export function createSlackScheduleCreateTaskTool(
|
|
|
424
460
|
task: {
|
|
425
461
|
text: input.task,
|
|
426
462
|
},
|
|
427
|
-
version: 1,
|
|
428
463
|
};
|
|
429
464
|
|
|
430
|
-
await
|
|
465
|
+
await schedulerStore(context).saveTask(task);
|
|
431
466
|
return {
|
|
432
467
|
ok: true,
|
|
433
468
|
task: compactTask(task),
|
|
@@ -448,7 +483,7 @@ export function createSlackScheduleListTasksTool(
|
|
|
448
483
|
execute: async () => {
|
|
449
484
|
const destination = requireActiveConversation(context);
|
|
450
485
|
|
|
451
|
-
const tasks = await
|
|
486
|
+
const tasks = await schedulerStore(context).listTasksForTeam(
|
|
452
487
|
destination.teamId,
|
|
453
488
|
);
|
|
454
489
|
const matching = tasks.filter((task) =>
|
|
@@ -571,10 +606,9 @@ export function createSlackScheduleUpdateTaskTool(
|
|
|
571
606
|
recurrence,
|
|
572
607
|
},
|
|
573
608
|
task: input.task ? { text: input.task } : lookup.task,
|
|
574
|
-
version: lookup.version + 1,
|
|
575
609
|
};
|
|
576
610
|
|
|
577
|
-
await
|
|
611
|
+
await schedulerStore(context).saveTask(next);
|
|
578
612
|
return {
|
|
579
613
|
ok: true,
|
|
580
614
|
task: compactTask(next),
|
|
@@ -607,10 +641,9 @@ export function createSlackScheduleDeleteTaskTool(
|
|
|
607
641
|
status: "deleted",
|
|
608
642
|
nextRunAtMs: undefined,
|
|
609
643
|
runNowAtMs: undefined,
|
|
610
|
-
version: lookup.version + 1,
|
|
611
644
|
};
|
|
612
645
|
|
|
613
|
-
await
|
|
646
|
+
await schedulerStore(context).saveTask(next);
|
|
614
647
|
return {
|
|
615
648
|
ok: true,
|
|
616
649
|
task: compactTask(next),
|
|
@@ -647,10 +680,9 @@ export function createSlackScheduleRunTaskNowTool(
|
|
|
647
680
|
...lookup,
|
|
648
681
|
updatedAtMs: nowMs,
|
|
649
682
|
runNowAtMs: nowMs,
|
|
650
|
-
version: lookup.version + 1,
|
|
651
683
|
};
|
|
652
684
|
|
|
653
|
-
await
|
|
685
|
+
await schedulerStore(context).saveTask(next);
|
|
654
686
|
return {
|
|
655
687
|
ok: true,
|
|
656
688
|
task: compactTask(next),
|