@springbrand/agent-runtime 0.3.0-alpha.1 → 0.3.0-alpha.11
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 +8 -8
- package/src/adapter/cloudflare/agent.ts +160 -14
- package/src/adapter/cloudflare/define-agent.ts +12 -7
- package/src/adapter/cloudflare/index.ts +18 -0
- package/src/adapter/cloudflare/scheduling/schedule-rule.ts +322 -0
- package/src/adapter/cloudflare/scheduling/zoned-cron.ts +11 -6
- package/src/adapter/cloudflare/schema.ts +15 -0
- package/src/adapter/cloudflare/subagent/definition.ts +4 -4
- package/src/adapter/cloudflare/subagent/runner.ts +1 -1
- package/src/adapter/cloudflare/subagent/tools.ts +2 -2
- package/src/adapter/cloudflare/universal-agent/preparation.ts +3 -3
- package/src/adapter/cloudflare/universal-agent/tools.ts +4 -6
- package/src/adapter/cloudflare/workspace/version-control.ts +1 -1
- package/src/config/agent-context.ts +1 -1
- package/src/config/assembler.ts +29 -31
- package/src/config/assembly-snapshot.ts +1 -1
- package/src/config/definition.ts +1 -1
- package/src/config/harness-runtime.ts +9 -1
- package/src/config/model-guidance.ts +21 -0
- package/src/config/pinning.ts +22 -3
- package/src/config/ports.ts +27 -7
- package/src/config/public-contracts.ts +2 -2
- package/src/context/assembly.ts +1 -1
- package/src/context/extension/assembly.ts +2 -2
- package/src/context/memory/budget/gate.ts +19 -3
- package/src/conversation/harness-stream.ts +2 -1
- package/src/conversation/history.ts +2 -0
- package/src/conversation/message/projection.ts +38 -21
- package/src/conversation/state.ts +23 -2
- package/src/harness/session-harness.ts +160 -6
- package/src/index.ts +8 -7
- package/src/lib/artifacts.ts +18 -0
- package/src/lib/execution-level.ts +13 -0
- package/src/lib/prompt.ts +9 -10
- package/src/lib/token-estimate.ts +8 -0
- package/src/models/index.ts +32 -10
- package/src/models/openrouter/messages.ts +13 -8
- package/src/models/provider-streams.ts +45 -0
- package/src/session/cloudflare/migration.ts +71 -11
- package/src/skills/springbrand-worker-website/index.ts +1 -1
- package/src/submission/admission.ts +12 -1
- package/src/submission/contracts.ts +42 -2
- package/src/submission/delivery.ts +1 -1
- package/src/submission/finalization.ts +1 -1
- package/src/submission/repository.ts +64 -4
- package/src/submission/unattended.ts +40 -0
- package/src/telemetry/coordinator.ts +1 -1
- package/src/telemetry/subagent-lifecycle.ts +1 -1
- package/src/telemetry/turn-events.ts +1 -1
- package/src/tools/README.md +44 -0
- package/src/tools/{ai-adapter.ts → adapters/ai-to-pi.ts} +6 -9
- package/src/tools/{declared.ts → adapters/declared.ts} +6 -2
- package/src/tools/{nested-tools.ts → adapters/pi-to-ai.ts} +4 -4
- package/src/tools/assistant/ask-user.ts +317 -0
- package/src/tools/assistant/candidate.ts +15 -0
- package/src/tools/assistant/followups.ts +34 -0
- package/src/tools/assistant/index.ts +19 -0
- package/src/tools/assistant/memory.ts +73 -0
- package/src/tools/assistant/plan.ts +98 -0
- package/src/tools/{time.ts → assistant/time.ts} +1 -1
- package/src/tools/{core-host.ts → codemode/adapter.ts} +50 -53
- package/src/tools/{core.ts → codemode/index.ts} +36 -92
- package/src/tools/codemode/workspace-connector.ts +198 -0
- package/src/tools/extension/index.ts +42 -0
- package/src/tools/index.ts +29 -26
- package/src/tools/{gateway.ts → mcp/gateway.ts} +19 -19
- package/src/tools/{mcp.ts → mcp/index.ts} +5 -5
- package/src/tools/runtime/activation.ts +40 -0
- package/src/tools/{approval → runtime/approval}/repository.ts +13 -12
- package/src/tools/{compiler.ts → runtime/compiler.ts} +84 -147
- package/src/tools/runtime/contracts.ts +140 -0
- package/src/tools/{decision-gate.ts → runtime/decision-gate.ts} +147 -37
- package/src/tools/{human-decisions.ts → runtime/human-decisions.ts} +4 -4
- package/src/tools/{interaction → runtime/interaction}/repository.ts +1 -7
- package/src/tools/runtime/result.ts +22 -0
- package/src/tools/runtime/unattended-error.ts +4 -0
- package/src/tools/sandbox/index.ts +228 -0
- package/src/tools/schedule/index.ts +301 -0
- package/src/tools/skill/contracts.ts +20 -0
- package/src/tools/skill/index.ts +99 -0
- package/src/tools/skill/resources.ts +322 -0
- package/src/tools/skill/scripts.ts +168 -0
- package/src/tools/{subagent-authority.ts → subagent/authority.ts} +31 -31
- package/src/tools/{subagent.ts → subagent/index.ts} +34 -37
- package/src/tools/{subagent-run-repository.ts → subagent/repository.ts} +1 -1
- package/src/tools/{subagent-runtime.ts → subagent/runtime.ts} +1 -1
- package/src/tools/{temporary-agent → subagent/temporary}/approvals.ts +4 -7
- package/src/tools/{temporary-agent → subagent/temporary}/core.ts +1 -1
- package/src/tools/{temporary-agent → subagent/temporary}/runner.ts +1 -1
- package/src/tools/{temporary-agent → subagent/temporary}/workspace.ts +1 -4
- package/src/tools/{web-fetch.ts → web/fetch.ts} +4 -13
- package/src/tools/web/index.ts +2 -0
- package/src/tools/web/search/api.ts +1320 -0
- package/src/tools/web/search/index.ts +2 -0
- package/src/tools/{web-search/web-search.ts → web/search/search.ts} +5 -7
- package/src/tools/{workspace-sandbox.ts → workspace/index.ts} +12 -272
- package/src/tools/{workspace-revision.ts → workspace/revision.ts} +3 -3
- package/src/tools/base.ts +0 -534
- package/src/tools/schedule.ts +0 -261
- package/src/tools/skill.ts +0 -565
- package/src/tools/web-search/api.ts +0 -1318
- package/src/tools/web-search/index.ts +0 -2
- package/src/tools/workspace-state-connector.ts +0 -146
- package/src/tools/{approval → runtime/approval}/index.ts +0 -0
- package/src/tools/{interaction → runtime/interaction}/index.ts +0 -0
- package/src/tools/{temporary-agent → subagent/temporary}/coordinator.ts +0 -0
- package/src/tools/{subagent-types → subagent/types}/contract.ts +0 -0
- package/src/tools/{subagent-types → subagent/types}/extract/index.ts +0 -0
- package/src/tools/{subagent-types → subagent/types}/fanout/index.ts +0 -0
- package/src/tools/{subagent-types → subagent/types}/registry.ts +1 -1
- /package/src/tools/{workspace-versioning.ts → workspace/versioning.ts} +0 -0
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
Api,
|
|
3
|
+
Model,
|
|
4
|
+
ProviderEnv,
|
|
5
|
+
ProviderStreams,
|
|
6
|
+
} from "@earendil-works/pi-ai";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* ProviderStreams 是普通对象,不是 class。完整适配先 spread 底层全部方法,再覆盖需要装饰的入口。
|
|
10
|
+
* 只手写 stream / streamSimple 会丢掉 Pi 后续挂上的 fetchDeferred / cancelDeferred。
|
|
11
|
+
*/
|
|
12
|
+
export function overlayProviderStreams(
|
|
13
|
+
base: ProviderStreams,
|
|
14
|
+
overlay: Partial<ProviderStreams>,
|
|
15
|
+
): ProviderStreams {
|
|
16
|
+
return { ...base, ...overlay };
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* 在保留其余方法的前提下,把每个已知的 model 入口改写到 remap 后的模型。
|
|
21
|
+
* 未知的未来方法仍通过 spread 透传,避免再出现白名单丢方法。
|
|
22
|
+
*/
|
|
23
|
+
export function remapProviderStreamModels(
|
|
24
|
+
streams: ProviderStreams,
|
|
25
|
+
remap: (model: Model<Api>, env: ProviderEnv | undefined) => Model<Api>,
|
|
26
|
+
): ProviderStreams {
|
|
27
|
+
return overlayProviderStreams(streams, {
|
|
28
|
+
stream: (model, context, options) =>
|
|
29
|
+
streams.stream(remap(model, options?.env), context, options),
|
|
30
|
+
streamSimple: (model, context, options) =>
|
|
31
|
+
streams.streamSimple(remap(model, options?.env), context, options),
|
|
32
|
+
...(streams.fetchDeferred
|
|
33
|
+
? {
|
|
34
|
+
fetchDeferred: (model, handle, options) =>
|
|
35
|
+
streams.fetchDeferred!(remap(model, options?.env), handle, options),
|
|
36
|
+
}
|
|
37
|
+
: {}),
|
|
38
|
+
...(streams.cancelDeferred
|
|
39
|
+
? {
|
|
40
|
+
cancelDeferred: (model, handle, options) =>
|
|
41
|
+
streams.cancelDeferred!(remap(model, options?.env), handle, options),
|
|
42
|
+
}
|
|
43
|
+
: {}),
|
|
44
|
+
});
|
|
45
|
+
}
|
|
@@ -43,6 +43,13 @@ const RETIRED_SCHEDULE_CALLBACKS = [
|
|
|
43
43
|
"_chatRecoveryRetry",
|
|
44
44
|
] as const;
|
|
45
45
|
|
|
46
|
+
const LEGACY_TERMINAL_SUBMISSION_STATUSES = new Set([
|
|
47
|
+
"completed",
|
|
48
|
+
"failed",
|
|
49
|
+
"aborted",
|
|
50
|
+
"skipped",
|
|
51
|
+
]);
|
|
52
|
+
|
|
46
53
|
type LegacyRecord = Record<string, unknown> & {
|
|
47
54
|
id: string;
|
|
48
55
|
parentId: string | null;
|
|
@@ -310,6 +317,34 @@ function hasToolExecution(value: ToolExecution | undefined): boolean {
|
|
|
310
317
|
typeof value.args === "object" && !Array.isArray(value.args));
|
|
311
318
|
}
|
|
312
319
|
|
|
320
|
+
/** A terminal Submission cannot resume, so its stale lane pointer is not active work. */
|
|
321
|
+
function clearTerminalOperationPointers(sql: SqlStorage): void {
|
|
322
|
+
if (!hasTable(sql, "pi_submissions")) return;
|
|
323
|
+
const lanes = sql.exec<{ key: string; body: string }>(
|
|
324
|
+
"SELECT key, body FROM pi_values WHERE namespace = ?",
|
|
325
|
+
laneState("").namespace,
|
|
326
|
+
).toArray();
|
|
327
|
+
for (const lane of lanes) {
|
|
328
|
+
const state = JSON.parse(lane.body) as LaneState;
|
|
329
|
+
if (state.currentOperationId === null) continue;
|
|
330
|
+
const terminal = sql.exec(
|
|
331
|
+
`SELECT 1 FROM pi_submissions
|
|
332
|
+
WHERE submission_id = ?
|
|
333
|
+
AND status IN ('completed', 'failed', 'aborted', 'skipped')
|
|
334
|
+
LIMIT 1`,
|
|
335
|
+
state.currentOperationId,
|
|
336
|
+
).toArray().length === 1;
|
|
337
|
+
if (!terminal) continue;
|
|
338
|
+
sql.exec(
|
|
339
|
+
`UPDATE pi_values
|
|
340
|
+
SET body = json_set(body, '$.currentOperationId', json('null'))
|
|
341
|
+
WHERE namespace = ? AND key = ?`,
|
|
342
|
+
laneState("").namespace,
|
|
343
|
+
lane.key,
|
|
344
|
+
);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
313
348
|
/** Old active tool outcomes cannot be reconstructed from error text or approval facts. */
|
|
314
349
|
function assertActiveToolProvenance(sql: SqlStorage): void {
|
|
315
350
|
const read = <T>(address: Value<T>): T | undefined => {
|
|
@@ -379,6 +414,7 @@ export function migrateCurrentPiSession(
|
|
|
379
414
|
if (version !== 1 && version !== 2) {
|
|
380
415
|
throw new Error(`Unsupported Cloudflare Pi storage version: ${version}`);
|
|
381
416
|
}
|
|
417
|
+
clearTerminalOperationPointers(sql);
|
|
382
418
|
assertActiveToolProvenance(sql);
|
|
383
419
|
if (version === 1) removeRetiredRuntimeArtifacts(sql);
|
|
384
420
|
repairDuplicatedUserSidecars(sql);
|
|
@@ -400,7 +436,6 @@ function assertSameToolResult(
|
|
|
400
436
|
"content",
|
|
401
437
|
"details",
|
|
402
438
|
"usage",
|
|
403
|
-
"addedToolNames",
|
|
404
439
|
"isError",
|
|
405
440
|
] as const) {
|
|
406
441
|
if (!isEqual(left[key], right[key]))
|
|
@@ -420,11 +455,8 @@ function legacyToolResultMessage(input: {
|
|
|
420
455
|
toolCallId: input.toolCallId,
|
|
421
456
|
toolName: input.toolName,
|
|
422
457
|
content: input.result.content ?? [],
|
|
423
|
-
details: input.result.details,
|
|
458
|
+
details: input.result.details as JsonValue | undefined,
|
|
424
459
|
...(input.result.usage ? { usage: input.result.usage } : {}),
|
|
425
|
-
...(input.result.addedToolNames?.length
|
|
426
|
-
? { addedToolNames: input.result.addedToolNames }
|
|
427
|
-
: {}),
|
|
428
460
|
isError: input.isError,
|
|
429
461
|
timestamp: input.timestamp,
|
|
430
462
|
};
|
|
@@ -680,6 +712,20 @@ export function migrateLegacyPiSession(
|
|
|
680
712
|
.map((row) => [row.message_id, row.submission_id] as const)
|
|
681
713
|
: [],
|
|
682
714
|
);
|
|
715
|
+
const submissionStatuses = new Map(
|
|
716
|
+
hasTable(sql, "pi_submissions")
|
|
717
|
+
? sql
|
|
718
|
+
.exec<{ submission_id: string; status: string }>(
|
|
719
|
+
"SELECT submission_id, status FROM pi_submissions",
|
|
720
|
+
)
|
|
721
|
+
.toArray()
|
|
722
|
+
.map((row) => [row.submission_id, row.status] as const)
|
|
723
|
+
: [],
|
|
724
|
+
);
|
|
725
|
+
const terminalSubmission = (submissionId: string): boolean =>
|
|
726
|
+
LEGACY_TERMINAL_SUBMISSION_STATUSES.has(
|
|
727
|
+
submissionStatuses.get(submissionId) ?? "",
|
|
728
|
+
);
|
|
683
729
|
const results = new Map<string, ToolResultMessage>();
|
|
684
730
|
for (const entry of converted.values()) {
|
|
685
731
|
if (entry.type !== "message" || entry.message.role !== "toolResult")
|
|
@@ -809,8 +855,12 @@ export function migrateLegacyPiSession(
|
|
|
809
855
|
if (!knownSubmissions.has(row.submission_id))
|
|
810
856
|
throw new Error("Dangling recovery submission");
|
|
811
857
|
const record = decodePiToolRecoveryMilestone(row.body);
|
|
812
|
-
if (!record || record.turnId !== row.submission_id)
|
|
858
|
+
if (!record || record.turnId !== row.submission_id) {
|
|
859
|
+
if (LEGACY_TERMINAL_SUBMISSION_STATUSES.has(
|
|
860
|
+
submissionStatuses.get(row.submission_id) ?? "",
|
|
861
|
+
)) continue;
|
|
813
862
|
throw new Error("Invalid legacy recovery milestone");
|
|
863
|
+
}
|
|
814
864
|
const bodies = bySubmission.get(row.submission_id) ?? [];
|
|
815
865
|
bodies.push(row.body);
|
|
816
866
|
bySubmission.set(row.submission_id, bodies);
|
|
@@ -819,8 +869,12 @@ export function migrateLegacyPiSession(
|
|
|
819
869
|
.canonicalToolResults) {
|
|
820
870
|
const key = `${row.submission_id}:${result.toolCallId}`;
|
|
821
871
|
const previous = results.get(key);
|
|
822
|
-
if (previous
|
|
823
|
-
|
|
872
|
+
if (previous && !terminalSubmission(row.submission_id)) {
|
|
873
|
+
assertSameToolResult(previous, result);
|
|
874
|
+
}
|
|
875
|
+
if (!previous || !terminalSubmission(row.submission_id)) {
|
|
876
|
+
results.set(key, result);
|
|
877
|
+
}
|
|
824
878
|
}
|
|
825
879
|
}
|
|
826
880
|
}
|
|
@@ -832,8 +886,12 @@ export function migrateLegacyPiSession(
|
|
|
832
886
|
if (call.result) {
|
|
833
887
|
const key = `${submissionId}:${call.toolCallId}`;
|
|
834
888
|
const previous = results.get(key);
|
|
835
|
-
if (previous
|
|
836
|
-
|
|
889
|
+
if (previous && !terminalSubmission(submissionId)) {
|
|
890
|
+
assertSameToolResult(previous, call.result);
|
|
891
|
+
}
|
|
892
|
+
if (!previous || !terminalSubmission(submissionId)) {
|
|
893
|
+
results.set(key, call.result);
|
|
894
|
+
}
|
|
837
895
|
}
|
|
838
896
|
}
|
|
839
897
|
}
|
|
@@ -901,7 +959,9 @@ export function migrateLegacyPiSession(
|
|
|
901
959
|
const recorded = results.get(
|
|
902
960
|
`${row.submission_id}:${row.tool_call_id}`,
|
|
903
961
|
);
|
|
904
|
-
if (recorded
|
|
962
|
+
if (recorded && !terminalSubmission(row.submission_id)) {
|
|
963
|
+
assertSameToolResult(recorded, expected);
|
|
964
|
+
}
|
|
905
965
|
}
|
|
906
966
|
}
|
|
907
967
|
// The old table remains an inert backup. Failure rolls back DDL, data and the version together.
|
|
@@ -163,7 +163,7 @@ export const SPRINGBRAND_WORKER_WEBSITE_SKILL = {
|
|
|
163
163
|
"5. Call `get_browser_console_logs` for the home page and for every primary internal page. Check navigation responses, missing assets, exceptions, CSP or CORS failures, and client fetch failures.",
|
|
164
164
|
"6. Open the Website with the available browser evidence tools. For a multi-page Website, follow real links from the home page to every primary internal page and verify navigation back to the home page. Check at least the normal desktop viewport and one narrow viewport; capture and inspect a screenshot when visual appearance matters.",
|
|
165
165
|
"7. Fix every failure, redeploy, and repeat the same checks. A clean home page does not prove a multi-page Website works.",
|
|
166
|
-
"8. After browser verification succeeds, call `save_creation` exactly once with the existing entry HTML Working Copy path. For the recommended layout, that is `/creations/<website-name>/public/index.html`.",
|
|
166
|
+
"8. After browser verification succeeds, call `save_creation` exactly once with a user-visible title and the existing entry HTML Working Copy path. For the recommended layout, that path is `/creations/<website-name>/public/index.html`.",
|
|
167
167
|
"9. Do not call the Website complete until `save_creation` returns an `artifactId`. Do not save referenced assets as separate Creations.",
|
|
168
168
|
"",
|
|
169
169
|
"## Publication contract",
|
|
@@ -12,7 +12,9 @@ import {
|
|
|
12
12
|
SubmissionQueueFullError,
|
|
13
13
|
type SubmissionInput,
|
|
14
14
|
} from "./lifecycle";
|
|
15
|
-
import { isTerminalSubmissionStatus, submissionAbortReason, type StoredSubmission, type SubmissionRepository } from "./repository";
|
|
15
|
+
import { isTerminalSubmissionStatus, submissionAbortReason, type StoredSubmission, type SubmissionOrigin, type SubmissionRepository } from "./repository";
|
|
16
|
+
import type { RuntimeUnattendedExecutionGrant } from "./contracts";
|
|
17
|
+
import type { ExecutionLevel } from "../lib/execution-level";
|
|
16
18
|
|
|
17
19
|
// Pending records retain identity indefinitely; only recent attempts reserve capacity.
|
|
18
20
|
const ADMISSION_ATTEMPT_WINDOW_MS = 60_000;
|
|
@@ -23,6 +25,12 @@ export interface SubmitMessageOptions {
|
|
|
23
25
|
userMessageId?: string;
|
|
24
26
|
userMessage?: UIMessage & { role: "user" };
|
|
25
27
|
regenerate?: boolean;
|
|
28
|
+
/** `scheduled` marks a Run injection that locks ordinary input until it ends. */
|
|
29
|
+
origin?: SubmissionOrigin;
|
|
30
|
+
/** Maximum execution level allowed by the Schedule Definition. */
|
|
31
|
+
scheduledMaxExecutionLevel?: ExecutionLevel;
|
|
32
|
+
/** Trusted Host grant; accepted only for a scheduled Submission. */
|
|
33
|
+
unattendedExecutionGrant?: RuntimeUnattendedExecutionGrant;
|
|
26
34
|
/** Internal transfer of a durable accepted steer; never a public message option. */
|
|
27
35
|
steerHandoff?: { submissionId: string; steerId: string };
|
|
28
36
|
}
|
|
@@ -182,6 +190,9 @@ export class SubmissionAdmission {
|
|
|
182
190
|
? options.userMessageId ?? null
|
|
183
191
|
: null,
|
|
184
192
|
runId,
|
|
193
|
+
origin: options.origin,
|
|
194
|
+
scheduledMaxExecutionLevel: options.scheduledMaxExecutionLevel,
|
|
195
|
+
unattendedExecutionGrant: options.unattendedExecutionGrant,
|
|
185
196
|
});
|
|
186
197
|
const admitted = this.options.submissions.find(runId)!;
|
|
187
198
|
this.options.onAdmitted(admitted, admission?.accepted === true);
|
|
@@ -13,6 +13,8 @@ export const APPROVAL_DECISIONS = [
|
|
|
13
13
|
"allow_level",
|
|
14
14
|
] as const satisfies readonly ApprovalDecision["decision"][];
|
|
15
15
|
|
|
16
|
+
export type { RuntimeUnattendedExecutionGrant } from "./unattended";
|
|
17
|
+
|
|
16
18
|
/**
|
|
17
19
|
* 记录一次待审批 Tool 执行的身份、摘要和原始输入。
|
|
18
20
|
*
|
|
@@ -58,12 +60,35 @@ export type ScheduleJob =
|
|
|
58
60
|
*
|
|
59
61
|
* 触发条件与工作放在同一份规格里,使更新时可以原地替换两者,不用改变任务标识。
|
|
60
62
|
*/
|
|
63
|
+
/**
|
|
64
|
+
* 一条按任务时区解释的当地时间规则。
|
|
65
|
+
*
|
|
66
|
+
* @remarks
|
|
67
|
+
* `once` 在单个本地日历日触发;`daily` 每个本地日触发;`weekly` 在选中的每个本地星期触发
|
|
68
|
+
* (0 = 周日 … 6 = 周六)。所有星期共用同一个本地钟点。
|
|
69
|
+
*
|
|
70
|
+
* 规则只描述当地墙上时间;缺失/重复钟点、起止日期和夏令时语义由
|
|
71
|
+
* `adapter/cloudflare/scheduling` 的公共计算边界处理。
|
|
72
|
+
*/
|
|
73
|
+
export type LocalScheduleRule =
|
|
74
|
+
| { kind: "once"; date: string; time: string }
|
|
75
|
+
| { kind: "daily"; time: string }
|
|
76
|
+
| { kind: "weekly"; weekdays: number[]; time: string };
|
|
77
|
+
|
|
61
78
|
export interface ScheduleSpec {
|
|
62
79
|
trigger:
|
|
63
80
|
| { kind: "once-at"; at: string }
|
|
64
81
|
| { kind: "once-in"; seconds: number }
|
|
65
82
|
| { kind: "cron"; cron: string }
|
|
66
|
-
| { kind: "interval"; seconds: number }
|
|
83
|
+
| { kind: "interval"; seconds: number }
|
|
84
|
+
| {
|
|
85
|
+
kind: "local";
|
|
86
|
+
rule: LocalScheduleRule;
|
|
87
|
+
/** 含当天的最早生效本地日期 `YYYY-MM-DD`。 */
|
|
88
|
+
startDate?: string;
|
|
89
|
+
/** 含当天的最后生效本地日期 `YYYY-MM-DD`。 */
|
|
90
|
+
endDate?: string;
|
|
91
|
+
};
|
|
67
92
|
job: ScheduleJob;
|
|
68
93
|
label?: string;
|
|
69
94
|
/**
|
|
@@ -115,7 +140,9 @@ export type MessageDelivery = "steer" | "enqueue";
|
|
|
115
140
|
export type MessageDispatchErrorCode =
|
|
116
141
|
| "queue_full"
|
|
117
142
|
| "not_steerable"
|
|
118
|
-
| "invalid_message"
|
|
143
|
+
| "invalid_message"
|
|
144
|
+
/** A scheduled Run owns this Session's input until it reaches a terminal state. */
|
|
145
|
+
| "input_locked";
|
|
119
146
|
|
|
120
147
|
export type MessageDispatchReceipt =
|
|
121
148
|
| {
|
|
@@ -139,3 +166,16 @@ export type MessageDispatchReceipt =
|
|
|
139
166
|
code: MessageDispatchErrorCode;
|
|
140
167
|
message: string;
|
|
141
168
|
};
|
|
169
|
+
|
|
170
|
+
/** Complete, credential-free task version prepared by a trusted Host for human confirmation. */
|
|
171
|
+
export interface RuntimeSchedulePreparation {
|
|
172
|
+
candidateId: string;
|
|
173
|
+
candidateVersion: string;
|
|
174
|
+
id?: string;
|
|
175
|
+
prompt: string;
|
|
176
|
+
label: string;
|
|
177
|
+
trigger: ScheduleSpec["trigger"];
|
|
178
|
+
timezone: string;
|
|
179
|
+
nextRunAt?: number;
|
|
180
|
+
authorization: { version: string; missing: string[]; scope: string };
|
|
181
|
+
}
|
|
@@ -2,7 +2,7 @@ import type { UIMessage } from "ai";
|
|
|
2
2
|
import type { PiCanonicalUserInput } from "../conversation/message/contract";
|
|
3
3
|
import type { RuntimeTelemetryCoordinator } from "../telemetry/coordinator";
|
|
4
4
|
import type { RuntimeTurnEvents } from "../telemetry/turn-events";
|
|
5
|
-
import type { HarnessHumanDecisions } from "../tools/human-decisions";
|
|
5
|
+
import type { HarnessHumanDecisions } from "../tools/runtime/human-decisions";
|
|
6
6
|
import type { SubmitMessageOptions } from "./admission";
|
|
7
7
|
import {
|
|
8
8
|
projectSubmissionReceipt,
|
|
@@ -4,7 +4,7 @@ import type { ConversationHistory } from "../conversation/history";
|
|
|
4
4
|
import type { MessageUiRepository } from "../conversation/repository";
|
|
5
5
|
import type { RuntimeTelemetryCoordinator } from "../telemetry/coordinator";
|
|
6
6
|
import type { RuntimeTurnEvents } from "../telemetry/turn-events";
|
|
7
|
-
import type { HarnessHumanDecisions } from "../tools/human-decisions";
|
|
7
|
+
import type { HarnessHumanDecisions } from "../tools/runtime/human-decisions";
|
|
8
8
|
import { USER_STOP_REASON } from "./contracts";
|
|
9
9
|
import type { SubmissionOutcome } from "./lifecycle";
|
|
10
10
|
import { isTerminalSubmissionStatus, type StoredSubmission, type SubmissionRepository } from "./repository";
|
|
@@ -1,5 +1,7 @@
|
|
|
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";
|
|
4
|
+
import type { RuntimeUnattendedExecutionGrant } from "./unattended";
|
|
3
5
|
|
|
4
6
|
// Submission 的统一定义见 ./index.ts。
|
|
5
7
|
// #region 状态与行类型
|
|
@@ -11,6 +13,14 @@ export type SubmissionStatus =
|
|
|
11
13
|
| "skipped"
|
|
12
14
|
| "error";
|
|
13
15
|
|
|
16
|
+
/**
|
|
17
|
+
* 谁发起了这条 Submission。
|
|
18
|
+
*
|
|
19
|
+
* `scheduled` 是定时 Run 的注入:它在本 Session 未结束期间独占输入,
|
|
20
|
+
* 普通 `user` Submission 必须被拒绝,而不是排队等它结束。
|
|
21
|
+
*/
|
|
22
|
+
export type SubmissionOrigin = "user" | "scheduled";
|
|
23
|
+
|
|
14
24
|
const TERMINAL_SUBMISSION_STATUSES: ReadonlySet<SubmissionStatus> = new Set([
|
|
15
25
|
"completed",
|
|
16
26
|
"aborted",
|
|
@@ -23,7 +33,7 @@ const TERMINAL_SUBMISSION_STATUSES: ReadonlySet<SubmissionStatus> = new Set([
|
|
|
23
33
|
// 终态集合收口在一处防止各生命周期的判断漂移;增加状态时必须先确认它是否真的不可再继续。
|
|
24
34
|
export function isTerminalSubmissionStatus(
|
|
25
35
|
status: SubmissionStatus,
|
|
26
|
-
):
|
|
36
|
+
): status is Exclude<SubmissionStatus, "pending" | "running"> {
|
|
27
37
|
return TERMINAL_SUBMISSION_STATUSES.has(status);
|
|
28
38
|
}
|
|
29
39
|
|
|
@@ -46,6 +56,10 @@ export interface StoredSubmission {
|
|
|
46
56
|
userMessageId: string | null;
|
|
47
57
|
regenerateMessageId: string | null;
|
|
48
58
|
runId: string | null;
|
|
59
|
+
origin: SubmissionOrigin;
|
|
60
|
+
scheduledMaxExecutionLevel: ExecutionLevel | null;
|
|
61
|
+
unattendedExecutionGrant: RuntimeUnattendedExecutionGrant | null;
|
|
62
|
+
unattendedFailure: string | null;
|
|
49
63
|
retryable?: boolean;
|
|
50
64
|
}
|
|
51
65
|
|
|
@@ -69,6 +83,9 @@ export interface NewSubmission {
|
|
|
69
83
|
userMessageId?: string | null;
|
|
70
84
|
regenerateMessageId?: string | null;
|
|
71
85
|
runId?: string | null;
|
|
86
|
+
origin?: SubmissionOrigin;
|
|
87
|
+
scheduledMaxExecutionLevel?: ExecutionLevel;
|
|
88
|
+
unattendedExecutionGrant?: RuntimeUnattendedExecutionGrant;
|
|
72
89
|
}
|
|
73
90
|
|
|
74
91
|
type SubmissionRow = {
|
|
@@ -90,7 +107,11 @@ type SubmissionRow = {
|
|
|
90
107
|
user_message_id: string | null;
|
|
91
108
|
regenerate_message_id: string | null;
|
|
92
109
|
run_id: string | null;
|
|
110
|
+
origin: string | null;
|
|
111
|
+
scheduled_max_execution_level: string | null;
|
|
93
112
|
admission_retryable: number | null;
|
|
113
|
+
unattended_execution_grant_json: string | null;
|
|
114
|
+
unattended_failure: string | null;
|
|
94
115
|
};
|
|
95
116
|
|
|
96
117
|
// #endregion
|
|
@@ -118,6 +139,12 @@ function mapRow(row: SubmissionRow): StoredSubmission {
|
|
|
118
139
|
userMessageId: row.user_message_id,
|
|
119
140
|
regenerateMessageId: row.regenerate_message_id,
|
|
120
141
|
runId: row.run_id,
|
|
142
|
+
origin: row.origin === "scheduled" ? "scheduled" : "user",
|
|
143
|
+
scheduledMaxExecutionLevel: row.scheduled_max_execution_level as ExecutionLevel | null,
|
|
144
|
+
unattendedExecutionGrant: row.unattended_execution_grant_json
|
|
145
|
+
? JSON.parse(row.unattended_execution_grant_json) as RuntimeUnattendedExecutionGrant
|
|
146
|
+
: null,
|
|
147
|
+
unattendedFailure: row.unattended_failure,
|
|
121
148
|
retryable: row.admission_retryable === null
|
|
122
149
|
? undefined
|
|
123
150
|
: row.admission_retryable === 1,
|
|
@@ -150,7 +177,11 @@ export class SubmissionRepository {
|
|
|
150
177
|
queued_input_json, queued_ui_message_json, user_message_id,
|
|
151
178
|
regenerate_message_id,
|
|
152
179
|
run_id,
|
|
153
|
-
|
|
180
|
+
origin,
|
|
181
|
+
scheduled_max_execution_level,
|
|
182
|
+
admission_retryable,
|
|
183
|
+
unattended_execution_grant_json,
|
|
184
|
+
unattended_failure
|
|
154
185
|
FROM pi_submissions
|
|
155
186
|
WHERE submission_id = ${id}
|
|
156
187
|
`[0];
|
|
@@ -251,7 +282,7 @@ export class SubmissionRepository {
|
|
|
251
282
|
created_at, assembly_revision, assembly_descriptor, runtime_key,
|
|
252
283
|
assistant_message_id, queued_input_json, queued_ui_message_json,
|
|
253
284
|
user_message_id, regenerate_message_id,
|
|
254
|
-
run_id
|
|
285
|
+
run_id, origin, scheduled_max_execution_level, unattended_execution_grant_json
|
|
255
286
|
) VALUES (
|
|
256
287
|
${s.submissionId},
|
|
257
288
|
${s.requestId},
|
|
@@ -267,11 +298,31 @@ export class SubmissionRepository {
|
|
|
267
298
|
${s.queuedUiMessageJson ?? null},
|
|
268
299
|
${s.userMessageId ?? null},
|
|
269
300
|
${s.regenerateMessageId ?? null},
|
|
270
|
-
${s.runId ?? s.submissionId}
|
|
301
|
+
${s.runId ?? s.submissionId},
|
|
302
|
+
${s.origin ?? "user"},
|
|
303
|
+
${s.scheduledMaxExecutionLevel ?? null},
|
|
304
|
+
${s.unattendedExecutionGrant
|
|
305
|
+
? JSON.stringify(s.unattendedExecutionGrant)
|
|
306
|
+
: null}
|
|
271
307
|
)
|
|
272
308
|
`;
|
|
273
309
|
}
|
|
274
310
|
|
|
311
|
+
/**
|
|
312
|
+
* 是否存在一条未结束的定时 Submission。
|
|
313
|
+
*
|
|
314
|
+
* 定时 Run 未结束时它独占本 Session 的输入:普通消息在准入前被拒绝,
|
|
315
|
+
* 不排队等待。Run 进入终态后查询自然为空,普通追问恢复。
|
|
316
|
+
*/
|
|
317
|
+
hasUnfinishedScheduled(): boolean {
|
|
318
|
+
return (
|
|
319
|
+
this.sql<{ count: number }>`
|
|
320
|
+
SELECT COUNT(*) AS count FROM pi_submissions
|
|
321
|
+
WHERE status IN ('pending', 'running') AND origin = 'scheduled'
|
|
322
|
+
`[0]?.count ?? 0
|
|
323
|
+
) > 0;
|
|
324
|
+
}
|
|
325
|
+
|
|
275
326
|
insertRejected(s: {
|
|
276
327
|
submissionId: string;
|
|
277
328
|
requestId: string;
|
|
@@ -310,6 +361,15 @@ export class SubmissionRepository {
|
|
|
310
361
|
return updated.length === 1;
|
|
311
362
|
}
|
|
312
363
|
|
|
364
|
+
recordUnattendedFailure(id: string, reason: string): void {
|
|
365
|
+
this.sql`
|
|
366
|
+
UPDATE pi_submissions
|
|
367
|
+
SET unattended_failure = COALESCE(unattended_failure, ${reason})
|
|
368
|
+
WHERE submission_id = ${id}
|
|
369
|
+
AND status IN ('pending', 'running')
|
|
370
|
+
`;
|
|
371
|
+
}
|
|
372
|
+
|
|
313
373
|
// 只在当前状态属于 from 时把 Submission 切换到 to,否则返回 false。
|
|
314
374
|
// SubmissionLifecycle 开始 Turn 时调用,调用方用返回值识别过期或重复的状态切换。
|
|
315
375
|
// Cloudflare SQLite 查询是同步的,两条语句之间不会让其他事件交错;UPDATE 再次带旧状态条件,不能去掉这层幂等保护。
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/** Host-confirmed authority pinned to one scheduled task Requirement version.
|
|
2
|
+
* Credentials never belong in scope; only trusted Host ports consume it.
|
|
3
|
+
*/
|
|
4
|
+
export interface RuntimeUnattendedExecutionGrant {
|
|
5
|
+
readonly taskId: string;
|
|
6
|
+
readonly requirementVersion: string;
|
|
7
|
+
readonly authorizationVersion: string;
|
|
8
|
+
readonly scope: Readonly<Record<string, unknown>>;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function pinnedUnattendedExecutionGrant(
|
|
12
|
+
value: RuntimeUnattendedExecutionGrant,
|
|
13
|
+
): RuntimeUnattendedExecutionGrant {
|
|
14
|
+
for (const [name, version] of [
|
|
15
|
+
["task ID", value.taskId],
|
|
16
|
+
["Requirement", value.requirementVersion],
|
|
17
|
+
["authorization", value.authorizationVersion],
|
|
18
|
+
] as const) {
|
|
19
|
+
if (typeof version !== "string" || !version.trim()) {
|
|
20
|
+
throw new Error(`Unattended execution ${name} is required`);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
if (!value.scope || typeof value.scope !== "object" || Array.isArray(value.scope)) {
|
|
24
|
+
throw new Error("Unattended execution scope must be an object");
|
|
25
|
+
}
|
|
26
|
+
try {
|
|
27
|
+
const scope = JSON.parse(JSON.stringify(value.scope)) as unknown;
|
|
28
|
+
if (!scope || typeof scope !== "object" || Array.isArray(scope)) {
|
|
29
|
+
throw new Error("scope changed shape during serialization");
|
|
30
|
+
}
|
|
31
|
+
return Object.freeze({
|
|
32
|
+
taskId: value.taskId,
|
|
33
|
+
requirementVersion: value.requirementVersion,
|
|
34
|
+
authorizationVersion: value.authorizationVersion,
|
|
35
|
+
scope: scope as Readonly<Record<string, unknown>>,
|
|
36
|
+
});
|
|
37
|
+
} catch (error) {
|
|
38
|
+
throw new Error("Unattended execution scope must be JSON-serializable", { cause: error });
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -5,7 +5,7 @@ import { TelemetryRecorder } from "./recorder";
|
|
|
5
5
|
import { RuntimeTelemetry } from "./runtime-telemetry";
|
|
6
6
|
import type { AgentTelemetryBinding } from "./contract";
|
|
7
7
|
import type { PiGenerationLifecycleEvent } from "../models";
|
|
8
|
-
import type { TemporaryAgentLifecycleEvent } from "../tools/temporary
|
|
8
|
+
import type { TemporaryAgentLifecycleEvent } from "../tools/subagent/temporary/coordinator";
|
|
9
9
|
|
|
10
10
|
const TELEMETRY_COLD_BUFFER_LIMIT = 1_000;
|
|
11
11
|
|
|
@@ -7,7 +7,7 @@ import type {
|
|
|
7
7
|
RuntimeTurnEventsPort,
|
|
8
8
|
} from "../config/ports";
|
|
9
9
|
import type { StoredSubmission } from "../submission/repository";
|
|
10
|
-
import { decodeSubagentCompletion, isZeroSubagentUsage, subagentTerminalStatus, type RuntimeSubagentRunMetadata } from "../tools/subagent
|
|
10
|
+
import { decodeSubagentCompletion, isZeroSubagentUsage, subagentTerminalStatus, type RuntimeSubagentRunMetadata } from "../tools/subagent/runtime";
|
|
11
11
|
import type { RuntimeTelemetryCoordinator } from "./coordinator";
|
|
12
12
|
import { summarizeModelUsage } from "./model-usage";
|
|
13
13
|
import type { RuntimeEventOutboxRepository } from "./runtime-event-repository";
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# Runtime Tools
|
|
2
|
+
|
|
3
|
+
工具按功能归组。根 `index.ts` 只维护包内统一导出;功能目录的 `index.ts` 定义或组合该功能的 Pi Tool。
|
|
4
|
+
|
|
5
|
+
| 目录 | 职责 |
|
|
6
|
+
| --- | --- |
|
|
7
|
+
| `runtime/` | 公共工具契约、Harness 编译、执行决策、审批与客户端交互结算 |
|
|
8
|
+
| `adapters/` | AI SDK ↔ Pi 转换,以及外部工具声明到 Pi Candidate 的适配 |
|
|
9
|
+
| `codemode/` | `execute` 和 `browser_execute` 的 Pi Tool 定义、Cloudflare 执行适配、Workspace Connector |
|
|
10
|
+
| `assistant/` | 用户问答、计划、后续建议、记忆与时间工具 |
|
|
11
|
+
| `workspace/` | 持久 Workspace 文件操作、写入串行化与版本操作工具 |
|
|
12
|
+
| `sandbox/` | 临时 Linux Sandbox 的命令、进程和显式文件发布工具 |
|
|
13
|
+
| `skill/` | Skill 工具组合、资源读取与复制、脚本运行与输出投影 |
|
|
14
|
+
| `subagent/` | 子 Agent 工具、权限、运行记录、类型和临时 Agent 协调 |
|
|
15
|
+
| `schedule/` | 定时任务的 Pi Tool 定义 |
|
|
16
|
+
| `web/` | 网页获取与 Provider-native 搜索 |
|
|
17
|
+
| `mcp/` | MCP Tool 接入及使用 MCP Session 的 Connector Gateway |
|
|
18
|
+
| `extension/` | 已加载 Extension 的发现工具;Extension 装载仍归 `context/extension/` |
|
|
19
|
+
|
|
20
|
+
## Code Mode
|
|
21
|
+
|
|
22
|
+
```text
|
|
23
|
+
codemode/index.ts Pi Tool 定义、执行级别与调用截止
|
|
24
|
+
|
|
|
25
|
+
v
|
|
26
|
+
RuntimeCodeExecutionPort 内部执行接口
|
|
27
|
+
|
|
|
28
|
+
v
|
|
29
|
+
codemode/adapter.ts Cloudflare 执行器组装、描述与结果适配
|
|
30
|
+
|
|
|
31
|
+
+-- workspace-connector.ts state.* 方法、发现与校验
|
|
32
|
+
+-- ../adapters/pi-to-ai.ts 现有 Pi 工具到 Cloudflare ToolSet 的转换
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
`pi-to-ai.ts` 也被 Skill 脚本执行使用,因此放在共用 `adapters/` 中。工具声明仍以 Pi 为唯一事实源。
|
|
36
|
+
|
|
37
|
+
## 修改位置
|
|
38
|
+
|
|
39
|
+
- 修改某个工具:进入对应功能目录,测试与实现放在一起。
|
|
40
|
+
- 修改所有工具共用的执行策略:进入 `runtime/`,不要复制到每个工具。
|
|
41
|
+
- 修改外部协议转换:进入 `adapters/`,不新增另一份工具定义。
|
|
42
|
+
- 小功能可以只有 `index.ts`,不强制拆分 Adapter、Service 或 Repository。
|
|
43
|
+
- `runtime/result.ts` 只共享结果包装与进度报告;`assistant/candidate.ts` 只共享助手工具已有的元数据默认值。
|
|
44
|
+
- 包的公共导出名及 `@springbrand/agent-runtime/declared-tool` 子路径保持不变。内部路径变更需要同步 Vitest 测试池配置。
|
|
@@ -1,13 +1,10 @@
|
|
|
1
1
|
// 本文件把 ai-sdk Tool 转成 Pi AgentTool。
|
|
2
2
|
// `@cloudflare/think`、Agents Skills 和 Workspace 等上游工厂生成 ai-sdk Tool 时调用这个边界。
|
|
3
|
-
// 转换只保留在这里,才能让后续治理和结算始终看到同一种 AgentTool 结果;核心术语见
|
|
3
|
+
// 转换只保留在这里,才能让后续治理和结算始终看到同一种 AgentTool 结果;核心术语见 `src/index.ts` 入口说明。
|
|
4
4
|
|
|
5
|
-
import type {
|
|
6
|
-
AgentTool,
|
|
7
|
-
AgentToolResult,
|
|
8
|
-
} from "@earendil-works/pi-agent-core";
|
|
5
|
+
import type { AgentTool, AgentToolResult } from "@earendil-works/pi-agent-core";
|
|
9
6
|
import { asSchema, type Tool as AiTool } from "ai";
|
|
10
|
-
import { serializeOutput } from "
|
|
7
|
+
import { serializeOutput } from "../../lib/artifacts";
|
|
11
8
|
|
|
12
9
|
/** 控制一个 ai-sdk Tool 如何投影到 Pi AgentTool。 */
|
|
13
10
|
export interface AiToolAdapterOptions {
|
|
@@ -141,9 +138,9 @@ export function aiToolToPi(
|
|
|
141
138
|
.toModelOutput;
|
|
142
139
|
const content = typeof toModelOutput === "function"
|
|
143
140
|
? modelContent(await (toModelOutput as (input: {
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
141
|
+
input: unknown;
|
|
142
|
+
output: unknown;
|
|
143
|
+
}) => unknown)({ input: params, output: raw }))
|
|
147
144
|
: null;
|
|
148
145
|
return {
|
|
149
146
|
content: content ?? [{
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { AgentTool, AgentToolResult } from "@earendil-works/pi-agent-core";
|
|
2
|
-
import type { PiToolCandidate } from "
|
|
2
|
+
import type { PiToolCandidate } from "../runtime/contracts";
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* 一个由 Runtime 之外声明、由 Runtime 之外执行的工具。
|
|
@@ -15,7 +15,7 @@ import type { PiToolCandidate } from "./compiler";
|
|
|
15
15
|
* 本模块只引入类型,因此可经 `@springbrand/agent-runtime/declared-tool` 单独导入:
|
|
16
16
|
* 声明方在 Node 下也能装配自己的工具,不必为此加载 Workers 运行时。
|
|
17
17
|
*
|
|
18
|
-
* `Pi`、`Tool Candidate` 与 `Runtime` 等核心术语见
|
|
18
|
+
* `Pi`、`Tool Candidate` 与 `Runtime` 等核心术语见 `src/index.ts`。
|
|
19
19
|
*/
|
|
20
20
|
export interface PiDeclaredTool {
|
|
21
21
|
/** 声明方自己的工具名,用于结果详情与诊断,不一定是模型看到的名字。 */
|
|
@@ -49,6 +49,7 @@ export interface PiDeclaredToolPolicy<Reply = unknown> {
|
|
|
49
49
|
readonly modelName: string;
|
|
50
50
|
readonly requiredExecutionLevel: PiToolCandidate["requiredExecutionLevel"];
|
|
51
51
|
readonly requiredExecutionLevelForInput?: PiToolCandidate["requiredExecutionLevelForInput"];
|
|
52
|
+
readonly unattendedExecution?: PiToolCandidate["unattendedExecution"];
|
|
52
53
|
readonly exposureMode?: PiToolCandidate["exposureMode"];
|
|
53
54
|
readonly retry?: PiToolCandidate["retry"];
|
|
54
55
|
readonly source?: PiToolCandidate["source"];
|
|
@@ -117,6 +118,9 @@ export function createPiDeclaredToolCandidate<Reply = unknown>(
|
|
|
117
118
|
...(policy.requiredExecutionLevelForInput
|
|
118
119
|
? { requiredExecutionLevelForInput: policy.requiredExecutionLevelForInput }
|
|
119
120
|
: {}),
|
|
121
|
+
...(policy.unattendedExecution
|
|
122
|
+
? { unattendedExecution: policy.unattendedExecution }
|
|
123
|
+
: {}),
|
|
120
124
|
...(policy.exposureMode ? { exposureMode: policy.exposureMode } : {}),
|
|
121
125
|
...(policy.retry ? { retry: policy.retry } : {}),
|
|
122
126
|
...(policy.source ? { source: policy.source } : {}),
|