@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.
@@ -1,19 +1,18 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
  import { Type } from "@sinclair/typebox";
3
3
  import {
4
- AgentPluginToolInputError,
5
- agentPluginCredentialSubjectSchema,
6
- destinationSchema,
7
- isSlackDestination,
8
- type AgentPluginCredentialSubject,
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 { createSchedulerStore } from "./store";
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?: AgentPluginCredentialSubject;
27
+ credentialSubject?: PluginCredentialSubject;
29
28
  requester?: SlackRequester;
30
- source?: SlackDestination;
31
- state: AgentPluginState;
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 AgentPluginToolInputError(error);
44
+ throw new PluginToolInputError(error);
46
45
  }
47
46
 
48
47
  function requireActiveConversation(
49
48
  context: SchedulerToolContext,
50
49
  ): SlackDestination {
51
- const parsed = destinationSchema.safeParse(context.source);
50
+ const parsed = sourceSchema.safeParse(context.source);
52
51
  if (!parsed.success) {
53
- const source = context.source as Partial<SlackDestination> | undefined;
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 (!isSlackDestination(parsed.data)) {
71
+ if (parsed.data.platform !== "slack") {
73
72
  throwToolInputError("No active Slack conversation is available.");
74
73
  }
75
74
 
76
- return parsed.data;
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: AgentPluginToolDefinition<TInput>,
103
- ): AgentPluginToolDefinition<TInput> {
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: AgentPluginCredentialSubject | undefined;
129
- }): AgentPluginCredentialSubject | undefined {
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 = agentPluginCredentialSubjectSchema.safeParse(args.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 createSchedulerStore(args.context.state).getTask(
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 future or recurring Junior task in the active Slack conversation. 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.",
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
- "Provide only for explicitly repeating schedules; omit for one-time requests like 'in 1 minute', 'tomorrow', or a specific date. Recurring tasks run at most once per day: use daily, weekly, monthly, or yearly only.",
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 createSchedulerStore(context.state).saveTask(task);
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 createSchedulerStore(context.state).listTasksForTeam(
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 createSchedulerStore(context.state).saveTask(next);
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 createSchedulerStore(context.state).saveTask(next);
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 createSchedulerStore(context.state).saveTask(next);
685
+ await schedulerStore(context).saveTask(next);
654
686
  return {
655
687
  ok: true,
656
688
  task: compactTask(next),