@qilin-agent/tool-schedule 3.1.7 → 3.1.8

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 "@qilin-agent/subagent";
2
2
  import { defineTool } from "@qilin-agent/tools";
3
- import { MAX_TITLE_LENGTH, MIN_EVERY_INTERVAL_SECONDS, REQUIRED_TITLE_MESSAGE, ScheduleId, ScheduleInputError, scheduleView } from "@qilin-agent/schedule";
3
+ import { DEFAULT_LIST_LIMIT, MAX_LIST_LIMIT, MAX_TITLE_LENGTH, MIN_EVERY_INTERVAL_SECONDS, ScheduleId, ScheduleInputError, formatScheduleLocalTime } from "@qilin-agent/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 @qilin-agent/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,188 +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.";
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
+ ];
261
405
  /** Deterministic model content for every canonical Schedule value. */
262
406
  function renderValue(_args, value) {
263
407
  return [{
@@ -265,6 +409,19 @@ function renderValue(_args, value) {
265
409
  text: JSON.stringify(value)
266
410
  }];
267
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
+ }
268
425
  /** Pure generic pending card. */
269
426
  function present(title, kind, rawInput) {
270
427
  return {
@@ -281,379 +438,255 @@ function internalError() {
281
438
  message: "The schedule operation failed."
282
439
  };
283
440
  }
284
- /** Translate invalid input while withholding internal storage failures. */
285
- function operationError(error) {
286
- return error instanceof ScheduleInputError ? {
287
- code: error.code,
288
- message: error.message
289
- } : internalError();
290
- }
291
441
  /**
292
- * Refuse a caller that is a delegated child. A subagent cannot use reminders at
293
- * all, so every tool rejects before dispatch instead of relying on the mounting
294
- * preset's tool filter alone.
295
- * @param agent - the calling agent, when the outer call has one.
296
- * @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.
297
450
  */
298
- function subagentCallerRefusal(agent) {
299
- if (agent === void 0 || delegationDepthOf(agent) === 0) return void 0;
300
- 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 {
301
455
  code: "subagent_session",
302
- message: "A delegated subagent cannot use reminders."
303
- };
304
- }
305
- /** One supplied fixed-rate interval: a safe integer at or above the Host floor, or undefined. */
306
- function invalidInterval(everySeconds) {
307
- if (everySeconds === void 0) return void 0;
308
- if (!Number.isSafeInteger(everySeconds)) return {
309
- code: "invalid_rule",
310
- message: "every_seconds must be a safe integer."
311
- };
312
- if (everySeconds < MIN_EVERY_INTERVAL_SECONDS) return {
313
- code: "frequency_too_high",
314
- message: `every_seconds must be at least ${MIN_EVERY_INTERVAL_SECONDS}.`
315
- };
316
- }
317
- /** Validate selector constraints that the open parameter root cannot express. */
318
- function validateCreateArgs(args) {
319
- 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 {
320
- code: "invalid_selector",
321
- message: "schedule_create accepts exactly one of after_seconds, at, every_seconds, daily, weekly, or cron."
322
- };
323
- if (args.prompt.trim().length === 0) return {
324
- code: "invalid_prompt",
325
- message: "prompt must be non-empty after trimming."
326
- };
327
- if (args.title.trim().length === 0) return {
328
- code: "invalid_prompt",
329
- message: REQUIRED_TITLE_MESSAGE
330
- };
331
- if (args.title.trim().length > MAX_TITLE_LENGTH) return {
332
- code: "invalid_prompt",
333
- message: `title must be at most ${MAX_TITLE_LENGTH} characters.`
334
- };
335
- if (args.after_seconds !== void 0 && (!Number.isSafeInteger(args.after_seconds) || args.after_seconds <= 0)) return {
336
- code: "invalid_rule",
337
- message: "after_seconds must be a positive safe integer."
338
- };
339
- return invalidInterval(args.every_seconds);
340
- }
341
- /** Validate the in-place update's selector count, id, and any supplied name, instruction, or interval. */
342
- function validateUpdateArgs(args) {
343
- const selectors = [
344
- args.at !== void 0,
345
- args.every_seconds !== void 0,
346
- args.daily !== void 0,
347
- args.weekly !== void 0,
348
- args.cron !== void 0
349
- ].filter(Boolean).length;
350
- 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 {
351
- code: "invalid_selector",
352
- message: "schedule_update accepts at most one of at, every_seconds, daily, weekly, or cron."
353
- };
354
- if (args.id.length === 0 || args.id.trim() !== args.id) return {
355
- code: "invalid_rule",
356
- message: "schedule_update id must be non-empty without surrounding whitespace."
357
- };
358
- if (selectors === 0 && args.title === void 0 && args.prompt === void 0) return {
359
- code: "invalid_selector",
360
- message: "schedule_update needs a new title, prompt, or one of at, every_seconds, daily, weekly, or cron."
361
- };
362
- if (args.title !== void 0 && args.title.trim().length === 0) return {
363
- code: "invalid_prompt",
364
- message: REQUIRED_TITLE_MESSAGE
365
- };
366
- if (args.title !== void 0 && args.title.trim().length > MAX_TITLE_LENGTH) return {
367
- code: "invalid_prompt",
368
- message: `title must be at most ${MAX_TITLE_LENGTH} characters.`
369
- };
370
- if (args.prompt !== void 0 && args.prompt.trim().length === 0) return {
371
- code: "invalid_prompt",
372
- message: "prompt must be non-empty after trimming."
373
- };
374
- return invalidInterval(args.every_seconds);
375
- }
376
- /** The one timing replacement the update carries, or undefined when the request keeps the committed target. */
377
- function timingChangeFrom(args) {
378
- if (args.at !== void 0) return {
379
- kind: "at",
380
- at: args.at
381
- };
382
- if (args.every_seconds !== void 0) return {
383
- kind: "every",
384
- every_seconds: args.every_seconds
385
- };
386
- if (args.daily !== void 0) return {
387
- kind: "daily",
388
- daily: args.daily
389
- };
390
- if (args.weekly !== void 0) return {
391
- kind: "weekly",
392
- weekly: args.weekly
393
- };
394
- if (args.cron !== void 0) return {
395
- kind: "cron",
396
- cron: args.cron
456
+ message: "A delegated subagent cannot use scheduled tasks."
397
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
+ }
398
470
  }
399
471
  /**
400
- * Selector parameters shared by `schedule_create` and `schedule_update`, in the order the
401
- * 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.
402
479
  */
403
- const SELECTOR_PARAMETERS = {
404
- every_seconds: {
405
- type: "number",
406
- 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.`
407
- },
408
- daily: {
409
- type: "object",
410
- additionalProperties: false,
411
- description: "Every day at a local time.",
412
- properties: {
413
- time: {
414
- type: "string",
415
- required: true,
416
- description: "HH:mm:ss with optional 1-3 fractional digits, for example 23:00:00."
417
- },
418
- time_zone: {
419
- type: "string",
420
- required: true,
421
- description: "UTC or IANA Area/Location, for example Asia/Shanghai."
422
- }
423
- }
424
- },
425
- weekly: {
426
- type: "object",
427
- additionalProperties: false,
428
- description: "On the given weekdays at a local time.",
429
- properties: {
430
- time: {
431
- type: "string",
432
- required: true,
433
- description: "HH:mm:ss with optional 1-3 fractional digits, for example 09:00:00."
434
- },
435
- time_zone: {
436
- type: "string",
437
- required: true,
438
- description: "UTC or IANA Area/Location, for example Asia/Shanghai."
439
- },
440
- weekdays: {
441
- type: "array",
442
- required: true,
443
- description: "ISO weekdays, Monday 1 through Sunday 7, without repetitions.",
444
- items: { type: "integer" }
445
- }
446
- }
447
- },
448
- cron: {
449
- type: "object",
450
- additionalProperties: false,
451
- description: "Five-field Vixie cron expression in a time zone.",
452
- properties: {
453
- expression: {
454
- type: "string",
455
- required: true,
456
- 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."
457
- },
458
- time_zone: {
459
- type: "string",
460
- required: true,
461
- description: "UTC or IANA Area/Location, for example Asia/Shanghai."
462
- }
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 }
463
492
  }
464
- },
465
- at: {
466
- description: "Absolute target: an RFC 3339 date-time with offset, or a local date, time, and IANA time_zone.",
467
- oneOf: [{ type: "string" }, {
468
- type: "object",
469
- additionalProperties: false,
470
- properties: {
471
- date: {
472
- type: "string",
473
- required: true
474
- },
475
- time: {
476
- type: "string",
477
- required: true
478
- },
479
- time_zone: {
480
- type: "string",
481
- required: true
482
- }
483
- }
484
- }]
485
- }
486
- };
493
+ };
494
+ }
487
495
  /**
488
- * Register the four Schedule tools in the mounting scope.
496
+ * Register the four Schedule tools and shared guidance in the mounting scope.
489
497
  *
490
- * Each call mutates the Session of the Agent that dispatched it, so a mount
491
- * outside an agent scope still registers the tools but every call without a
492
- * caller Agent is refused as an internal failure.
493
- * @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.
494
502
  */
495
503
  function apply(ctx) {
496
- ctx.inject(["schedule"], (scheduleCtx) => {
497
- registerScheduleTools(scheduleCtx);
498
- });
504
+ ctx.inject(["schedule"], registerScheduleTools);
499
505
  }
500
506
  /**
501
- * Register the four reminder tools in the scope that resolved the Schedule
502
- * service.
507
+ * Register the tools and shared guidance in the scope that resolved Schedule.
503
508
  * @param ctx - Scope whose `schedule` service the tools act through.
504
509
  */
505
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);
506
517
  ctx.tools.register(defineTool({
507
518
  name: "schedule_create",
508
519
  description: CREATE_DESCRIPTION,
509
520
  parameters: {
510
- prompt: {
521
+ title: {
511
522
  type: "string",
512
523
  required: true,
513
- description: "Reminder content to present when the target becomes due."
524
+ description: TITLE_HELP
514
525
  },
515
- title: {
526
+ prompt: {
516
527
  type: "string",
517
528
  required: true,
518
- description: `Task name of at most ${MAX_TITLE_LENGTH} characters, shown on the task card and in task lists.`
529
+ description: "Instruction delivered at each occurrence."
519
530
  },
520
- after_seconds: {
521
- type: "number",
522
- description: "Delay in whole seconds."
531
+ rrule: {
532
+ type: "string",
533
+ required: true,
534
+ description: RRULE_HELP
523
535
  },
524
- ...SELECTOR_PARAMETERS
536
+ target: {
537
+ type: "string",
538
+ required: true,
539
+ enum: TARGET_CHOICES,
540
+ description: targetDescription
541
+ }
525
542
  },
526
543
  output: {
527
544
  schema: CREATE_OUTPUT_SCHEMA,
528
545
  render: renderValue
529
546
  },
530
- async execute(args, exec) {
531
- const agent = exec.agent;
532
- if (agent === void 0) return internalError();
533
- const refusal = subagentCallerRefusal(agent);
534
- if (refusal !== void 0) return refusal;
535
- const invalid = validateCreateArgs(args);
536
- if (invalid !== void 0) return invalid;
537
- if (exec.signal.aborted) return internalError();
538
- try {
539
- return scheduleView(await ctx.schedule.create(agent.session.id, args, exec.signal), Date.now());
540
- } catch (error) {
541
- return operationError(error);
542
- }
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));
543
557
  },
544
- presentCall: (args) => present("Create reminder", "other", args.prompt)
558
+ presentCall: (args) => present("Create scheduled task", "other", args.title)
545
559
  }));
546
560
  ctx.tools.register(defineTool({
547
561
  name: "schedule_list",
548
562
  description: LIST_DESCRIPTION,
549
- 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
+ },
550
577
  output: {
551
578
  schema: LIST_OUTPUT_SCHEMA,
552
579
  render: renderValue
553
580
  },
554
- async execute(_args, exec) {
555
- const agent = exec.agent;
556
- if (agent === void 0) return internalError();
557
- const refusal = subagentCallerRefusal(agent);
558
- if (refusal !== void 0) return refusal;
559
- if (exec.signal.aborted) return internalError();
560
- try {
561
- return (await ctx.schedule.list({ sessionId: agent.session.id })).map((record) => scheduleView(record, Date.now()));
562
- } catch (error) {
563
- return operationError(error);
564
- }
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
+ })));
565
590
  },
566
- presentCall: () => present("List reminders", "read")
591
+ presentCall: () => present("List scheduled tasks", "read")
567
592
  }));
568
593
  ctx.tools.register(defineTool({
569
- name: "schedule_delete",
570
- description: DELETE_DESCRIPTION,
594
+ name: "schedule_view",
595
+ description: VIEW_DESCRIPTION,
571
596
  parameters: { id: {
572
597
  type: "string",
573
598
  required: true,
574
- description: "Exact schedule id."
599
+ description: "Task id."
575
600
  } },
576
601
  output: {
577
- schema: DELETE_OUTPUT_SCHEMA,
602
+ schema: VIEW_OUTPUT_SCHEMA,
578
603
  render: renderValue
579
604
  },
580
- async execute(args, exec) {
581
- if (args.id.length === 0 || args.id.trim() !== args.id) return {
582
- code: "invalid_rule",
583
- message: "schedule_delete id must be non-empty without surrounding whitespace."
584
- };
585
- const id = ScheduleId(args.id);
586
- const agent = exec.agent;
587
- if (agent === void 0) return internalError();
588
- const refusal = subagentCallerRefusal(agent);
589
- if (refusal !== void 0) return refusal;
590
- if (exec.signal.aborted) return internalError();
591
- try {
592
- return await ctx.schedule.delete({
593
- sessionId: agent.session.id,
594
- id
595
- }, exec.signal);
596
- } catch (error) {
597
- return operationError(error);
598
- }
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));
599
607
  },
600
- presentCall: (args) => present("Delete reminder", "other", args.id)
608
+ presentCall: (args) => present("View scheduled task", "read", args.id)
601
609
  }));
602
610
  ctx.tools.register(defineTool({
603
- name: "schedule_update",
604
- description: UPDATE_DESCRIPTION,
611
+ name: "schedule_edit",
612
+ description: EDIT_DESCRIPTION,
605
613
  parameters: {
606
614
  id: {
607
615
  type: "string",
608
616
  required: true,
609
- 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."
610
624
  },
611
625
  title: {
612
626
  type: "string",
613
- description: `New task name of at most ${MAX_TITLE_LENGTH} characters.`
627
+ description: TITLE_HELP
614
628
  },
615
629
  prompt: {
616
630
  type: "string",
617
- description: "New reminder content."
631
+ description: "New instruction delivered at each occurrence."
618
632
  },
619
- ...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
+ }
620
650
  },
621
651
  output: {
622
- schema: UPDATE_OUTPUT_SCHEMA,
652
+ schema: EDIT_OUTPUT_SCHEMA,
623
653
  render: renderValue
624
654
  },
625
- async execute(args, exec) {
626
- const agent = exec.agent;
627
- if (agent === void 0) return internalError();
628
- const refusal = subagentCallerRefusal(agent);
629
- if (refusal !== void 0) return refusal;
630
- const invalid = validateUpdateArgs(args);
631
- if (invalid !== void 0) return invalid;
632
- if (exec.signal.aborted) return internalError();
633
- const id = ScheduleId(args.id);
634
- try {
635
- const sessionId = agent.session.id;
636
- const expected = (await ctx.schedule.list({ sessionId })).find((record) => record.id === id);
637
- if (expected === void 0) return {
638
- id,
639
- updated: false,
640
- code: (await ctx.schedule.catalog()).some((entry) => entry.sessionId === sessionId && entry.id === id) ? "schedule_ended" : "schedule_not_found"
641
- };
642
- const change = timingChangeFrom(args);
643
- const result = await ctx.schedule.update({
644
- sessionId,
645
- id,
646
- expected,
647
- ...change === void 0 ? {} : { change },
648
- ...args.title === void 0 ? {} : { title: args.title },
649
- ...args.prompt === void 0 ? {} : { prompt: args.prompt }
650
- }, exec.signal);
651
- return "record" in result ? scheduleView(result.record, Date.now()) : result;
652
- } catch (error) {
653
- return operationError(error);
654
- }
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
+ });
655
688
  },
656
- presentCall: (args) => present("Update reminder", "other", args.id)
689
+ presentCall: (args) => present(args.action === "delete" ? "Delete scheduled task" : "Edit scheduled task", "other", args.id)
657
690
  }));
658
691
  }
659
692
  //#endregion