@springbrand/agent-runtime 0.3.0-alpha.6 → 0.3.0-alpha.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@springbrand/agent-runtime",
3
- "version": "0.3.0-alpha.6",
3
+ "version": "0.3.0-alpha.8",
4
4
  "type": "module",
5
5
  "files": [
6
6
  "src",
@@ -36,7 +36,6 @@ import {
36
36
  type RuntimeUnattendedExecutionGrant,
37
37
  } from "../../submission";
38
38
  import type { ExecutionLevel } from "../../lib/execution-level";
39
- import { pinnedUnattendedExecutionGrant } from "../../submission/unattended";
40
39
  import {
41
40
  HarnessRuntimeConfig,
42
41
  activateRuntimeCandidate,
@@ -519,11 +518,15 @@ export abstract class AgentRuntimeKernel<
519
518
  }> {
520
519
  if (fresh) await this.prepareRuntimeForSubmission(submissionId);
521
520
  else await this.ensureRuntimeReady();
522
- return this.currentAssemblyPin();
521
+ const submission = this.db.submissions.find(submissionId);
522
+ if (!submission) {
523
+ throw new Error(`Unknown SpringBrand submission: ${submissionId}`);
524
+ }
525
+ return this.currentAssemblyPin(submission.origin !== "scheduled");
523
526
  }
524
527
 
525
528
  // initConfig can call this inside its serialized load; re-entering ensureRuntimeReady would deadlock.
526
- private async currentAssemblyPin(): Promise<{
529
+ private async currentAssemblyPin(includeMemory = true): Promise<{
527
530
  revision: string;
528
531
  descriptor: string;
529
532
  }> {
@@ -532,16 +535,18 @@ export abstract class AgentRuntimeKernel<
532
535
  if (!runtime) {
533
536
  throw new Error("Runtime revision is not initialized");
534
537
  }
535
- return this.pinAssembly(runtime.pi, runtime.revision);
538
+ return this.pinAssembly(runtime.pi, runtime.revision, includeMemory);
536
539
  }
537
540
 
538
541
  private async pinAssembly(
539
542
  prepared: PreparedPiRuntime,
540
543
  baseRevision: string,
544
+ includeMemory: boolean,
541
545
  ): Promise<{ revision: string; descriptor: string }> {
542
546
  const pinned = await this.pi.pin({
543
547
  prepared,
544
548
  baseRevision,
549
+ includeMemory,
545
550
  readExtensionContext: ({ label }) =>
546
551
  this._hostGetContext(label),
547
552
  });
@@ -792,7 +797,7 @@ export abstract class AgentRuntimeKernel<
792
797
  requestId: submission.requestId,
793
798
  executionLevel: activeRuntime.snapshot.profile.executionLevel,
794
799
  origin: submission.origin,
795
- unattendedExecutionGrant: submission.unattendedExecutionGrant,
800
+ scheduledMaxExecutionLevel: submission.scheduledMaxExecutionLevel ?? undefined,
796
801
  },
797
802
  candidates: harnessInput.candidates,
798
803
  telemetry: this.telemetry,
@@ -820,8 +825,6 @@ export abstract class AgentRuntimeKernel<
820
825
  }
821
826
  },
822
827
  gateTool: harnessInput.platform.gateTool,
823
- authorizeUnattendedExecution:
824
- harnessInput.platform.authorizeUnattendedExecution,
825
828
  onUnattendedFailure: (reason) => {
826
829
  this.db.submissions.recordUnattendedFailure(submissionId, reason);
827
830
  },
@@ -1425,6 +1428,8 @@ export abstract class AgentRuntimeKernel<
1425
1428
  prompt: string,
1426
1429
  options?: {
1427
1430
  idempotencyKey?: string;
1431
+ scheduledMaxExecutionLevel?: ExecutionLevel;
1432
+ /** @deprecated Retained for rolling upgrades; ignored by Runtime. */
1428
1433
  unattendedExecutionGrant?: RuntimeUnattendedExecutionGrant;
1429
1434
  },
