@deepseek-ai/dsh-tool-schedule 0.2.1-rc.5 → 0.2.2-alpha.3

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/lib/index.js CHANGED
@@ -1,12 +1,13 @@
1
1
  import { delegationDepthOf } from "@deepseek-ai/dsh-subagent";
2
2
  import { defineTool } from "@deepseek-ai/dsh-tools";
3
- import { MAX_TITLE_LENGTH, MIN_EVERY_INTERVAL_SECONDS, REQUIRED_TITLE_MESSAGE, ScheduleId, ScheduleInputError, scheduleView } from "@deepseek-ai/dsh-schedule";
3
+ import { DEFAULT_LIST_LIMIT, MAX_LIST_LIMIT, MAX_TITLE_LENGTH, MIN_EVERY_INTERVAL_SECONDS, ScheduleId, ScheduleInputError, formatScheduleLocalTime } from "@deepseek-ai/dsh-schedule";
4
4
  //#region lib/types/index.js
5
5
  /**
6
- * Model-facing `schedule_create`, `schedule_list`, `schedule_update`, and
7
- * `schedule_delete` tools over the Host `ctx.schedule` service. Mounting the
6
+ * Model-facing `schedule_create`, `schedule_list`, `schedule_view`, and
7
+ * `schedule_edit` tools over the Host `ctx.schedule` service. Mounting the
8
8
  * plugin registers them in the mounting scope, so a preset decides which
9
- * agents receive them; every call acts on the calling Agent's Session.
9
+ * agents receive them. Tasks belong to the Host, not to a Session: the tools
10
+ * read and change any task by id, and only the delivery target names a Session.
10
11
  * @module @deepseek-ai/dsh-tool-schedule
11
12
  */
12
13
  /** Plugin name registered with the Loader. */
@@ -14,48 +15,27 @@ const name = "tool-schedule";
14
15
  /** Host services this plugin consumes. The Schedule service is resolved per
15
16
  * scope, so a preset that declares this plugin stays inert while the shipped
16
17
  * composition keeps that service off. */
