@springbrand/agent-runtime 0.3.0-alpha.7 → 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 +4 -7
- package/src/adapter/cloudflare/define-agent.ts +2 -0
- package/src/adapter/cloudflare/schema.ts +3 -0
- 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/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,
|
|
@@ -798,7 +797,7 @@ export abstract class AgentRuntimeKernel<
|
|
|
798
797
|
requestId: submission.requestId,
|
|
799
798
|
executionLevel: activeRuntime.snapshot.profile.executionLevel,
|
|
800
799
|
origin: submission.origin,
|
|
801
|
-
|
|
800
|
+
scheduledMaxExecutionLevel: submission.scheduledMaxExecutionLevel ?? undefined,
|
|
802
801
|
},
|
|
803
802
|
candidates: harnessInput.candidates,
|
|
804
803
|
telemetry: this.telemetry,
|
|
@@ -826,8 +825,6 @@ export abstract class AgentRuntimeKernel<
|
|
|
826
825
|
}
|
|
827
826
|
},
|
|
828
827
|
gateTool: harnessInput.platform.gateTool,
|
|
829
|
-
authorizeUnattendedExecution:
|
|
830
|
-
harnessInput.platform.authorizeUnattendedExecution,
|
|
831
828
|
onUnattendedFailure: (reason) => {
|
|
832
829
|
this.db.submissions.recordUnattendedFailure(submissionId, reason);
|
|
833
830
|
},
|
|
@@ -1431,6 +1428,8 @@ export abstract class AgentRuntimeKernel<
|
|
|
1431
1428
|
prompt: string,
|
|
1432
1429
|
options?: {
|
|
1433
1430
|
idempotencyKey?: string;
|
|
1431
|
+
scheduledMaxExecutionLevel?: ExecutionLevel;
|
|
1432
|
+
/** @deprecated Retained for rolling upgrades; ignored by Runtime. */
|
|
1434
1433
|
unattendedExecutionGrant?: RuntimeUnattendedExecutionGrant;
|
|
1435
1434
|
},
|
|
1436
1435
|
): Promise<SubmissionReceipt> {
|
|
@@ -1445,9 +1444,7 @@ export abstract class AgentRuntimeKernel<
|
|
|
1445
1444
|
requestId,
|
|
1446
1445
|
idempotencyKey: options?.idempotencyKey,
|
|
1447
1446
|
origin: "scheduled",
|
|
1448
|
-
|
|
1449
|
-
? pinnedUnattendedExecutionGrant(options.unattendedExecutionGrant)
|
|
1450
|
-
: undefined,
|
|
1447
|
+
scheduledMaxExecutionLevel: options?.scheduledMaxExecutionLevel,
|
|
1451
1448
|
},
|
|
1452
1449
|
SCHEDULED_STABLE_TIMEOUT_MS,
|
|
1453
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/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> {
|