1430
1435
  ): Promise<SubmissionReceipt> {
@@ -1439,9 +1444,7 @@ export abstract class AgentRuntimeKernel<
1439
1444
  requestId,
1440
1445
  idempotencyKey: options?.idempotencyKey,
1441
1446
  origin: "scheduled",
1442
- unattendedExecutionGrant: options?.unattendedExecutionGrant
1443
- ? pinnedUnattendedExecutionGrant(options.unattendedExecutionGrant)
1444
- : undefined,
1447
+ scheduledMaxExecutionLevel: options?.scheduledMaxExecutionLevel,
1445
1448
  },
1446
1449
  SCHEDULED_STABLE_TIMEOUT_MS,
1447
1450
  );
@@ -264,6 +264,8 @@ export interface RuntimeAgentControls {
264
264
  prompt: string,
265
265
  opts?: {
266
266
  idempotencyKey?: string;
267
+ scheduledMaxExecutionLevel?: import("../../lib/execution-level").ExecutionLevel;
268
+ /** @deprecated Retained for rolling upgrades; ignored by Runtime. */
267
269
  unattendedExecutionGrant?: import("../../submission").RuntimeUnattendedExecutionGrant;
268
270
  },
269
271
  ): Promise<SubmissionReceipt>;
@@ -54,6 +54,9 @@ export function initializeSchema(sql: SqlTaggedTemplate): void {
54
54
  if (!submissionColumns.has("origin")) {
55
55
  sql`ALTER TABLE pi_submissions ADD COLUMN origin TEXT NOT NULL DEFAULT 'user'`;
56
56
  }
57
+ if (!submissionColumns.has("scheduled_max_execution_level")) {
58
+ sql`ALTER TABLE pi_submissions ADD COLUMN scheduled_max_execution_level TEXT`;
59
+ }
57
60
  if (!submissionColumns.has("unattended_execution_grant_json")) {
58
61
  sql`ALTER TABLE pi_submissions ADD COLUMN unattended_execution_grant_json TEXT`;
59
62
  }
@@ -157,6 +157,11 @@ export interface PreparedPiRuntime {
157
157
  export interface PinPiRuntimeOptions {
158
158
  readonly prepared: PreparedPiRuntime;
159
159
  readonly baseRevision: string;
160
+ /**
161
+ * Whether this Submission may inherit automatic hot and cold Memory context.
162
+ * Scheduled task input is intentionally isolated from those sources.
163
+ */
164
+ readonly includeMemory?: boolean;
160
165
  /**
161
166
  * 读取一个扩展要写入固定系统提示词的上下文。
162
167
  *
@@ -492,11 +497,14 @@ export async function pinPiRuntime(
492
497
  ): Promise<PinnedPiRuntime> {
493
498
  const state = readPreparedPiRuntime(options.prepared, owner);
494
499
  const { snapshot, assembly } = state;
500
+ const includeMemory = options.includeMemory !== false;
495
501
  const context = await assemblePiSystemContext({
496
502
  baseSystemPrompt: snapshot.pi.systemPrompt,
497
- memory: snapshot.profile.memory,
498
- memoryPort: snapshot.bindings.memory,
499
- workspace: snapshot.bindings.workspace,
503
+ memory: includeMemory
504
+ ? snapshot.profile.memory
505
+ : { ...snapshot.profile.memory, enabled: false },
506
+ memoryPort: includeMemory ? snapshot.bindings.memory : undefined,
507
+ workspace: includeMemory ? snapshot.bindings.workspace : undefined,
500
508
  skills: snapshot.bindings.skills.sources,
501
509
  extensionContexts: assembly.context,
502
510
  readExtensionContext: options.readExtensionContext,
@@ -6,7 +6,6 @@ import type { RuntimeActivityProjection } from "../conversation/state";
6
6
  import type { ExecutionLevel } from "../lib/execution-level";
7
7
  import type { AgentToolResult } from "@earendil-works/pi-agent-core";
8
8
  import type { PiToolCandidate } from "../tools/compiler";
9
- import type { RuntimeUnattendedExecutionGrant } from "../submission/contracts";
10
9
  import type {
11
10
  RuntimeEventConfirmation,
12
11
  RuntimeLifecycleFact,
@@ -1039,17 +1038,8 @@ export interface RuntimePlatformPort {
1039
1038
  * It is called before dispatch and again at the effect boundary so a grant
1040
1039
  * revocation cannot be hidden by an earlier decision.
1041
1040
  */
1042
- authorizeUnattendedExecution?(request: {
1043
- grant: RuntimeUnattendedExecutionGrant;
1044
- capability: string;
1045
- operation: string;
1046
- object?: unknown;
1047
- toolCallId: string;
1048
- toolName: string;
1049
- input: unknown;
1050
- requiredExecutionLevel: ExecutionLevel;
1051
- signal: AbortSignal;
1052
- }): Promise<void>;
1041
+ /** @deprecated Scheduled execution is authorized locally by execution level. */
1042
+ authorizeUnattendedExecution?(request: unknown): Promise<void>;
1053
1043
  }
1054
1044
 
1055
1045
  /**
@@ -25,3 +25,16 @@ export function maxExecutionLevel(
25
25
  )
26
26
  ]!;
27
27
  }
28
+
29
+ /** The most permissive level shared by an Agent and a Scheduled Task ceiling. */
30
+ export function minExecutionLevel(
31
+ left: ExecutionLevel,
32
+ right: ExecutionLevel,
33
+ ): ExecutionLevel {
34
+ return EXECUTION_LEVELS[
35
+ Math.min(
36
+ EXECUTION_LEVELS.indexOf(left),
37
+ EXECUTION_LEVELS.indexOf(right),
38
+ )
39
+ ]!;
40
+ }
@@ -14,6 +14,7 @@ import {
14
14
  } from "./lifecycle";
15
15
  import { isTerminalSubmissionStatus, submissionAbortReason, type StoredSubmission, type SubmissionOrigin, type SubmissionRepository } from "./repository";
16
16
  import type { RuntimeUnattendedExecutionGrant } from "./contracts";
17
+ import type { ExecutionLevel } from "../lib/execution-level";
17
18
 
18
19
  // Pending records retain identity indefinitely; only recent attempts reserve capacity.
19
20
  const ADMISSION_ATTEMPT_WINDOW_MS = 60_000;
@@ -26,6 +27,8 @@ export interface SubmitMessageOptions {
26
27
  regenerate?: boolean;
27
28
  /** `scheduled` marks a Run injection that locks ordinary input until it ends. */
28
29
  origin?: SubmissionOrigin;
30
+ /** Maximum execution level allowed by the Schedule Definition. */
31
+ scheduledMaxExecutionLevel?: ExecutionLevel;
29
32
  /** Trusted Host grant; accepted only for a scheduled Submission. */
30
33
  unattendedExecutionGrant?: RuntimeUnattendedExecutionGrant;
31
34
  /** Internal transfer of a durable accepted steer; never a public message option. */
@@ -188,6 +191,7 @@ export class SubmissionAdmission {
188
191
  : null,
189
192
  runId,
190
193
  origin: options.origin,
194
+ scheduledMaxExecutionLevel: options.scheduledMaxExecutionLevel,
191
195
  unattendedExecutionGrant: options.unattendedExecutionGrant,
192
196
  });
193
197
  const admitted = this.options.submissions.find(runId)!;
@@ -1,5 +1,6 @@
1
1
  import type { SqlTaggedTemplate } from "agents/chat";
2
2
  import type { LaneSnapshot } from "@earendil-works/pi-agent-core";
3
+ import type { ExecutionLevel } from "../lib/execution-level";
3
4
  import type { RuntimeUnattendedExecutionGrant } from "./unattended";
4
5
 
5
6
  // Submission 的统一定义见 ./index.ts。
@@ -56,6 +57,7 @@ export interface StoredSubmission {
56
57
  regenerateMessageId: string | null;
57
58
  runId: string | null;
58
59
  origin: SubmissionOrigin;
60
+ scheduledMaxExecutionLevel: ExecutionLevel | null;
59
61
  unattendedExecutionGrant: RuntimeUnattendedExecutionGrant | null;
60
62
  unattendedFailure: string | null;
61
63
  retryable?: boolean;
@@ -82,6 +84,7 @@ export interface NewSubmission {
82
84
  regenerateMessageId?: string | null;
83
85
  runId?: string | null;
84
86
  origin?: SubmissionOrigin;
87
+ scheduledMaxExecutionLevel?: ExecutionLevel;
85
88
  unattendedExecutionGrant?: RuntimeUnattendedExecutionGrant;
86
89
  }
87
90
 
@@ -105,6 +108,7 @@ type SubmissionRow = {
105
108
  regenerate_message_id: string | null;
106
109
  run_id: string | null;
107
110
  origin: string | null;
111
+ scheduled_max_execution_level: string | null;
108
112
  admission_retryable: number | null;
109
113
  unattended_execution_grant_json: string | null;
110
114
  unattended_failure: string | null;
@@ -136,6 +140,7 @@ function mapRow(row: SubmissionRow): StoredSubmission {
136
140
  regenerateMessageId: row.regenerate_message_id,
137
141
  runId: row.run_id,
138
142
  origin: row.origin === "scheduled" ? "scheduled" : "user",
143
+ scheduledMaxExecutionLevel: row.scheduled_max_execution_level as ExecutionLevel | null,
139
144
  unattendedExecutionGrant: row.unattended_execution_grant_json
140
145
  ? JSON.parse(row.unattended_execution_grant_json) as RuntimeUnattendedExecutionGrant
141
146
  : null,
@@ -173,6 +178,7 @@ export class SubmissionRepository {
173
178
  regenerate_message_id,
174
179
  run_id,
175
180
  origin,
181
+ scheduled_max_execution_level,
176
182
  admission_retryable,
177
183
  unattended_execution_grant_json,
178
184
  unattended_failure
@@ -276,7 +282,7 @@ export class SubmissionRepository {
276
282
  created_at, assembly_revision, assembly_descriptor, runtime_key,
277
283
  assistant_message_id, queued_input_json, queued_ui_message_json,
278
284
  user_message_id, regenerate_message_id,
279
- run_id, origin, unattended_execution_grant_json
285
+ run_id, origin, scheduled_max_execution_level, unattended_execution_grant_json
280
286
  ) VALUES (
281
287
  ${s.submissionId},
282
288
  ${s.requestId},
@@ -294,6 +300,7 @@ export class SubmissionRepository {
294
300
  ${s.regenerateMessageId ?? null},
295
301
  ${s.runId ?? s.submissionId},
296
302
  ${s.origin ?? "user"},
303
+ ${s.scheduledMaxExecutionLevel ?? null},
297
304
  ${s.unattendedExecutionGrant
298
305
  ? JSON.stringify(s.unattendedExecutionGrant)
299
306
  : null}
@@ -87,6 +87,8 @@ export interface PiToolCandidate {
87
87
  readonly alwaysRequiresApproval?: boolean;
88
88
  /** Declares the concrete grant identity derived from actual Tool input. */
89
89
  readonly unattendedExecution?: PiToolUnattendedExecutionSpec;
90
+ /** Explicitly forbids this Tool from unattended Scheduled Runs. */
91
+ readonly unattendedMode?: "forbidden";
90
92
  /** Recovery may replay this Tool Call only when the candidate guarantees convergence. Omitted means non-idempotent. */
91
93
  readonly retry?: "idempotent" | "non-idempotent";
92
94
  readonly summary?: string;
@@ -5,13 +5,13 @@ import type {
5
5
  import type { ExecutionLevel } from "../lib/execution-level";
6
6
  import {
7
7
  EXECUTION_LEVELS,
8
+ minExecutionLevel,
8
9
  requiresExecutionApproval,
9
10
  } from "../lib/execution-level";
10
11
  import type { PiToolCandidate } from "./compiler";
11
12
  import type { ApprovalRepository } from "./approval";
12
13
  import type { ToolInteractionRepository } from "./interaction";
13
14
  import type { RuntimeTelemetryCoordinator } from "../telemetry/coordinator";
14
- import type { RuntimeUnattendedExecutionGrant } from "../submission/contracts";
15
15
  import { UnattendedExecutionError } from "./unattended-error";
16
16
 
17
17
  export interface HarnessDecisionSubmission {
@@ -19,7 +19,10 @@ export interface HarnessDecisionSubmission {
19
19
  readonly requestId: string;
20
20
  readonly executionLevel: ExecutionLevel;
21
21
  readonly origin?: "user" | "scheduled";
22
- readonly unattendedExecutionGrant?: RuntimeUnattendedExecutionGrant | null;
22
+ /** Maximum level allowed by the owning Schedule Definition. */
23
+ readonly scheduledMaxExecutionLevel?: ExecutionLevel;
24
+ /** @deprecated Retained for rolling upgrades; ignored by the decision gate. */
25
+ readonly unattendedExecutionGrant?: unknown;
23
26
  }
24
27
 
25
28
  export interface HarnessDecisionGateOptions {
@@ -41,17 +44,8 @@ export interface HarnessDecisionGateOptions {
41
44
  readonly requiredExecutionLevel: ExecutionLevel;
42
45
  readonly signal: AbortSignal;
43
46
  }) => void | Promise<void>;
44
- readonly authorizeUnattendedExecution?: (input: {
45
- grant: RuntimeUnattendedExecutionGrant;
46
- capability: string;
47
- operation: string;
48
- object?: unknown;
49
- toolCallId: string;
50
- toolName: string;
51
- input: unknown;
52
- requiredExecutionLevel: ExecutionLevel;
53
- signal: AbortSignal;
54
- }) => void | Promise<void>;
47
+ /** @deprecated Scheduled authorization is local and never calls this hook. */
48
+ readonly authorizeUnattendedExecution?: (input: unknown) => void | Promise<void>;
55
49
  readonly onUnattendedFailure?: (reason: string) => void | Promise<void>;
56
50
  readonly unattendedFailure?: () => string | null | undefined;
57
51
  }
@@ -105,11 +99,9 @@ export class HarnessDecisionGate {
105
99
  if (!(error instanceof UnattendedExecutionError)) throw error;
106
100
  return { block: { reason: error.message, terminate: true } };
107
101
  }
102
+ const effectiveLevel = this.effectiveExecutionLevel();
108
103
  const needsApproval = candidate.alwaysRequiresApproval ||
109
- requiresExecutionApproval(
110
- this.options.submission.executionLevel,
111
- requiredExecutionLevel,
112
- );
104
+ requiresExecutionApproval(effectiveLevel, requiredExecutionLevel);
113
105
  const executionId = `${this.options.submission.submissionId}:${event.toolCallId}`;
114
106
  let unattendedAuthorized = false;
115
107
  try {
@@ -240,6 +232,14 @@ export class HarnessDecisionGate {
240
232
  if (failure) throw new UnattendedExecutionError(failure);
241
233
  }
242
234
 
235
+ private effectiveExecutionLevel(): ExecutionLevel {
236
+ const submission = this.options.submission;
237
+ const ceiling = submission.origin === "scheduled"
238
+ ? submission.scheduledMaxExecutionLevel ?? "high"
239
+ : submission.executionLevel;
240
+ return minExecutionLevel(submission.executionLevel, ceiling);
241
+ }
242
+
243
243
  private async resolveExecutionLevel(candidate: PiToolCandidate, input: unknown): Promise<ExecutionLevel> {
244
244
  this.assertUnattendedActive();
245
245
  try {
@@ -253,7 +253,9 @@ export class HarnessDecisionGate {
253
253
  } catch (error) {
254
254
  if (this.options.submission.origin !== "scheduled") throw error;
255
255
  const reason = error instanceof Error ? error.message : String(error);
256
- return this.denyUnattended(`Unattended execution risk check failed for ${candidate.tool.name}: ${reason}`);
256
+ return this.denyUnattended(
257
+ `SCHEDULED_EXECUTION_RISK_CHECK_FAILED: ${candidate.tool.name}: ${reason}`,
258
+ );
257
259
  }
258
260
  }
259
261
 
@@ -265,52 +267,28 @@ export class HarnessDecisionGate {
265
267
  ): Promise<boolean> {
266
268
  if (this.options.submission.origin !== "scheduled") return false;
267
269
  this.assertUnattendedActive();
268
- const spec = candidate.unattendedExecution;
269
270
  if (candidate.interaction) {
270
271
  return this.denyUnattended(
271
- `Unattended execution requires human input for ${candidate.tool.name}: ${stableInput(input)}`,
272
+ `SCHEDULED_EXECUTION_REQUIRES_INTERACTION: ${candidate.tool.name}`,
272
273
  );
273
274
  }
274
275
  if (candidate.alwaysRequiresApproval) {
275
276
  return this.denyUnattended(
276
- `Unattended execution denied for ${candidate.tool.name}: this operation requires approval for every call`,
277
+ `SCHEDULED_EXECUTION_ALWAYS_REQUIRES_APPROVAL: ${candidate.tool.name}`,
277
278
  );
278
279
  }
279
- if (!spec) {
280
+ if (candidate.unattendedMode === "forbidden") {
280
281
  return this.denyUnattended(
281
- `Unattended execution denied for ${candidate.tool.name}: this Tool has no pre-authorizable operation contract`,
282
+ `SCHEDULED_EXECUTION_FORBIDDEN: ${candidate.tool.name}`,
282
283
  );
283
284
  }
284
- const grant = this.options.submission.unattendedExecutionGrant;
285
- const authorize = this.options.authorizeUnattendedExecution;
286
- if (!grant || !authorize) {
287
- const reason =
288
- `Unattended execution is not authorized for ${spec.capability}.${spec.operation}`;
289
- await this.options.onUnattendedFailure?.(reason);
290
- throw new UnattendedExecutionError(reason);
291
- }
292
- try {
293
- await authorize({
294
- grant,
295
- capability: spec.capability,
296
- operation: spec.operation,
297
- ...(spec.objectForInput
298
- ? { object: await spec.objectForInput(input) }
299
- : {}),
300
- toolCallId: invocation.toolCallId,
301
- toolName: candidate.tool.name,
302
- input,
303
- requiredExecutionLevel,
304
- signal: invocation.signal,
305
- });
306
- return true;
307
- } catch (error) {
308
- const reason = error instanceof Error ? error.message : String(error);
309
- const message =
310
- `Unattended execution denied for ${spec.capability}.${spec.operation}: ${reason}`;
311
- await this.options.onUnattendedFailure?.(message);
312
- throw new UnattendedExecutionError(message, { cause: error });
285
+ const effectiveLevel = this.effectiveExecutionLevel();
286
+ if (requiresExecutionApproval(effectiveLevel, requiredExecutionLevel)) {
287
+ return this.denyUnattended(
288
+ `SCHEDULED_EXECUTION_LEVEL_EXCEEDED: ${candidate.tool.name} requires ${requiredExecutionLevel}; effective level is ${effectiveLevel}`,
289
+ );
313
290
  }
291
+ return true;
314
292
  }
315
293
 
316
294
  private async denyUnattended(reason: string): Promise<never> {
@@ -25,34 +25,41 @@ const scheduleTriggerParameters = Type.Union([
25
25
  endDate: Type.Optional(Type.String({ description: "Inclusive last local date YYYY-MM-DD." })),
26
26
  }, { additionalProperties: false }),
27
27
  Type.Object({
28
- kind: Type.Literal("once-in"),
28
+ kind: Type.Literal("once-in", { description: "Run once after a delay." }),
29
29
  seconds: Type.Integer({
30
30
  minimum: 1,
31
31
  description: "Fire once, this many seconds from now.",
32
32
  }),
33
33
  }, { additionalProperties: false }),
34
34
  Type.Object({
35
- kind: Type.Literal("once-at"),
35
+ kind: Type.Literal("once-at", { description: "Run once at an absolute UTC time." }),
36
36
  at: Type.String({
37
37
  description:
38
38
  "Fire once at this absolute UTC ISO time, e.g. 2026-07-20T09:00:00Z.",
39
39
  }),
40
40
  }, { additionalProperties: false }),
41
41
  Type.Object({
42
- kind: Type.Literal("cron"),
42
+ kind: Type.Literal("cron", { description: "Repeat on a UTC cron schedule." }),
43
43
  cron: Type.String({
44
44
  description:
45
45
  "Repeat on a standard UTC cron, e.g. '0 9 * * *' = daily 09:00 UTC.",
46
46
  }),
47
47
  }, { additionalProperties: false }),
48
48
  Type.Object({
49
- kind: Type.Literal("interval"),
49
+ kind: Type.Literal("interval", { description: "Repeat at fixed intervals." }),
50
50
  seconds: Type.Integer({
51
51
  minimum: 1,
52
52
  description: "Repeat every this many seconds.",
53
53
  }),
54
54
  }, { additionalProperties: false }),
55
- ], { description: "When to fire." });
55
+ ], {
56
+ description:
57
+ "When to fire. Discriminate on the `kind` field (never \"type\"). " +
58
+ 'Shapes: {"kind":"local","rule":{"kind":"once","date":"2026-09-17","time":"14:06"}} runs once at 14:06 in `timezone`; ' +
59
+ '{"kind":"local","rule":{"kind":"daily","time":"09:00"}} runs daily; ' +
60
+ '{"kind":"local","rule":{"kind":"weekly","weekdays":[1,3],"time":"09:30"}} runs on Mondays and Wednesdays; ' +
61
+ '{"kind":"once-at","at":"2026-09-17T06:06:00Z"} fires once at an absolute UTC time.',
62
+ });
56
63
 
57
64
  const scheduleParameters = Type.Object({
58
65
  prompt: Type.String({ minLength: 1, maxLength: 1000, pattern: "\\S",
@@ -125,7 +132,7 @@ export function schedulePiToolCandidates(
125
132
  name: "schedule",
126
133
  label: "Schedule prompt",
127
134
  description:
128
- "Schedule a prompt to run LATER, autonomously. Required inputs: prompt (Requirement, at most 1000 Unicode characters), trigger and timezone. Prefer local once/daily/weekly rules. label is optional (at most 50 characters); the Host generates an omitted name. Ask for missing information. Use when the user asks to be reminded, or to run something after a delay / at a time / on a recurring basis. The user always reviews the proposed schedule before it is created. The scheduled prompt runs in its own dedicated session at each trigger with NO memory of this conversation, so phrase `prompt` as a fully standalone instruction. After scheduling, tell the user what you set.",
135
+ "Schedule a prompt to run LATER, autonomously. Required inputs: prompt (Requirement, at most 1000 Unicode characters), trigger and timezone. Prefer local once/daily/weekly rules. trigger discriminates on `kind` (never \"type\"); see the trigger parameter schema for detailed shapes. label is optional (at most 50 characters); the Host generates an omitted name. Ask for missing information. Use when the user asks to be reminded, or to run something after a delay / at a time / on a recurring basis. The user always reviews the proposed schedule before it is created. The scheduled prompt runs in its own dedicated session at each trigger with NO memory of this conversation, so phrase `prompt` as a fully standalone instruction. After scheduling, tell the user what you set.",
129
136
  parameters: scheduleParameters,
130
137
  async execute(_toolCallId, input, signal) {
131
138
  signal?.throwIfAborted();
@@ -156,7 +163,7 @@ export function schedulePiToolCandidates(
156
163
  name: "update_schedule",
157
164
  label: "Update schedule",
158
165
  description:
159
- "Update one scheduled task without changing its id or history. Required input: id. Optional prompt, trigger, label and timezone preserve the saved values when omitted. Every edit requires user confirmation; changing Requirement requires a fresh Host authorization check. Call `list_schedules` first and use an exact id. If multiple tasks plausibly match the user's wording, ask which one instead of guessing.",
166
+ "Update one scheduled task without changing its id or history. Required input: id. Optional prompt, trigger, label and timezone preserve the saved values when omitted; trigger uses the same `kind`-based shapes as `schedule`. Every edit requires user confirmation; changing Requirement requires a fresh Host authorization check. Call `list_schedules` first and use an exact id. If multiple tasks plausibly match the user's wording, ask which one instead of guessing.",
160
167
  parameters: updateScheduleParameters,
161
168
  async execute(_toolCallId, input, signal) {
162
169
  signal?.throwIfAborted();
@@ -255,8 +262,8 @@ export function schedulePiToolCandidates(
255
262
  if (!confirmation) return [];
256
263
  return [{ ...toolCandidate, alwaysRequiresApproval: true, confirmation: {
257
264
  async prepare(args, { intentId }) {
258
- validateToolArguments(toolCandidate.tool, fauxToolCall(name, args as Record<string, unknown>));
259
- const input = args as { id?: string; prompt?: string; trigger?: ScheduleSpec["trigger"]; label?: string; timezone?: string };
265
+ const validated = validateToolArguments(toolCandidate.tool, fauxToolCall(name, args as Record<string, unknown>));
266
+ const input = validated as { id?: string; prompt?: string; trigger?: ScheduleSpec["trigger"]; label?: string; timezone?: string };
260
267
  if (input.timezone !== undefined && !isValidTimeZone(input.timezone)) throw new Error("A valid IANA timezone is required");
261
268
  if (input.trigger?.kind === "local" && input.timezone) {
262
269
  const error = validateScheduleRuleSpec(toScheduleRuleSpec(input.trigger, input.timezone));
@@ -270,7 +277,7 @@ export function schedulePiToolCandidates(
270
277
  });
271
278
  if (prepared.id !== input.id) throw new Error("Host preparation changed the target Task ID");
272
279
  const fullInput = { prompt: prepared.prompt, label: prepared.label, trigger: prepared.trigger, timezone: prepared.timezone };
273
- validateToolArguments({ ...toolCandidate.tool, parameters: scheduleParameters }, fauxToolCall(name, fullInput));
280
+ void validateToolArguments({ ...toolCandidate.tool, parameters: scheduleParameters }, fauxToolCall(name, fullInput));
274
281
  if (!prepared.label?.trim() || !isValidTimeZone(prepared.timezone) || !prepared.candidateId?.trim() || !prepared.candidateVersion?.trim() || !prepared.authorization?.version?.trim() || !prepared.authorization.scope?.trim() || !Array.isArray(prepared.authorization.missing)) {
275
282
  throw new Error("Host preparation is incomplete");
276
283
  }