@springbrand/agent-runtime 0.3.0-alpha.4 → 0.3.0-alpha.6
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 +73 -9
- package/src/adapter/cloudflare/define-agent.ts +4 -1
- 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 +12 -0
- package/src/config/assembler.ts +12 -0
- package/src/config/harness-runtime.ts +9 -1
- package/src/config/model-guidance.ts +21 -0
- package/src/config/pinning.ts +11 -0
- package/src/config/ports.ts +34 -4
- package/src/conversation/state.ts +22 -1
- package/src/harness/session-harness.ts +3 -2
- package/src/lib/prompt.ts +4 -1
- package/src/submission/admission.ts +8 -1
- package/src/submission/contracts.ts +42 -2
- package/src/submission/repository.ts +56 -3
- package/src/submission/unattended.ts +40 -0
- package/src/tools/approval/repository.ts +11 -4
- package/src/tools/base.ts +8 -0
- package/src/tools/compiler.ts +42 -16
- package/src/tools/decision-gate.ts +164 -29
- package/src/tools/declared.ts +4 -0
- package/src/tools/schedule.ts +66 -34
- package/src/tools/unattended-error.ts +4 -0
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
createModels,
|
|
3
|
+
lazyStream,
|
|
3
4
|
type MutableModels,
|
|
4
5
|
} from "@earendil-works/pi-ai";
|
|
5
6
|
import {
|
|
@@ -23,6 +24,7 @@ import {
|
|
|
23
24
|
type PreparePiRuntimeOptions,
|
|
24
25
|
type PreparedPiRuntime,
|
|
25
26
|
} from "./pinning";
|
|
27
|
+
import { refreshModelGuidance } from "./model-guidance";
|
|
26
28
|
|
|
27
29
|
/**
|
|
28
30
|
* Runtime 通过这个配置入口准备、固定并读取 Harness 的模型、提示和工具输入。
|
|
@@ -109,11 +111,17 @@ export class HarnessRuntimeConfig {
|
|
|
109
111
|
: target.getModel(provider, modelId);
|
|
110
112
|
}
|
|
111
113
|
if (property === "streamSimple") {
|
|
112
|
-
|
|
114
|
+
const stream = withProviderBoundary(
|
|
113
115
|
target.streamSimple.bind(target),
|
|
114
116
|
request.modelSessionId,
|
|
115
117
|
request.onGeneration,
|
|
116
118
|
);
|
|
119
|
+
return ((model, context, options) => lazyStream(model, async () => {
|
|
120
|
+
options?.signal?.throwIfAborted();
|
|
121
|
+
const refreshed = await refreshModelGuidance(state.candidates, context);
|
|
122
|
+
options?.signal?.throwIfAborted();
|
|
123
|
+
return stream(model, refreshed, options);
|
|
124
|
+
})) satisfies typeof stream;
|
|
117
125
|
}
|
|
118
126
|
if (property === "completeSimple") {
|
|
119
127
|
return target.completeSimple.bind(models);
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { Context } from "@earendil-works/pi-ai";
|
|
2
|
+
import type { PiToolCandidate } from "../tools/compiler";
|
|
3
|
+
|
|
4
|
+
/** Only descriptions and guidance may vary inside a pinned Submission. */
|
|
5
|
+
export async function refreshModelGuidance(
|
|
6
|
+
candidates: readonly PiToolCandidate[],
|
|
7
|
+
context: Context,
|
|
8
|
+
): Promise<Context> {
|
|
9
|
+
const guidance = new Map(await Promise.all(candidates
|
|
10
|
+
.filter(candidate => candidate.modelGuidance && context.tools?.some(tool => tool.name === candidate.tool.name))
|
|
11
|
+
.map(async candidate => [candidate.tool.name, await candidate.modelGuidance!()] as const)));
|
|
12
|
+
if (!guidance.size) return context;
|
|
13
|
+
return {
|
|
14
|
+
...context,
|
|
15
|
+
systemPrompt: [context.systemPrompt, ...new Set([...guidance.values()].map(item => item.systemPrompt).filter(Boolean))].filter(Boolean).join("\n\n"),
|
|
16
|
+
tools: context.tools?.map(tool => {
|
|
17
|
+
const prompt = guidance.get(tool.name);
|
|
18
|
+
return prompt ? { ...tool, description: prompt.description } : tool;
|
|
19
|
+
}),
|
|
20
|
+
};
|
|
21
|
+
}
|
package/src/config/pinning.ts
CHANGED
|
@@ -249,9 +249,20 @@ function describeTools(candidates: readonly PiToolCandidate[]) {
|
|
|
249
249
|
description: candidate.tool.description ?? candidate.tool.label,
|
|
250
250
|
parameters: candidate.tool.parameters,
|
|
251
251
|
requiredExecutionLevel: candidate.requiredExecutionLevel,
|
|
252
|
+
...(candidate.confirmation ? { confirmation: true } : {}),
|
|
252
253
|
...(candidate.alwaysRequiresApproval
|
|
253
254
|
? { alwaysRequiresApproval: true }
|
|
254
255
|
: {}),
|
|
256
|
+
...(candidate.unattendedExecution
|
|
257
|
+
? {
|
|
258
|
+
unattendedExecution: {
|
|
259
|
+
capability: candidate.unattendedExecution.capability,
|
|
260
|
+
operation: candidate.unattendedExecution.operation,
|
|
261
|
+
objectConstrained:
|
|
262
|
+
candidate.unattendedExecution.objectForInput !== undefined,
|
|
263
|
+
},
|
|
264
|
+
}
|
|
265
|
+
: {}),
|
|
255
266
|
...(candidate.deferLoading ? { deferLoading: true } : {}),
|
|
256
267
|
...(candidate.source ? { source: candidate.source } : {}),
|
|
257
268
|
retry: piToolRetryPolicy(candidate),
|
package/src/config/ports.ts
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
|
+
export type { RuntimeSchedulePreparation } from "../submission/contracts";
|
|
1
2
|
import type { RuntimeModelInputModality } from "./profile";
|
|
2
3
|
import type { SkillSource } from "agents/skills";
|
|
3
|
-
import type { ScheduleSpec } from "../submission/contracts";
|
|
4
|
+
import type { ScheduleSpec, RuntimeSchedulePreparation } from "../submission/contracts";
|
|
4
5
|
import type { RuntimeActivityProjection } from "../conversation/state";
|
|
5
6
|
import type { ExecutionLevel } from "../lib/execution-level";
|
|
6
7
|
import type { AgentToolResult } from "@earendil-works/pi-agent-core";
|
|
7
8
|
import type { PiToolCandidate } from "../tools/compiler";
|
|
9
|
+
import type { RuntimeUnattendedExecutionGrant } from "../submission/contracts";
|
|
8
10
|
import type {
|
|
9
11
|
RuntimeEventConfirmation,
|
|
10
12
|
RuntimeLifecycleFact,
|
|
@@ -669,16 +671,25 @@ export interface RuntimeScheduleAgentSummary {
|
|
|
669
671
|
* 让 Runtime 通过 Host 管理当前用户的定时任务。
|
|
670
672
|
*
|
|
671
673
|
* @remarks
|
|
672
|
-
*
|
|
674
|
+
* Host 提供准备与提交操作;Runtime 在现有持久化审批中固定候选并等待用户确认。
|
|
673
675
|
*
|
|
674
|
-
*
|
|
676
|
+
* 任务事实、候选版本、授权检查和保存幂等由 Host 持有,不存放凭据。
|
|
675
677
|
*/
|
|
678
|
+
export interface RuntimeScheduleConfirmationPort {
|
|
679
|
+
/** Read-only preparation; bind the full candidate and authorization to this stable intent. */
|
|
680
|
+
prepare(request: RuntimeScheduleUpdate & { intentId: string; id?: string }): Promise<RuntimeSchedulePreparation>;
|
|
681
|
+
/** Atomically check candidate/version, live authorization and quotas. Replay must return the same task. */
|
|
682
|
+
commit(prepared: RuntimeSchedulePreparation, context: { intentId: string; signal: AbortSignal }): Promise<{ id: string }>;
|
|
683
|
+
}
|
|
684
|
+
|
|
676
685
|
export interface RuntimeSchedulePort {
|
|
686
|
+
/** Required for chat creation/editing. Legacy management callers retain their CRUD contract. */
|
|
687
|
+
confirmation?: RuntimeScheduleConfirmationPort;
|
|
677
688
|
/**
|
|
678
689
|
* 创建一条定时任务并返回它的 ID。
|
|
679
690
|
*
|
|
680
691
|
* @remarks
|
|
681
|
-
* `schedule`
|
|
692
|
+
* 保留给既有宿主管理调用者;聊天 `schedule` 只经 confirmation.commit 保存。
|
|
682
693
|
*
|
|
683
694
|
* Host 在该边界内绑定用户与会话,Runtime 不携带业务归属 ID。
|
|
684
695
|
*/
|
|
@@ -1020,6 +1031,25 @@ export interface RuntimePlatformPort {
|
|
|
1020
1031
|
requiredExecutionLevel: ExecutionLevel;
|
|
1021
1032
|
signal: AbortSignal;
|
|
1022
1033
|
}): Promise<void>;
|
|
1034
|
+
/**
|
|
1035
|
+
* Match a pinned scheduled-task grant against the concrete Tool operation
|
|
1036
|
+
* and recheck the Host-owned live account authorization.
|
|
1037
|
+
*
|
|
1038
|
+
* This callback must fail closed by throwing a user-safe, specific reason.
|
|
1039
|
+
* It is called before dispatch and again at the effect boundary so a grant
|
|
1040
|
+
* revocation cannot be hidden by an earlier decision.
|
|
1041
|
+
*/
|
|
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>;
|
|
1023
1053
|
}
|
|
1024
1054
|
|
|
1025
1055
|
/**
|
|
@@ -68,6 +68,17 @@ export interface RuntimeTurnState {
|
|
|
68
68
|
queued: RuntimeQueuedSubmission[];
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
+
/**
|
|
72
|
+
* 本 Session 当前是否拒绝普通用户输入。
|
|
73
|
+
*
|
|
74
|
+
* 定时 Run 未结束时它独占输入:宿主据此禁用输入框并显示原因。
|
|
75
|
+
* 服务端准入用同一事实拒绝消息,所以客户端状态与后端判定不会漂移。
|
|
76
|
+
*/
|
|
77
|
+
export interface RuntimeInputLock {
|
|
78
|
+
/** 稳定原因码,由宿主/UI 决定文案。 */
|
|
79
|
+
reason: string;
|
|
80
|
+
}
|
|
81
|
+
|
|
71
82
|
/**
|
|
72
83
|
* 保存 Kernel 在当前会话里自己维护的审批状态。
|
|
73
84
|
*
|
|
@@ -85,6 +96,8 @@ export interface RuntimeState {
|
|
|
85
96
|
activity?: RuntimeActivityProjection;
|
|
86
97
|
approvals?: ApprovalReceipt[];
|
|
87
98
|
turn?: RuntimeTurnState;
|
|
99
|
+
/** 有值时普通输入被锁定,直到锁定的 Submission 进入终态。 */
|
|
100
|
+
inputLock?: RuntimeInputLock;
|
|
88
101
|
}
|
|
89
102
|
|
|
90
103
|
type SubmissionProjection = Pick<
|
|
@@ -103,6 +116,8 @@ export function projectRuntimeState(input: {
|
|
|
103
116
|
readonly hasPendingInteraction: (submissionId: string) => boolean;
|
|
104
117
|
readonly hasPendingSteer: (submissionId: string) => boolean;
|
|
105
118
|
readonly hasRunningAgentTools: boolean;
|
|
119
|
+
/** A non-terminal scheduled Submission owns ordinary input until it ends. */
|
|
120
|
+
readonly hasUnfinishedScheduled?: boolean;
|
|
106
121
|
readonly previousState: RuntimeState;
|
|
107
122
|
}): { state: RuntimeState; changed: boolean } {
|
|
108
123
|
const approvals: ApprovalReceipt[] = [
|
|
@@ -164,12 +179,18 @@ export function projectRuntimeState(input: {
|
|
|
164
179
|
backgroundWork: input.hasRunningAgentTools,
|
|
165
180
|
revision: (previousActivity?.revision ?? 0) + 1,
|
|
166
181
|
};
|
|
182
|
+
const inputLock: RuntimeInputLock | undefined = input.hasUnfinishedScheduled
|
|
183
|
+
? { reason: "scheduled-run" }
|
|
184
|
+
: undefined;
|
|
167
185
|
const changed = input.previousState.approvals === undefined ||
|
|
168
186
|
JSON.stringify(input.previousState.approvals) !== JSON.stringify(approvals) ||
|
|
169
187
|
JSON.stringify(input.previousState.turn) !== JSON.stringify(turn) ||
|
|
188
|
+
JSON.stringify(input.previousState.inputLock) !== JSON.stringify(inputLock) ||
|
|
170
189
|
nextActivity !== previousActivity;
|
|
171
190
|
return {
|
|
172
|
-
state: changed
|
|
191
|
+
state: changed
|
|
192
|
+
? { ...input.previousState, activity: nextActivity, approvals, turn, inputLock }
|
|
193
|
+
: input.previousState,
|
|
173
194
|
changed,
|
|
174
195
|
};
|
|
175
196
|
}
|
|
@@ -175,12 +175,13 @@ export class SessionHarness<TContext extends object | undefined> {
|
|
|
175
175
|
}
|
|
176
176
|
|
|
177
177
|
private async operationEntries(context: Context): Promise<Entry[]> {
|
|
178
|
+
// Stop boundaries follow traversal order; walk back from the current tip.
|
|
178
179
|
const entries = await this.lane.findEntries({
|
|
179
180
|
...(this.operationStartTip === null ? {} : { stopAtId: this.operationStartTip }),
|
|
180
181
|
type: "message",
|
|
181
|
-
order: "
|
|
182
|
+
order: "newestFirst",
|
|
182
183
|
}, context);
|
|
183
|
-
return entries.filter(({ id }) => id !== this.operationStartTip);
|
|
184
|
+
return entries.filter(({ id }) => id !== this.operationStartTip).reverse();
|
|
184
185
|
}
|
|
185
186
|
|
|
186
187
|
private async restoreToolGovernance(context: Context): Promise<void> {
|
package/src/lib/prompt.ts
CHANGED
|
@@ -70,7 +70,10 @@ export const TOOLS =
|
|
|
70
70
|
"prioritize verification, saving durable results, and the final response. " +
|
|
71
71
|
"When related Tool calls can run independently, all are available inside execute, and execute is present, run them inside that execute rather than as parallel top-level calls; " +
|
|
72
72
|
"only independent top-level-only Direct Tools should be issued together in one turn. When you reference " +
|
|
73
|
-
"code, cite it as file_path:line_number."
|
|
73
|
+
"code, cite it as file_path:line_number. " +
|
|
74
|
+
"When schedule tools are available, prepare a standalone Requirement (at most 1000 Unicode characters), a name (at most 50), a time rule and an explicit timezone. Ask for missing information. " +
|
|
75
|
+
"Use local once/daily/weekly rules for new scheduled tasks. The Host prepares the authorization scope and the user must confirm before saving; never claim a task was created before the Tool succeeds. " +
|
|
76
|
+
"One source Chat may create multiple tasks. Before editing, list tasks and use the exact Task ID; ask when the target is ambiguous. Never attach source history or hidden context to the Requirement.";
|
|
74
77
|
|
|
75
78
|
// A real browser is the only way to observe what a page actually does, and the
|
|
76
79
|
// shared failure mode is a browser tool that returns nothing and looks clean.
|
|
@@ -12,7 +12,8 @@ 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";
|
|
16
17
|
|
|
17
18
|
// Pending records retain identity indefinitely; only recent attempts reserve capacity.
|
|
18
19
|
const ADMISSION_ATTEMPT_WINDOW_MS = 60_000;
|
|
@@ -23,6 +24,10 @@ export interface SubmitMessageOptions {
|
|
|
23
24
|
userMessageId?: string;
|
|
24
25
|
userMessage?: UIMessage & { role: "user" };
|
|
25
26
|
regenerate?: boolean;
|
|
27
|
+
/** `scheduled` marks a Run injection that locks ordinary input until it ends. */
|
|
28
|
+
origin?: SubmissionOrigin;
|
|
29
|
+
/** Trusted Host grant; accepted only for a scheduled Submission. */
|
|
30
|
+
unattendedExecutionGrant?: RuntimeUnattendedExecutionGrant;
|
|
26
31
|
/** Internal transfer of a durable accepted steer; never a public message option. */
|
|
27
32
|
steerHandoff?: { submissionId: string; steerId: string };
|
|
28
33
|
}
|
|
@@ -182,6 +187,8 @@ export class SubmissionAdmission {
|
|
|
182
187
|
? options.userMessageId ?? null
|
|
183
188
|
: null,
|
|
184
189
|
runId,
|
|
190
|
+
origin: options.origin,
|
|
191
|
+
unattendedExecutionGrant: options.unattendedExecutionGrant,
|
|
185
192
|
});
|
|
186
193
|
const admitted = this.options.submissions.find(runId)!;
|
|
187
194
|
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
|
+
}
|
|
@@ -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 { RuntimeUnattendedExecutionGrant } from "./unattended";
|
|
3
4
|
|
|
4
5
|
// Submission 的统一定义见 ./index.ts。
|
|
5
6
|
// #region 状态与行类型
|
|
@@ -11,6 +12,14 @@ export type SubmissionStatus =
|
|
|
11
12
|
| "skipped"
|
|
12
13
|
| "error";
|
|
13
14
|
|
|
15
|
+
/**
|
|
16
|
+
* 谁发起了这条 Submission。
|
|
17
|
+
*
|
|
18
|
+
* `scheduled` 是定时 Run 的注入:它在本 Session 未结束期间独占输入,
|
|
19
|
+
* 普通 `user` Submission 必须被拒绝,而不是排队等它结束。
|
|
20
|
+
*/
|
|
21
|
+
export type SubmissionOrigin = "user" | "scheduled";
|
|
22
|
+
|
|
14
23
|
const TERMINAL_SUBMISSION_STATUSES: ReadonlySet<SubmissionStatus> = new Set([
|
|
15
24
|
"completed",
|
|
16
25
|
"aborted",
|
|
@@ -46,6 +55,9 @@ export interface StoredSubmission {
|
|
|
46
55
|
userMessageId: string | null;
|
|
47
56
|
regenerateMessageId: string | null;
|
|
48
57
|
runId: string | null;
|
|
58
|
+
origin: SubmissionOrigin;
|
|
59
|
+
unattendedExecutionGrant: RuntimeUnattendedExecutionGrant | null;
|
|
60
|
+
unattendedFailure: string | null;
|
|
49
61
|
retryable?: boolean;
|
|
50
62
|
}
|
|
51
63
|
|
|
@@ -69,6 +81,8 @@ export interface NewSubmission {
|
|
|
69
81
|
userMessageId?: string | null;
|
|
70
82
|
regenerateMessageId?: string | null;
|
|
71
83
|
runId?: string | null;
|
|
84
|
+
origin?: SubmissionOrigin;
|
|
85
|
+
unattendedExecutionGrant?: RuntimeUnattendedExecutionGrant;
|
|
72
86
|
}
|
|
73
87
|
|
|
74
88
|
type SubmissionRow = {
|
|
@@ -90,7 +104,10 @@ type SubmissionRow = {
|
|
|
90
104
|
user_message_id: string | null;
|
|
91
105
|
regenerate_message_id: string | null;
|
|
92
106
|
run_id: string | null;
|
|
107
|
+
origin: string | null;
|
|
93
108
|
admission_retryable: number | null;
|
|
109
|
+
unattended_execution_grant_json: string | null;
|
|
110
|
+
unattended_failure: string | null;
|
|
94
111
|
};
|
|
95
112
|
|
|
96
113
|
// #endregion
|
|
@@ -118,6 +135,11 @@ function mapRow(row: SubmissionRow): StoredSubmission {
|
|
|
118
135
|
userMessageId: row.user_message_id,
|
|
119
136
|
regenerateMessageId: row.regenerate_message_id,
|
|
120
137
|
runId: row.run_id,
|
|
138
|
+
origin: row.origin === "scheduled" ? "scheduled" : "user",
|
|
139
|
+
unattendedExecutionGrant: row.unattended_execution_grant_json
|
|
140
|
+
? JSON.parse(row.unattended_execution_grant_json) as RuntimeUnattendedExecutionGrant
|
|
141
|
+
: null,
|
|
142
|
+
unattendedFailure: row.unattended_failure,
|
|
121
143
|
retryable: row.admission_retryable === null
|
|
122
144
|
? undefined
|
|
123
145
|
: row.admission_retryable === 1,
|
|
@@ -150,7 +172,10 @@ export class SubmissionRepository {
|
|
|
150
172
|
queued_input_json, queued_ui_message_json, user_message_id,
|
|
151
173
|
regenerate_message_id,
|
|
152
174
|
run_id,
|
|
153
|
-
|
|
175
|
+
origin,
|
|
176
|
+
admission_retryable,
|
|
177
|
+
unattended_execution_grant_json,
|
|
178
|
+
unattended_failure
|
|
154
179
|
FROM pi_submissions
|
|
155
180
|
WHERE submission_id = ${id}
|
|
156
181
|
`[0];
|
|
@@ -251,7 +276,7 @@ export class SubmissionRepository {
|
|
|
251
276
|
created_at, assembly_revision, assembly_descriptor, runtime_key,
|
|
252
277
|
assistant_message_id, queued_input_json, queued_ui_message_json,
|
|
253
278
|
user_message_id, regenerate_message_id,
|
|
254
|
-
run_id
|
|
279
|
+
run_id, origin, unattended_execution_grant_json
|
|
255
280
|
) VALUES (
|
|
256
281
|
${s.submissionId},
|
|
257
282
|
${s.requestId},
|
|
@@ -267,11 +292,30 @@ export class SubmissionRepository {
|
|
|
267
292
|
${s.queuedUiMessageJson ?? null},
|
|
268
293
|
${s.userMessageId ?? null},
|
|
269
294
|
${s.regenerateMessageId ?? null},
|
|
270
|
-
${s.runId ?? s.submissionId}
|
|
295
|
+
${s.runId ?? s.submissionId},
|
|
296
|
+
${s.origin ?? "user"},
|
|
297
|
+
${s.unattendedExecutionGrant
|
|
298
|
+
? JSON.stringify(s.unattendedExecutionGrant)
|
|
299
|
+
: null}
|
|
271
300
|
)
|
|
272
301
|
`;
|
|
273
302
|
}
|
|
274
303
|
|
|
304
|
+
/**
|
|
305
|
+
* 是否存在一条未结束的定时 Submission。
|
|
306
|
+
*
|
|
307
|
+
* 定时 Run 未结束时它独占本 Session 的输入:普通消息在准入前被拒绝,
|
|
308
|
+
* 不排队等待。Run 进入终态后查询自然为空,普通追问恢复。
|
|
309
|
+
*/
|
|
310
|
+
hasUnfinishedScheduled(): boolean {
|
|
311
|
+
return (
|
|
312
|
+
this.sql<{ count: number }>`
|
|
313
|
+
SELECT COUNT(*) AS count FROM pi_submissions
|
|
314
|
+
WHERE status IN ('pending', 'running') AND origin = 'scheduled'
|
|
315
|
+
`[0]?.count ?? 0
|
|
316
|
+
) > 0;
|
|
317
|
+
}
|
|
318
|
+
|
|
275
319
|
insertRejected(s: {
|
|
276
320
|
submissionId: string;
|
|
277
321
|
requestId: string;
|
|
@@ -310,6 +354,15 @@ export class SubmissionRepository {
|
|
|
310
354
|
return updated.length === 1;
|
|
311
355
|
}
|
|
312
356
|
|
|
357
|
+
recordUnattendedFailure(id: string, reason: string): void {
|
|
358
|
+
this.sql`
|
|
359
|
+
UPDATE pi_submissions
|
|
360
|
+
SET unattended_failure = COALESCE(unattended_failure, ${reason})
|
|
361
|
+
WHERE submission_id = ${id}
|
|
362
|
+
AND status IN ('pending', 'running')
|
|
363
|
+
`;
|
|
364
|
+
}
|
|
365
|
+
|
|
313
366
|
// 只在当前状态属于 from 时把 Submission 切换到 to,否则返回 false。
|
|
314
367
|
// SubmissionLifecycle 开始 Turn 时调用,调用方用返回值识别过期或重复的状态切换。
|
|
315
368
|
// 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
|
+
}
|
|
@@ -16,6 +16,7 @@ export interface StoredApproval {
|
|
|
16
16
|
executionLevel: ExecutionLevel;
|
|
17
17
|
requiredExecutionLevel: ExecutionLevel;
|
|
18
18
|
inputJson: string;
|
|
19
|
+
confirmationJson?: string | null;
|
|
19
20
|
status: ApprovalStatus;
|
|
20
21
|
createdAt: number;
|
|
21
22
|
decidedAt: number | null;
|
|
@@ -33,6 +34,7 @@ export interface NewApproval {
|
|
|
33
34
|
executionLevel: ExecutionLevel;
|
|
34
35
|
requiredExecutionLevel: ExecutionLevel;
|
|
35
36
|
inputJson: string;
|
|
37
|
+
confirmationJson?: string | null;
|
|
36
38
|
status: ApprovalStatus;
|
|
37
39
|
createdAt: number;
|
|
38
40
|
}
|
|
@@ -46,6 +48,7 @@ export interface PendingApprovalSummary {
|
|
|
46
48
|
executionLevel: ExecutionLevel;
|
|
47
49
|
requiredExecutionLevel: ExecutionLevel;
|
|
48
50
|
inputJson: string;
|
|
51
|
+
confirmationJson?: string | null;
|
|
49
52
|
requestId: string;
|
|
50
53
|
}
|
|
51
54
|
|
|
@@ -66,6 +69,7 @@ type ApprovalRow = {
|
|
|
66
69
|
execution_level: string;
|
|
67
70
|
required_level: string;
|
|
68
71
|
input_json: string;
|
|
72
|
+
confirmation_json: string | null;
|
|
69
73
|
status: string;
|
|
70
74
|
created_at: number;
|
|
71
75
|
decided_at: number | null;
|
|
@@ -89,6 +93,7 @@ function mapRow(row: ApprovalRow): StoredApproval {
|
|
|
89
93
|
executionLevel: row.execution_level as ExecutionLevel,
|
|
90
94
|
requiredExecutionLevel: row.required_level as ExecutionLevel,
|
|
91
95
|
inputJson: row.input_json,
|
|
96
|
+
confirmationJson: row.confirmation_json,
|
|
92
97
|
status: row.status as ApprovalStatus,
|
|
93
98
|
createdAt: row.created_at,
|
|
94
99
|
decidedAt: row.decided_at,
|
|
@@ -111,7 +116,7 @@ export class ApprovalRepository {
|
|
|
111
116
|
const row = this.sql<ApprovalRow>`
|
|
112
117
|
SELECT execution_id, submission_id, request_id, source,
|
|
113
118
|
tool_call_id, tool_name, summary,
|
|
114
|
-
execution_level, required_level, input_json,
|
|
119
|
+
execution_level, required_level, input_json, confirmation_json,
|
|
115
120
|
status, created_at, decided_at, reason
|
|
116
121
|
FROM pi_approvals
|
|
117
122
|
WHERE execution_id = ${executionId}
|
|
@@ -128,7 +133,7 @@ export class ApprovalRepository {
|
|
|
128
133
|
INSERT INTO pi_approvals (
|
|
129
134
|
execution_id, submission_id, request_id, source,
|
|
130
135
|
tool_call_id, tool_name, summary,
|
|
131
|
-
execution_level, required_level, input_json,
|
|
136
|
+
execution_level, required_level, input_json, confirmation_json,
|
|
132
137
|
status, created_at
|
|
133
138
|
) VALUES (
|
|
134
139
|
${approval.executionId},
|
|
@@ -141,6 +146,7 @@ export class ApprovalRepository {
|
|
|
141
146
|
${approval.executionLevel},
|
|
142
147
|
${approval.requiredExecutionLevel},
|
|
143
148
|
${approval.inputJson},
|
|
149
|
+
${approval.confirmationJson ?? null},
|
|
144
150
|
${approval.status},
|
|
145
151
|
${approval.createdAt}
|
|
146
152
|
)
|
|
@@ -192,12 +198,13 @@ export class ApprovalRepository {
|
|
|
192
198
|
execution_level: string;
|
|
193
199
|
required_level: string;
|
|
194
200
|
input_json: string;
|
|
201
|
+
confirmation_json: string | null;
|
|
195
202
|
request_id: string;
|
|
196
203
|
}>`
|
|
197
204
|
SELECT approval.execution_id, approval.tool_call_id, approval.source,
|
|
198
205
|
approval.tool_name, approval.summary,
|
|
199
206
|
approval.execution_level, approval.required_level,
|
|
200
|
-
approval.input_json, approval.request_id
|
|
207
|
+
approval.input_json, approval.confirmation_json, approval.request_id
|
|
201
208
|
FROM pi_approvals approval
|
|
202
209
|
JOIN pi_submissions submission
|
|
203
210
|
ON submission.submission_id = approval.submission_id
|
|
@@ -212,7 +219,7 @@ export class ApprovalRepository {
|
|
|
212
219
|
summary: row.summary,
|
|
213
220
|
executionLevel: row.execution_level as ExecutionLevel,
|
|
214
221
|
requiredExecutionLevel: row.required_level as ExecutionLevel,
|
|
215
|
-
inputJson: row.input_json,
|
|
222
|
+
inputJson: row.confirmation_json ?? row.input_json,
|
|
216
223
|
requestId: row.request_id,
|
|
217
224
|
}));
|
|
218
225
|
}
|
package/src/tools/base.ts
CHANGED
|
@@ -452,6 +452,10 @@ export function basePiToolCandidates(
|
|
|
452
452
|
return result({ noted: true, count: input.items.length });
|
|
453
453
|
},
|
|
454
454
|
}),
|
|
455
|
+
unattendedExecution: {
|
|
456
|
+
capability: "runtime-ui",
|
|
457
|
+
operation: "suggest-followups",
|
|
458
|
+
},
|
|
455
459
|
exposureMode: "direct",
|
|
456
460
|
},
|
|
457
461
|
{
|
|
@@ -470,6 +474,10 @@ export function basePiToolCandidates(
|
|
|
470
474
|
});
|
|
471
475
|
},
|
|
472
476
|
}),
|
|
477
|
+
unattendedExecution: {
|
|
478
|
+
capability: "runtime-ui",
|
|
479
|
+
operation: "update",
|
|
480
|
+
},
|
|
473
481
|
exposureMode: "direct",
|
|
474
482
|
},
|
|
475
483
|
...(webSearch ? [webSearchPiToolCandidate(webSearch)] : []),
|