@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 +1 -1
- package/src/adapter/cloudflare/agent.ts +13 -10
- package/src/adapter/cloudflare/define-agent.ts +2 -0
- package/src/adapter/cloudflare/schema.ts +3 -0
- package/src/config/pinning.ts +11 -3
- package/src/config/ports.ts +2 -12
- package/src/lib/execution-level.ts +13 -0
- package/src/submission/admission.ts +4 -0
- package/src/submission/repository.ts +8 -1
- package/src/tools/compiler.ts +2 -0
- package/src/tools/decision-gate.ts +30 -52
- package/src/tools/schedule.ts +17 -10
package/package.json
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|
package/src/config/pinning.ts
CHANGED
|
@@ -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:
|
|
498
|
-
|
|
499
|
-
|
|
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,
|
package/src/config/ports.ts
CHANGED
|
@@ -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
|
-
|
|
1043
|
-
|
|
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}
|
package/src/tools/compiler.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
45
|
-
|
|
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(
|
|
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
|
-
`
|
|
272
|
+
`SCHEDULED_EXECUTION_REQUIRES_INTERACTION: ${candidate.tool.name}`,
|
|
272
273
|
);
|
|
273
274
|
}
|
|
274
275
|
if (candidate.alwaysRequiresApproval) {
|
|
275
276
|
return this.denyUnattended(
|
|
276
|
-
`
|
|
277
|
+
`SCHEDULED_EXECUTION_ALWAYS_REQUIRES_APPROVAL: ${candidate.tool.name}`,
|
|
277
278
|
);
|
|
278
279
|
}
|
|
279
|
-
if (
|
|
280
|
+
if (candidate.unattendedMode === "forbidden") {
|
|
280
281
|
return this.denyUnattended(
|
|
281
|
-
`
|
|
282
|
+
`SCHEDULED_EXECUTION_FORBIDDEN: ${candidate.tool.name}`,
|
|
282
283
|
);
|
|
283
284
|
}
|
|
284
|
-
const
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
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> {
|
package/src/tools/schedule.ts
CHANGED
|
@@ -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
|
-
], {
|
|
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
|
|
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 =
|
|
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
|
}
|