17
- const inject = ["tools"];
18
- const SHARED_VIEW_PROPERTIES = {
19
- id: {
20
- type: "string",
21
- required: true
22
- },
23
- title: {
24
- type: "string",
25
- required: true
26
- },
27
- prompt: {
28
- type: "string",
29
- required: true
30
- },
31
- scheduledAt: {
32
- type: "string",
33
- required: true
34
- },
35
- state: {
36
- type: "string",
37
- required: true,
38
- enum: ["scheduled", "overdue"]
39
- },
40
- deliveryMode: {
41
- type: "string",
42
- required: true,
43
- const: "host"
18
+ const inject = ["tools", "systemPrompt"];
19
+ const SESSION_CREATE_SCHEMA = {
20
+ type: "object",
21
+ additionalProperties: false,
22
+ properties: {
23
+ cwd: { type: "string" },
24
+ agentPreset: { type: "string" }
44
25
  }
45
26
  };
46
- const VIEW_SCHEMA = { oneOf: [
27
+ const TARGET_SCHEMA = { oneOf: [
47
28
  {
48
29
  type: "object",
49
30
  additionalProperties: false,
50
31
  properties: {
51
- ...SHARED_VIEW_PROPERTIES,
52
32
  kind: {
53
33
  type: "string",
54
34
  required: true,
55
- const: "after"
35
+ const: "session"
56
36
  },
57
- afterSeconds: {
58
- type: "integer",
37
+ sessionId: {
38
+ type: "string",
59
39
  required: true
60
40
  }
61
41
  }
@@ -64,11 +44,18 @@ const VIEW_SCHEMA = { oneOf: [
64
44
  type: "object",
65
45
  additionalProperties: false,
66
46
  properties: {
67
- ...SHARED_VIEW_PROPERTIES,
68
47
  kind: {
69
48
  type: "string",
70
49
  required: true,
71
- const: "at"
50
+ const: "new_once"
51
+ },
52
+ sessionId: {
53
+ type: "string",
54
+ required: true
55
+ },
56
+ create: {
57
+ ...SESSION_CREATE_SCHEMA,
58
+ required: true
72
59
  }
73
60
  }
74
61
  },
@@ -76,189 +63,345 @@ const VIEW_SCHEMA = { oneOf: [
76
63
  type: "object",
77
64
  additionalProperties: false,
78
65
  properties: {
79
- ...SHARED_VIEW_PROPERTIES,
80
66
  kind: {
81
67
  type: "string",
82
68
  required: true,
83
- const: "every"
69
+ const: "new"
84
70
  },
85
- everySeconds: {
86
- type: "integer",
71
+ create: {
72
+ ...SESSION_CREATE_SCHEMA,
87
73
  required: true
88
74
  }
89
75
  }
90
- },
76
+ }
77
+ ] };
78
+ const DELIVERY_SCHEMA = {
79
+ type: "object",
80
+ additionalProperties: false,
81
+ properties: {
82
+ scheduledAt: {
83
+ type: "string",
84
+ required: true
85
+ },
86
+ deliveredAt: {
87
+ type: "string",
88
+ required: true
89
+ },
90
+ targetSessionId: {
91
+ type: "string",
92
+ required: true
93
+ }
94
+ }
95
+ };
96
+ const TASK_PROPERTIES = {
97
+ type: "object",
98
+ additionalProperties: false,
99
+ properties: {
100
+ id: {
101
+ type: "string",
102
+ required: true
103
+ },
104
+ title: {
105
+ type: "string",
106
+ required: true
107
+ },
108
+ prompt: {
109
+ type: "string",
110
+ required: true
111
+ },
112
+ scheduledAt: {
113
+ type: "string",
114
+ required: true
115
+ },
116
+ scheduledAtLocal: {
117
+ type: "string",
118
+ required: true
119
+ },
120
+ target: {
121
+ ...TARGET_SCHEMA,
122
+ required: true
123
+ },
124
+ status: {
125
+ type: "string",
126
+ required: true,
127
+ enum: [
128
+ "active",
129
+ "paused",
130
+ "ended"
131
+ ]
132
+ },
133
+ updatedAt: { type: "string" },
134
+ lastDelivery: DELIVERY_SCHEMA
135
+ }
136
+ };
137
+ const TASK_SCHEMA = { oneOf: [
91
138
  {
92
- type: "object",
93
- additionalProperties: false,
139
+ ...TASK_PROPERTIES,
94
140
  properties: {
95
- ...SHARED_VIEW_PROPERTIES,
141
+ ...TASK_PROPERTIES.properties,
96
142
  kind: {
97
143
  type: "string",
98
144
  required: true,
99
- const: "daily"
145
+ const: "rrule"
100
146
  },
101
- time: {
102
- type: "string",
103
- required: true
104
- },
105
- timeZone: {
147
+ rrule: {
106
148
  type: "string",
107
149
  required: true
108
150
  }
109
151
  }
110
152
  },
111
153
  {
112
- type: "object",
113
- additionalProperties: false,
154
+ ...TASK_PROPERTIES,
114
155
  properties: {
115
- ...SHARED_VIEW_PROPERTIES,
156
+ ...TASK_PROPERTIES.properties,
116
157
  kind: {
117
158
  type: "string",
118
159
  required: true,
119
- const: "weekly"
160
+ const: "cron"
120
161
  },
121
- time: {
162
+ expression: {
122
163
  type: "string",
123
164
  required: true
124
165
  },
125
166
  timeZone: {
126
167
  type: "string",
127
168
  required: true
128
- },
129
- weekdays: {
130
- type: "array",
131
- required: true,
132
- items: { type: "integer" }
133
169
  }
134
170
  }
135
171
  },
136
172
  {
137
- type: "object",
138
- additionalProperties: false,
173
+ ...TASK_PROPERTIES,
139
174
  properties: {
140
- ...SHARED_VIEW_PROPERTIES,
175
+ ...TASK_PROPERTIES.properties,
141
176
  kind: {
142
177
  type: "string",
143
178
  required: true,
144
- const: "cron"
145
- },
146
- expression: {
147
- type: "string",
148
- required: true
179
+ const: "every"
149
180
  },
150
- timeZone: {
151
- type: "string",
181
+ everySeconds: {
182
+ type: "integer",
152
183
  required: true
153
184
  }
154
185
  }
155
- }
156
- ] };
157
- /** Build one exact two-field error schema while preserving its literal code. */
158
- function basicErrorSchema(code) {
159
- return {
160
- type: "object",
161
- additionalProperties: false,
186
+ },
187
+ {
188
+ ...TASK_PROPERTIES,
162
189
  properties: {
163
- code: {
190
+ ...TASK_PROPERTIES.properties,
191
+ kind: {
164
192
  type: "string",
165
193
  required: true,
166
- const: code
194
+ const: "after"
167
195
  },
168
- message: {
169
- type: "string",
196
+ afterSeconds: {
197
+ type: "integer",
170
198
  required: true
171
199
  }
172
200
  }
173
- };
174
- }
175
- const ERROR_SCHEMAS = [
176
- basicErrorSchema("invalid_prompt"),
177
- basicErrorSchema("invalid_selector"),
178
- basicErrorSchema("invalid_rule"),
179
- basicErrorSchema("invalid_time_zone"),
180
- basicErrorSchema("not_future"),
181
- basicErrorSchema("time_out_of_range"),
182
- basicErrorSchema("frequency_too_high"),
183
- basicErrorSchema("subagent_session"),
184
- basicErrorSchema("internal_error")
185
- ];
186
- const CREATE_OUTPUT_SCHEMA = { oneOf: [VIEW_SCHEMA, ...ERROR_SCHEMAS] };
187
- const LIST_OUTPUT_SCHEMA = { oneOf: [{
188
- type: "array",
189
- items: VIEW_SCHEMA
190
- }, ...ERROR_SCHEMAS] };
191
- const DELETE_OUTPUT_SCHEMA = { oneOf: [
201
+ },
192
202
  {
193
- type: "object",
194
- additionalProperties: false,
203
+ ...TASK_PROPERTIES,
195
204
  properties: {
196
- id: {
205
+ ...TASK_PROPERTIES.properties,
206
+ kind: {
197
207
  type: "string",
198
- required: true
199
- },
200
- deleted: {
201
- type: "boolean",
202
208
  required: true,
203
- const: true
209
+ const: "at"
204
210
  }
205
211
  }
206
212
  },
207
213
  {
208
- type: "object",
209
- additionalProperties: false,
214
+ ...TASK_PROPERTIES,
210
215
  properties: {
211
- id: {
216
+ ...TASK_PROPERTIES.properties,
217
+ kind: {
218
+ type: "string",
219
+ required: true,
220
+ const: "daily"
221
+ },
222
+ time: {
212
223
  type: "string",
213
224
  required: true
214
225
  },
215
- deleted: {
216
- type: "boolean",
226
+ timeZone: {
227
+ type: "string",
228
+ required: true
229
+ }
230
+ }
231
+ },
232
+ {
233
+ ...TASK_PROPERTIES,
234
+ properties: {
235
+ ...TASK_PROPERTIES.properties,
236
+ kind: {
237
+ type: "string",
217
238
  required: true,
218
- const: false
239
+ const: "weekly"
219
240
  },
220
- code: {
241
+ time: {
221
242
  type: "string",
243
+ required: true
244
+ },
245
+ timeZone: {
246
+ type: "string",
247
+ required: true
248
+ },
249
+ weekdays: {
250
+ type: "array",
222
251
  required: true,
223
- const: "schedule_not_found"
252
+ items: { type: "integer" }
224
253
  }
225
254
  }
226
- },
227
- ...ERROR_SCHEMAS
255
+ }
228
256
  ] };
229
- const UPDATE_OUTPUT_SCHEMA = { oneOf: [
230
- VIEW_SCHEMA,
231
- {
257
+ const LIST_PAGE_SCHEMA = {
258
+ type: "object",
259
+ additionalProperties: false,
260
+ properties: {
261
+ items: {
262
+ type: "array",
263
+ required: true,
264
+ items: TASK_SCHEMA
265
+ },
266
+ shown: {
267
+ type: "integer",
268
+ required: true
269
+ },
270
+ total: {
271
+ type: "integer",
272
+ required: true
273
+ },
274
+ limit: {
275
+ type: "integer",
276
+ required: true
277
+ },
278
+ offset: {
279
+ type: "integer",
280
+ required: true
281
+ }
282
+ }
283
+ };
284
+ const NOT_FOUND_SCHEMA = {
285
+ type: "object",
286
+ additionalProperties: false,
287
+ properties: {
288
+ id: {
289
+ type: "string",
290
+ required: true
291
+ },
292
+ code: {
293
+ type: "string",
294
+ required: true,
295
+ const: "schedule_not_found"
296
+ }
297
+ }
298
+ };
299
+ const DELETED_SCHEMA = {
300
+ type: "object",
301
+ additionalProperties: false,
302
+ properties: {
303
+ id: {
304
+ type: "string",
305
+ required: true
306
+ },
307
+ deleted: {
308
+ type: "boolean",
309
+ required: true,
310
+ const: true
311
+ }
312
+ }
313
+ };
314
+ const CONFLICT_SCHEMA = {
315
+ type: "object",
316
+ additionalProperties: false,
317
+ properties: {
318
+ id: {
319
+ type: "string",
320
+ required: true
321
+ },
322
+ code: {
323
+ type: "string",
324
+ required: true,
325
+ const: "schedule_conflict"
326
+ }
327
+ }
328
+ };
329
+ /** Errors every tool can return: a delegated caller and a withheld failure. */
330
+ const CALLER_ERRORS = ["subagent_session", "internal_error"];
331
+ /** Errors of a new title, prompt, or recurrence. */
332
+ const CREATE_ERRORS = [
333
+ "invalid_prompt",
334
+ "invalid_rule",
335
+ "invalid_time_zone",
336
+ "not_future",
337
+ "frequency_too_high",
338
+ ...CALLER_ERRORS
339
+ ];
340
+ /** Errors of an invalid page request. */
341
+ const LIST_ERRORS = ["invalid_rule", ...CALLER_ERRORS];
342
+ /** Errors of a changed field or an invalid field combination. */
343
+ const EDIT_ERRORS = ["invalid_selector", ...CREATE_ERRORS];
344
+ /** Build the exact two-field error schema of one tool from the codes it can return. */
345
+ function errorSchema(codes) {
346
+ return {
232
347
  type: "object",
233
348
  additionalProperties: false,
234
349
  properties: {
235
- id: {
350
+ code: {
236
351
  type: "string",
237
- required: true
238
- },
239
- updated: {
240
- type: "boolean",
241
352
  required: true,
242
- const: false
353
+ enum: codes
243
354
  },
244
- code: {
355
+ message: {
245
356
  type: "string",
246
- required: true,
247
- enum: [
248
- "schedule_not_found",
249
- "schedule_ended",
250
- "schedule_conflict"
251
- ]
357
+ required: true
252
358
  }
253
359
  }
254
- },
255
- ...ERROR_SCHEMAS
360
+ };
361
+ }
362
+ const CREATE_OUTPUT_SCHEMA = { oneOf: [TASK_SCHEMA, errorSchema(CREATE_ERRORS)] };
363
+ const LIST_OUTPUT_SCHEMA = { oneOf: [LIST_PAGE_SCHEMA, errorSchema(LIST_ERRORS)] };
364
+ const VIEW_OUTPUT_SCHEMA = { oneOf: [
365
+ TASK_SCHEMA,
366
+ NOT_FOUND_SCHEMA,
367
+ errorSchema(CALLER_ERRORS)
368
+ ] };
369
+ const EDIT_OUTPUT_SCHEMA = { oneOf: [
370
+ TASK_SCHEMA,
371
+ DELETED_SCHEMA,
372
+ NOT_FOUND_SCHEMA,
373
+ CONFLICT_SCHEMA,
374
+ errorSchema(EDIT_ERRORS)
256
375
  ] };
257
- const CREATE_DESCRIPTION = "Create a reminder in the current session that delivers prompt when it becomes due. Supply exactly one timing parameter: after_seconds, at, every_seconds, daily, weekly, or cron. Local times that do not exist in the zone are skipped; repeated local times fire once, at the earlier instant. After downtime, a recurring reminder delivers only its latest missed occurrence. Delivery can repeat after a crash.";
258
- const LIST_DESCRIPTION = "List the active reminders in the current session.";
259
- const DELETE_DESCRIPTION = "Delete a reminder in the current session, active or inactive. Deletion does not retract a reminder message that is already queued.";
260
- const UPDATE_DESCRIPTION = "Change a reminder in place, keeping its id. Supply a new title, prompt, or at most one timing parameter; omitted fields keep their stored values. To change a relative delay, create a new reminder.";
261
- const CREATE_TITLE_DESCRIPTION = `Concise task name in the user's language, at most ${MAX_TITLE_LENGTH} characters. Name only the core action and subject; keep essential target identifiers such as PR numbers and branch names. Do not include execution frequency (e.g. daily, hourly), scheduled dates/times, time zones, task status, or configuration and execution details (e.g. read-only mode, profile-tag filters), even when embedded in the requested or existing title. Do not append parenthetical notes. Put scheduling information in the timing parameters and execution instructions in prompt.`;
376
+ const MANAGEMENT_AUTHORIZATION = "Only create or modify scheduled tasks at the user's explicit request; prior explicit user authorization for the running scheduled task remains valid.";
377
+ const CREATE_DESCRIPTION = "Create a scheduled task. At each occurrence of rrule, prompt is delivered to the target session as a scheduled message from the user. The result includes the task id and the next occurrence as scheduledAt in UTC and scheduledAtLocal in the zone of rrule. After downtime, only the latest missed occurrence is delivered. Delivery can repeat after a crash.";
378
+ const LIST_DESCRIPTION = "List scheduled tasks, including tasks created from other sessions. Active tasks come first, ordered by scheduledAt.";
379
+ const VIEW_DESCRIPTION = "View one scheduled task by id.";
380
+ const EDIT_DESCRIPTION = "Update, pause, resume, or delete one scheduled task by id. Deleting does not retract queued messages.";
381
+ const TITLE_HELP = `Task name of at most ${MAX_TITLE_LENGTH} characters in the user's language: action plus object. Omit timing, status, ids, and bracketed suffixes.`;
382
+ const RRULE_HELP = "Supported RFC 5545 subset: one DTSTART line, a newline, and one RRULE line. DTSTART ends in Z for UTC or carries TZID with an IANA time zone; a fixed offset such as UTC+5 is Etc/GMT-5, with the sign reversed. DTSTART must name an existing local time. A TZID rule keeps local clock time across daylight-saving changes; generated nonexistent local times are skipped, and repeated ones occur once, at the earlier instant. DTSTART may be in the past; delivery starts with the first occurrence after creation. A one-time delivery is RRULE:FREQ=DAILY;COUNT=1. No SECONDLY; BYSECOND takes one value. COUNT is at most 1000; use UNTIL for longer rules. MINUTELY and HOURLY accept no BY parts, so select clock times with DAILY or WEEKLY plus BYHOUR and BYMINUTE. BYMONTHDAY and ordinal BYDAY require MONTHLY or YEARLY; BYYEARDAY and BYWEEKNO require YEARLY. Ordinal BYDAY cannot combine with BYWEEKNO. Each FREQ period allows at most 10080 candidates before BYSETPOS, which accepts at most 31 values. UNTIL is a UTC instant ending in Z. Example: \"DTSTART;TZID=Asia/Singapore:20260901T080000\\nRRULE:FREQ=MONTHLY;BYDAY=MO,TU,WE,TH,FR;BYSETPOS=-1\"";
383
+ const TARGET_CHOICES = [
384
+ "this_session",
385
+ "new_once",
386
+ "new"
387
+ ];
388
+ /**
389
+ * Describe the target choices, naming the configured spacing of `new` deliveries when it exceeds the one-minute
390
+ * spacing of every task.
391
+ * @param newSessionIntervalSeconds - Effective `newSessionIntervalSeconds` of the Host Schedule service.
392
+ * @returns The `target` parameter description.
393
+ */
394
+ function targetHelp(newSessionIntervalSeconds) {
395
+ return `this_session delivers to this session; new_once delivers to the same new session every time, opened at the first delivery; new opens a different new session for every delivery${newSessionIntervalSeconds <= MIN_EVERY_INTERVAL_SECONDS ? "" : newSessionIntervalSeconds % 60 === 0 ? `, at most once every ${newSessionIntervalSeconds / 60} minutes` : `, at most once every ${newSessionIntervalSeconds} seconds`}. New sessions use this session's working directory and preset. With this_session or new_once, a delivery waits until the previous one finishes, and only the latest occurrence that passed meanwhile is delivered. Use this_session for reminders and for follow-ups to this conversation; use new_once or new for independent work, such as a recurring report, and whenever the user asks for a new conversation.`;
396
+ }
397
+ /** Edit parameters that only an update accepts. */
398
+ const PATCH_FIELDS = [
399
+ "title",
400
+ "prompt",
401
+ "rrule",
402
+ "target",
403
+ "active"
404
+ ];
262
405
  /** Deterministic model content for every canonical Schedule value. */
263
406
  function renderValue(_args, value) {
264
407
  return [{
@@ -266,6 +409,19 @@ function renderValue(_args, value) {
266
409
  text: JSON.stringify(value)
267
410
  }];
268
411
  }
412
+ /** Preserve timing, destination, and edit revision while withholding Host bookkeeping. */
413
+ function modelTask(task) {
414
+ const { involvedSessionIds: _involved, createdAt: _created, executionActive: _execution, deliveryFloorEndsAt: _floor, lastDelivery, ...record } = task;
415
+ return {
416
+ ...record,
417
+ scheduledAtLocal: formatScheduleLocalTime(task, task.scheduledAt),
418
+ ...lastDelivery === void 0 ? {} : { lastDelivery: {
419
+ scheduledAt: lastDelivery.scheduledAt,
420
+ deliveredAt: lastDelivery.deliveredAt,
421
+ targetSessionId: lastDelivery.targetSessionId
422
+ } }
423
+ };
424
+ }
269
425
  /** Pure generic pending card. */
270
426
  function present(title, kind, rawInput) {
271
427
  return {
@@ -282,379 +438,255 @@ function internalError() {
282
438
  message: "The schedule operation failed."
283
439
  };
284
440
  }
285
- /** Translate invalid input while withholding internal storage failures. */
286
- function operationError(error) {
287
- return error instanceof ScheduleInputError ? {
288
- code: error.code,
289
- message: error.message
290
- } : internalError();
291
- }
292
441
  /**
293
- * Refuse a caller that is a delegated child. A subagent cannot use reminders at
294
- * all, so every tool rejects before dispatch instead of relying on the mounting
295
- * preset's tool filter alone.
296
- * @param agent - the calling agent, when the outer call has one.
297
- * @returns the stable refusal, or undefined for a top-level caller.
442
+ * Run one Schedule operation for the root Agent that dispatched the call.
443
+ *
444
+ * A delegated subagent cannot use scheduled tasks, so every tool refuses it here even when its preset exposes the
445
+ * tool. An input error keeps its code when the tool declares that code; every other failure becomes `internal_error`.
446
+ * @param exec - Run context of the call.
447
+ * @param codes - Error codes of the tool.
448
+ * @param operation - Service call for the calling Agent.
449
+ * @returns The operation result, or an error with a declared code.
298
450
  */
299
- function subagentCallerRefusal(agent) {
300
- if (agent === void 0 || delegationDepthOf(agent) === 0) return void 0;
301
- return {
451
+ async function runForCaller(exec, codes, operation) {
452
+ const agent = exec.agent;
453
+ if (agent === void 0) return internalError();
454
+ if (delegationDepthOf(agent) > 0) return {
302
455
  code: "subagent_session",
303
- message: "A delegated subagent cannot use reminders."
304
- };
305
- }
306
- /** One supplied fixed-rate interval: a safe integer at or above the Host floor, or undefined. */
307
- function invalidInterval(everySeconds) {
308
- if (everySeconds === void 0) return void 0;
309
- if (!Number.isSafeInteger(everySeconds)) return {
310
- code: "invalid_rule",
311
- message: "every_seconds must be a safe integer."
312
- };
313
- if (everySeconds < MIN_EVERY_INTERVAL_SECONDS) return {
314
- code: "frequency_too_high",
315
- message: `every_seconds must be at least ${MIN_EVERY_INTERVAL_SECONDS}.`
316
- };
317
- }
318
- /** Validate selector constraints that the open parameter root cannot express. */
319
- function validateCreateArgs(args) {
320
- if (Object.keys(args).some((key) => key !== "prompt" && key !== "title" && key !== "after_seconds" && key !== "at" && key !== "every_seconds" && key !== "daily" && key !== "weekly" && key !== "cron") || Number(args.after_seconds !== void 0) + Number(args.at !== void 0) + Number(args.every_seconds !== void 0) + Number(args.daily !== void 0) + Number(args.weekly !== void 0) + Number(args.cron !== void 0) !== 1) return {
321
- code: "invalid_selector",
322
- message: "schedule_create accepts exactly one of after_seconds, at, every_seconds, daily, weekly, or cron."
323
- };
324
- if (args.prompt.trim().length === 0) return {
325
- code: "invalid_prompt",
326
- message: "prompt must be non-empty after trimming."
327
- };
328
- if (args.title.trim().length === 0) return {
329
- code: "invalid_prompt",
330
- message: REQUIRED_TITLE_MESSAGE
331
- };
332
- if (args.title.trim().length > MAX_TITLE_LENGTH) return {
333
- code: "invalid_prompt",
334
- message: `title must be at most ${MAX_TITLE_LENGTH} characters.`
335
- };
336
- if (args.after_seconds !== void 0 && (!Number.isSafeInteger(args.after_seconds) || args.after_seconds <= 0)) return {
337
- code: "invalid_rule",
338
- message: "after_seconds must be a positive safe integer."
339
- };
340
- return invalidInterval(args.every_seconds);
341
- }
342
- /** Validate the in-place update's selector count, id, and any supplied name, instruction, or interval. */
343
- function validateUpdateArgs(args) {
344
- const selectors = [
345
- args.at !== void 0,
346
- args.every_seconds !== void 0,
347
- args.daily !== void 0,
348
- args.weekly !== void 0,
349
- args.cron !== void 0
350
- ].filter(Boolean).length;
351
- if (Object.keys(args).some((key) => key !== "id" && key !== "title" && key !== "prompt" && key !== "at" && key !== "every_seconds" && key !== "daily" && key !== "weekly" && key !== "cron") || selectors > 1) return {
352
- code: "invalid_selector",
353
- message: "schedule_update accepts at most one of at, every_seconds, daily, weekly, or cron."
354
- };
355
- if (args.id.length === 0 || args.id.trim() !== args.id) return {
356
- code: "invalid_rule",
357
- message: "schedule_update id must be non-empty without surrounding whitespace."
358
- };
359
- if (selectors === 0 && args.title === void 0 && args.prompt === void 0) return {
360
- code: "invalid_selector",
361
- message: "schedule_update needs a new title, prompt, or one of at, every_seconds, daily, weekly, or cron."
362
- };
363
- if (args.title !== void 0 && args.title.trim().length === 0) return {
364
- code: "invalid_prompt",
365
- message: REQUIRED_TITLE_MESSAGE
366
- };
367
- if (args.title !== void 0 && args.title.trim().length > MAX_TITLE_LENGTH) return {
368
- code: "invalid_prompt",
369
- message: `title must be at most ${MAX_TITLE_LENGTH} characters.`
370
- };
371
- if (args.prompt !== void 0 && args.prompt.trim().length === 0) return {
372
- code: "invalid_prompt",
373
- message: "prompt must be non-empty after trimming."
374
- };
375
- return invalidInterval(args.every_seconds);
376
- }
377
- /** The one timing replacement the update carries, or undefined when the request keeps the committed target. */
378
- function timingChangeFrom(args) {
379
- if (args.at !== void 0) return {
380
- kind: "at",
381
- at: args.at
382
- };
383
- if (args.every_seconds !== void 0) return {
384
- kind: "every",
385
- every_seconds: args.every_seconds
386
- };
387
- if (args.daily !== void 0) return {
388
- kind: "daily",
389
- daily: args.daily
390
- };
391
- if (args.weekly !== void 0) return {
392
- kind: "weekly",
393
- weekly: args.weekly
394
- };
395
- if (args.cron !== void 0) return {
396
- kind: "cron",
397
- cron: args.cron
456
+ message: "A delegated subagent cannot use scheduled tasks."
398
457
  };
458
+ try {
459
+ return await operation(agent);
460
+ } catch (error) {
461
+ if (error instanceof ScheduleInputError) {
462
+ const code = codes.find((code) => code === error.code);
463
+ if (code !== void 0) return {
464
+ code,
465
+ message: error.message
466
+ };
467
+ }
468
+ return internalError();
469
+ }
399
470
  }
400
471
  /**
401
- * Selector parameters shared by `schedule_create` and `schedule_update`, in the order the
402
- * generated tool catalog states them.
472
+ * Resolve one model target choice to a delivery destination.
473
+ *
474
+ * A created Session uses the caller's working directory and current composed preset; a preset selection in an
475
+ * empty Session changes that composition without rewriting the Session header.
476
+ * @param agent - Calling root Agent.
477
+ * @param target - Model target choice.
478
+ * @returns Destination request for the Schedule service.
403
479
  */
404
- const SELECTOR_PARAMETERS = {
405
- every_seconds: {
406
- type: "number",
407
- description: `Fixed-rate interval in whole seconds, at least ${MIN_EVERY_INTERVAL_SECONDS}, aligned to the creation time; changing it with schedule_update re-aligns it to the save time.`
408
- },
409
- daily: {
410
- type: "object",
411
- additionalProperties: false,
412
- description: "Every day at a local time.",
413
- properties: {
414
- time: {
415
- type: "string",
416
- required: true,
417
- description: "HH:mm:ss with optional 1-3 fractional digits, for example 23:00:00."
418
- },
419
- time_zone: {
420
- type: "string",
421
- required: true,
422
- description: "UTC or IANA Area/Location, for example Asia/Shanghai."
423
- }
424
- }
425
- },
426
- weekly: {
427
- type: "object",
428
- additionalProperties: false,
429
- description: "On the given weekdays at a local time.",
430
- properties: {
431
- time: {
432
- type: "string",
433
- required: true,
434
- description: "HH:mm:ss with optional 1-3 fractional digits, for example 09:00:00."
435
- },
436
- time_zone: {
437
- type: "string",
438
- required: true,
439
- description: "UTC or IANA Area/Location, for example Asia/Shanghai."
440
- },
441
- weekdays: {
442
- type: "array",
443
- required: true,
444
- description: "ISO weekdays, Monday 1 through Sunday 7, without repetitions.",
445
- items: { type: "integer" }
446
- }
447
- }
448
- },
449
- cron: {
450
- type: "object",
451
- additionalProperties: false,
452
- description: "Five-field Vixie cron expression in a time zone.",
453
- properties: {
454
- expression: {
455
- type: "string",
456
- required: true,
457
- description: "minute hour day-of-month month day-of-week, for example \"*/15 9-17 * * 1-5\". When both day fields are restricted, a date matches if either one matches."
458
- },
459
- time_zone: {
460
- type: "string",
461
- required: true,
462
- description: "UTC or IANA Area/Location, for example Asia/Shanghai."
463
- }
480
+ function targetFor(agent, target) {
481
+ if (target === "this_session") return {
482
+ kind: "session",
483
+ sessionId: agent.session.id
484
+ };
485
+ const cwd = agent.session.header.cwd;
486
+ const agentPreset = agent.ctx.get("agentPresets")?.composedPreset(agent.ctx);
487
+ return {
488
+ kind: target,
489
+ create: {
490
+ ...cwd === void 0 ? {} : { cwd },
491
+ ...agentPreset === void 0 ? {} : { agentPreset }
464
492
  }
465
- },
466
- at: {
467
- description: "Absolute target: an RFC 3339 date-time with offset, or a local date, time, and IANA time_zone.",
468
- oneOf: [{ type: "string" }, {
469
- type: "object",
470
- additionalProperties: false,
471
- properties: {
472
- date: {
473
- type: "string",
474
- required: true
475
- },
476
- time: {
477
- type: "string",
478
- required: true
479
- },
480
- time_zone: {
481
- type: "string",
482
- required: true
483
- }
484
- }
485
- }]
486
- }
487
- };
493
+ };
494
+ }
488
495
  /**
489
- * Register the four Schedule tools in the mounting scope.
496
+ * Register the four Schedule tools and shared guidance in the mounting scope.
490
497
  *
491
- * Each call mutates the Session of the Agent that dispatched it, so a mount
492
- * outside an agent scope still registers the tools but every call without a
493
- * caller Agent is refused as an internal failure.
494
- * @param ctx - Context owning the `tools` registry and the Host `schedule` service.
498
+ * Each call acts for the Agent that dispatched it, so a mount outside an
499
+ * agent scope still registers the tools but every call without a caller Agent
500
+ * is refused as an internal failure.
501
+ * @param ctx - Context owning the tool and system-prompt registries and the Host Schedule service.
495
502
  */
496
503
  function apply(ctx) {
497
- ctx.inject(["schedule"], (scheduleCtx) => {
498
- registerScheduleTools(scheduleCtx);
499
- });
504
+ ctx.inject(["schedule"], registerScheduleTools);
500
505
  }
501
506
  /**
502
- * Register the four reminder tools in the scope that resolved the Schedule
503
- * service.
507
+ * Register the tools and shared guidance in the scope that resolved Schedule.
504
508
  * @param ctx - Scope whose `schedule` service the tools act through.
505
509
  */
506
510
  function registerScheduleTools(ctx) {
511
+ ctx.systemPrompt.section({
512
+ name: "tool:schedule",
513
+ order: ctx.systemPrompt.getSectionOrder("TOOL_SCHEDULE"),
514
+ text: ({ scope }) => ctx.tools.get("schedule_create", scope) !== void 0 || ctx.tools.get("schedule_edit", scope) !== void 0 ? MANAGEMENT_AUTHORIZATION : ""
515
+ });
516
+ const targetDescription = targetHelp(ctx.schedule.deliveryLimits().newSessionIntervalSeconds);
507
517
  ctx.tools.register(defineTool({
508
518
  name: "schedule_create",
509
519
  description: CREATE_DESCRIPTION,
510
520
  parameters: {
511
- prompt: {
521
+ title: {
512
522
  type: "string",
513
523
  required: true,
514
- description: "Reminder content to present when the target becomes due."
524
+ description: TITLE_HELP
515
525
  },
516
- title: {
526
+ prompt: {
517
527
  type: "string",
518
528
  required: true,
519
- description: CREATE_TITLE_DESCRIPTION
529
+ description: "Instruction delivered at each occurrence."
520
530
  },
521
- after_seconds: {
522
- type: "number",
523
- description: "Delay in whole seconds."
531
+ rrule: {
532
+ type: "string",
533
+ required: true,
534
+ description: RRULE_HELP
524
535
  },
525
- ...SELECTOR_PARAMETERS
536
+ target: {
537
+ type: "string",
538
+ required: true,
539
+ enum: TARGET_CHOICES,
540
+ description: targetDescription
541
+ }
526
542
  },
527
543
  output: {
528
544
  schema: CREATE_OUTPUT_SCHEMA,
529
545
  render: renderValue
530
546
  },
531
- async execute(args, exec) {
532
- const agent = exec.agent;
533
- if (agent === void 0) return internalError();
534
- const refusal = subagentCallerRefusal(agent);
535
- if (refusal !== void 0) return refusal;
536
- const invalid = validateCreateArgs(args);
537
- if (invalid !== void 0) return invalid;
538
- if (exec.signal.aborted) return internalError();
539
- try {
540
- return scheduleView(await ctx.schedule.create(agent.session.id, args, exec.signal), Date.now());
541
- } catch (error) {
542
- return operationError(error);
543
- }
547
+ execute(args, exec) {
548
+ return runForCaller(exec, CREATE_ERRORS, (agent) => ctx.schedule.create({
549
+ title: args.title,
550
+ prompt: args.prompt,
551
+ timing: {
552
+ kind: "rrule",
553
+ rrule: args.rrule
554
+ },
555
+ target: targetFor(agent, args.target)
556
+ }, agent.session.id, exec.signal).then(modelTask));
544
557
  },
545
- presentCall: (args) => present("Create reminder", "other", args.prompt)
558
+ presentCall: (args) => present("Create scheduled task", "other", args.title)
546
559
  }));
547
560
  ctx.tools.register(defineTool({
548
561
  name: "schedule_list",
549
562
  description: LIST_DESCRIPTION,
550
- parameters: {},
563
+ parameters: {
564
+ active: {
565
+ type: "boolean",
566
+ description: "true for active tasks only; false for paused and ended tasks only; omitted for all tasks."
567
+ },
568
+ limit: {
569
+ type: "integer",
570
+ description: `Page size from 1 to ${MAX_LIST_LIMIT}; defaults to ${DEFAULT_LIST_LIMIT}.`
571
+ },
572
+ offset: {
573
+ type: "integer",
574
+ description: "Number of tasks to skip; defaults to 0."
575
+ }
576
+ },
551
577
  output: {
552
578
  schema: LIST_OUTPUT_SCHEMA,
553
579
  render: renderValue
554
580
  },
555
- async execute(_args, exec) {
556
- const agent = exec.agent;
557
- if (agent === void 0) return internalError();
558
- const refusal = subagentCallerRefusal(agent);
559
- if (refusal !== void 0) return refusal;
560
- if (exec.signal.aborted) return internalError();
561
- try {
562
- return (await ctx.schedule.list({ sessionId: agent.session.id })).map((record) => scheduleView(record, Date.now()));
563
- } catch (error) {
564
- return operationError(error);
565
- }
581
+ execute({ active, limit, offset }, exec) {
582
+ return runForCaller(exec, LIST_ERRORS, () => ctx.schedule.list({
583
+ ...active === void 0 ? {} : { active },
584
+ ...limit === void 0 ? {} : { limit },
585
+ ...offset === void 0 ? {} : { offset }
586
+ }).then((page) => ({
587
+ ...page,
588
+ items: page.items.map(modelTask)
589
+ })));
566
590
  },
567
- presentCall: () => present("List reminders", "read")
591
+ presentCall: () => present("List scheduled tasks", "read")
568
592
  }));
569
593
  ctx.tools.register(defineTool({
570
- name: "schedule_delete",
571
- description: DELETE_DESCRIPTION,
594
+ name: "schedule_view",
595
+ description: VIEW_DESCRIPTION,
572
596
  parameters: { id: {
573
597
  type: "string",
574
598
  required: true,
575
- description: "Exact schedule id."
599
+ description: "Task id."
576
600
  } },
577
601
  output: {
578
- schema: DELETE_OUTPUT_SCHEMA,
602
+ schema: VIEW_OUTPUT_SCHEMA,
579
603
  render: renderValue
580
604
  },
581
- async execute(args, exec) {
582
- if (args.id.length === 0 || args.id.trim() !== args.id) return {
583
- code: "invalid_rule",
584
- message: "schedule_delete id must be non-empty without surrounding whitespace."
585
- };
586
- const id = ScheduleId(args.id);
587
- const agent = exec.agent;
588
- if (agent === void 0) return internalError();
589
- const refusal = subagentCallerRefusal(agent);
590
- if (refusal !== void 0) return refusal;
591
- if (exec.signal.aborted) return internalError();
592
- try {
593
- return await ctx.schedule.delete({
594
- sessionId: agent.session.id,
595
- id
596
- }, exec.signal);
597
- } catch (error) {
598
- return operationError(error);
599
- }
605
+ execute(args, exec) {
606
+ return runForCaller(exec, CALLER_ERRORS, () => ctx.schedule.view({ id: ScheduleId(args.id) }).then((result) => "kind" in result ? modelTask(result) : result));
600
607
  },
601
- presentCall: (args) => present("Delete reminder", "other", args.id)
608
+ presentCall: (args) => present("View scheduled task", "read", args.id)
602
609
  }));
603
610
  ctx.tools.register(defineTool({
604
- name: "schedule_update",
605
- description: UPDATE_DESCRIPTION,
611
+ name: "schedule_edit",
612
+ description: EDIT_DESCRIPTION,
606
613
  parameters: {
607
614
  id: {
608
615
  type: "string",
609
616
  required: true,
610
- description: "Schedule id returned by schedule_list."
617
+ description: "Task id."
618
+ },
619
+ action: {
620
+ type: "string",
621
+ required: true,
622
+ enum: ["update", "delete"],
623
+ description: "update applies the provided fields, at least one; delete removes the task."
611
624
  },
612
625
  title: {
613
626
  type: "string",
614
- description: "New task name; follow the schedule_create title rules."
627
+ description: TITLE_HELP
615
628
  },
616
629
  prompt: {
617
630
  type: "string",
618
- description: "New reminder content."
631
+ description: "New instruction delivered at each occurrence."
619
632
  },
620
- ...SELECTOR_PARAMETERS
633
+ rrule: {
634
+ type: "string",
635
+ description: "New recurrence in the schedule_create rrule format."
636
+ },
637
+ target: {
638
+ type: "string",
639
+ enum: TARGET_CHOICES,
640
+ description: "Omit to retain the destination and its creation settings. Providing target replaces them; new_once reserves a different Session. New targets capture this Session's working directory and preset. Choices are the same as schedule_create."
641
+ },
642
+ active: {
643
+ type: "boolean",
644
+ description: "false pauses the task; true resumes only if its timing has a future occurrence. Otherwise provide a new rrule."
645
+ },
646
+ expectedUpdatedAt: {
647
+ type: "string",
648
+ description: "updatedAt from the latest result that returned this task; if the task changed after that result, the result is schedule_conflict and nothing changes."
649
+ }
621
650
  },
622
651
  output: {
623
- schema: UPDATE_OUTPUT_SCHEMA,
652
+ schema: EDIT_OUTPUT_SCHEMA,
624
653
  render: renderValue
625
654
  },
626
- async execute(args, exec) {
627
- const agent = exec.agent;
628
- if (agent === void 0) return internalError();
629
- const refusal = subagentCallerRefusal(agent);
630
- if (refusal !== void 0) return refusal;
631
- const invalid = validateUpdateArgs(args);
632
- if (invalid !== void 0) return invalid;
633
- if (exec.signal.aborted) return internalError();
634
- const id = ScheduleId(args.id);
635
- try {
636
- const sessionId = agent.session.id;
637
- const expected = (await ctx.schedule.list({ sessionId })).find((record) => record.id === id);
638
- if (expected === void 0) return {
639
- id,
640
- updated: false,
641
- code: (await ctx.schedule.catalog()).some((entry) => entry.sessionId === sessionId && entry.id === id) ? "schedule_ended" : "schedule_not_found"
642
- };
643
- const change = timingChangeFrom(args);
644
- const result = await ctx.schedule.update({
645
- sessionId,
646
- id,
647
- expected,
648
- ...change === void 0 ? {} : { change },
649
- ...args.title === void 0 ? {} : { title: args.title },
650
- ...args.prompt === void 0 ? {} : { prompt: args.prompt }
651
- }, exec.signal);
652
- return "record" in result ? scheduleView(result.record, Date.now()) : result;
653
- } catch (error) {
654
- return operationError(error);
655
- }
655
+ execute(args, exec) {
656
+ return runForCaller(exec, EDIT_ERRORS, (agent) => {
657
+ const id = ScheduleId(args.id);
658
+ const expected = args.expectedUpdatedAt === void 0 ? {} : { expectedUpdatedAt: args.expectedUpdatedAt };
659
+ let request;
660
+ if (args.action === "delete") {
661
+ if (PATCH_FIELDS.some((field) => args[field] !== void 0)) throw new ScheduleInputError("invalid_selector", "delete accepts only id, action, and expectedUpdatedAt.");
662
+ request = {
663
+ id,
664
+ action: "delete",
665
+ ...expected
666
+ };
667
+ } else {
668
+ const patch = {
669
+ ...args.title === void 0 ? {} : { title: args.title },
670
+ ...args.prompt === void 0 ? {} : { prompt: args.prompt },
671
+ ...args.rrule === void 0 ? {} : { timing: {
672
+ kind: "rrule",
673
+ rrule: args.rrule
674
+ } },
675
+ ...args.target === void 0 ? {} : { target: targetFor(agent, args.target) },
676
+ ...args.active === void 0 ? {} : { active: args.active }
677
+ };
678
+ if (Object.keys(patch).length === 0) throw new ScheduleInputError("invalid_selector", "update requires at least one of title, prompt, rrule, target, or active.");
679
+ request = {
680
+ id,
681
+ action: "update",
682
+ patch,
683
+ ...expected
684
+ };
685
+ }
686
+ return ctx.schedule.edit(request, agent.session.id, exec.signal).then((result) => "kind" in result ? modelTask(result) : result);
687
+ });
656
688
  },
657
- presentCall: (args) => present("Update reminder", "other", args.id)
689
+ presentCall: (args) => present(args.action === "delete" ? "Delete scheduled task" : "Edit scheduled task", "other", args.id)
658
690
  }));
659
691
  }
660
692
  //#endregion