@springbrand/agent-runtime 0.3.0-alpha.2 → 0.3.0-alpha.21

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.
Files changed (131) hide show
  1. package/package.json +11 -9
  2. package/src/adapter/cloudflare/agent.ts +233 -179
  3. package/src/adapter/cloudflare/define-agent.ts +252 -104
  4. package/src/adapter/cloudflare/index.ts +18 -0
  5. package/src/adapter/cloudflare/scheduling/schedule-rule.ts +322 -0
  6. package/src/adapter/cloudflare/scheduling/zoned-cron.ts +11 -6
  7. package/src/adapter/cloudflare/schema.ts +21 -2
  8. package/src/adapter/cloudflare/subagent/definition.ts +25 -13
  9. package/src/adapter/cloudflare/subagent/runner.ts +43 -50
  10. package/src/adapter/cloudflare/subagent/tools.ts +4 -4
  11. package/src/adapter/cloudflare/universal-agent/preparation.ts +3 -3
  12. package/src/adapter/cloudflare/universal-agent/tools.ts +4 -6
  13. package/src/adapter/cloudflare/workspace/version-control.ts +1 -1
  14. package/src/config/activation.ts +13 -15
  15. package/src/config/agent-context.ts +1 -1
  16. package/src/config/assembler.ts +48 -31
  17. package/src/config/assembly-snapshot.ts +1 -1
  18. package/src/config/definition.ts +1 -1
  19. package/src/config/harness-runtime.ts +77 -49
  20. package/src/config/index.ts +1 -1
  21. package/src/config/model-guidance.ts +20 -0
  22. package/src/config/ports.ts +57 -7
  23. package/src/config/{pinning.ts → prepared-runtime.ts} +59 -123
  24. package/src/config/public-contracts.ts +2 -2
  25. package/src/config/runtime-config.ts +2 -1
  26. package/src/context/assembly.ts +1 -1
  27. package/src/context/extension/assembly.ts +2 -2
  28. package/src/context/memory/budget/gate.ts +19 -3
  29. package/src/conversation/harness-resume.ts +89 -238
  30. package/src/conversation/harness-stream.ts +87 -71
  31. package/src/conversation/history.ts +154 -152
  32. package/src/conversation/message/contract.ts +28 -4
  33. package/src/conversation/message/conversion.ts +46 -5
  34. package/src/conversation/message/projection.ts +61 -21
  35. package/src/conversation/session-entry.ts +29 -0
  36. package/src/conversation/state.ts +23 -2
  37. package/src/harness/governance.ts +206 -0
  38. package/src/harness/index.ts +3 -0
  39. package/src/harness/session-harness.ts +269 -445
  40. package/src/harness/submission-operation.ts +15 -40
  41. package/src/index.ts +18 -8
  42. package/src/lib/artifacts.ts +18 -0
  43. package/src/lib/execution-level.ts +13 -0
  44. package/src/lib/prompt.ts +9 -10
  45. package/src/lib/token-estimate.ts +8 -0
  46. package/src/model-protocol/openrouter.ts +41 -0
  47. package/src/models/index.ts +200 -35
  48. package/src/models/openrouter/messages.ts +13 -8
  49. package/src/models/openrouter/openai.ts +200 -0
  50. package/src/models/openrouter/protocol-response.ts +85 -0
  51. package/src/models/provider-streams.ts +45 -0
  52. package/src/session/cloudflare/migration.ts +382 -864
  53. package/src/session/cloudflare/storage.ts +13 -536
  54. package/src/session/index.ts +22 -35
  55. package/src/skills/springbrand-worker-website/index.ts +1 -1
  56. package/src/submission/admission.ts +20 -30
  57. package/src/submission/contracts.ts +42 -2
  58. package/src/submission/delivery.ts +1 -1
  59. package/src/submission/finalization.ts +36 -15
  60. package/src/submission/lifecycle.ts +28 -15
  61. package/src/submission/repository.ts +97 -49
  62. package/src/submission/steer-delivery.ts +32 -76
  63. package/src/submission/unattended.ts +40 -0
  64. package/src/telemetry/contract.ts +0 -1
  65. package/src/telemetry/coordinator.ts +1 -1
  66. package/src/telemetry/runtime-telemetry.ts +0 -4
  67. package/src/telemetry/subagent-lifecycle.ts +1 -1
  68. package/src/telemetry/turn-events.ts +35 -25
  69. package/src/tools/README.md +44 -0
  70. package/src/tools/{ai-adapter.ts → adapters/ai-to-pi.ts} +6 -9
  71. package/src/tools/{declared.ts → adapters/declared.ts} +6 -2
  72. package/src/tools/{nested-tools.ts → adapters/pi-to-ai.ts} +4 -4
  73. package/src/tools/assistant/ask-user.ts +319 -0
  74. package/src/tools/assistant/candidate.ts +15 -0
  75. package/src/tools/assistant/followups.ts +34 -0
  76. package/src/tools/assistant/index.ts +19 -0
  77. package/src/tools/assistant/memory.ts +73 -0
  78. package/src/tools/assistant/plan.ts +98 -0
  79. package/src/tools/{time.ts → assistant/time.ts} +1 -1
  80. package/src/tools/{core-host.ts → codemode/adapter.ts} +50 -53
  81. package/src/tools/{core.ts → codemode/index.ts} +36 -92
  82. package/src/tools/codemode/workspace-connector.ts +198 -0
  83. package/src/tools/extension/index.ts +42 -0
  84. package/src/tools/index.ts +29 -26
  85. package/src/tools/{gateway.ts → mcp/gateway.ts} +19 -19
  86. package/src/tools/{mcp.ts → mcp/index.ts} +5 -5
  87. package/src/tools/runtime/activation.ts +40 -0
  88. package/src/tools/{approval → runtime/approval}/repository.ts +13 -12
  89. package/src/tools/runtime/compiler.ts +246 -0
  90. package/src/tools/runtime/contracts.ts +140 -0
  91. package/src/tools/{decision-gate.ts → runtime/decision-gate.ts} +149 -39
  92. package/src/tools/{human-decisions.ts → runtime/human-decisions.ts} +4 -4
  93. package/src/tools/{interaction → runtime/interaction}/repository.ts +1 -7
  94. package/src/tools/runtime/result.ts +22 -0
  95. package/src/tools/runtime/unattended-error.ts +4 -0
  96. package/src/tools/sandbox/index.ts +228 -0
  97. package/src/tools/schedule/index.ts +301 -0
  98. package/src/tools/skill/contracts.ts +20 -0
  99. package/src/tools/skill/index.ts +99 -0
  100. package/src/tools/skill/resources.ts +322 -0
  101. package/src/tools/skill/scripts.ts +168 -0
  102. package/src/tools/{subagent-authority.ts → subagent/authority.ts} +31 -31
  103. package/src/tools/{subagent.ts → subagent/index.ts} +34 -37
  104. package/src/tools/{subagent-run-repository.ts → subagent/repository.ts} +1 -1
  105. package/src/tools/{subagent-runtime.ts → subagent/runtime.ts} +1 -1
  106. package/src/tools/{temporary-agent → subagent/temporary}/approvals.ts +4 -7
  107. package/src/tools/{temporary-agent → subagent/temporary}/core.ts +2 -3
  108. package/src/tools/{temporary-agent → subagent/temporary}/runner.ts +1 -1
  109. package/src/tools/{temporary-agent → subagent/temporary}/workspace.ts +1 -4
  110. package/src/tools/{web-fetch.ts → web/fetch.ts} +4 -13
  111. package/src/tools/web/index.ts +2 -0
  112. package/src/tools/web/search/api.ts +1322 -0
  113. package/src/tools/web/search/index.ts +2 -0
  114. package/src/tools/{web-search/web-search.ts → web/search/search.ts} +5 -7
  115. package/src/tools/{workspace-sandbox.ts → workspace/index.ts} +12 -272
  116. package/src/tools/{workspace-revision.ts → workspace/revision.ts} +3 -3
  117. package/src/tools/base.ts +0 -534
  118. package/src/tools/compiler.ts +0 -327
  119. package/src/tools/schedule.ts +0 -261
  120. package/src/tools/skill.ts +0 -565
  121. package/src/tools/web-search/api.ts +0 -1318
  122. package/src/tools/web-search/index.ts +0 -2
  123. package/src/tools/workspace-state-connector.ts +0 -146
  124. package/src/tools/{approval → runtime/approval}/index.ts +0 -0
  125. package/src/tools/{interaction → runtime/interaction}/index.ts +0 -0
  126. package/src/tools/{temporary-agent → subagent/temporary}/coordinator.ts +0 -0
  127. package/src/tools/{subagent-types → subagent/types}/contract.ts +0 -0
  128. package/src/tools/{subagent-types → subagent/types}/extract/index.ts +0 -0
  129. package/src/tools/{subagent-types → subagent/types}/fanout/index.ts +0 -0
  130. package/src/tools/{subagent-types → subagent/types}/registry.ts +1 -1
  131. /package/src/tools/{workspace-versioning.ts → workspace/versioning.ts} +0 -0
@@ -0,0 +1,322 @@
1
+ import type {
2
+ LocalScheduleRule,
3
+ ScheduleSpec,
4
+ } from "../../../submission/contracts";
5
+ import { isValidTimeZone, wallTimeInstants, zoneOffsetMs } from "./zoned-cron";
6
+
7
+ const DAY_MS = 86_400_000;
8
+ const HOUR_MS = 3_600_000;
9
+ const MINUTE_MS = 60_000;
10
+ /** 扫描上限:每日/每周规则一定在 7 天内出现,这里只兜底防止坏输入空转。 */
11
+ const MAX_SCAN_DAYS = 366 * 5;
12
+
13
+ /**
14
+ * 宿主允许在计划时点之后多久仍可启动该轮次。宿主可覆盖,
15
+ * 通用模块只把它当作可参数化的迟到窗口,不承载套餐或任务策略。
16
+ */
17
+ export const DEFAULT_LATENESS_WINDOW_MS = 5 * MINUTE_MS;
18
+
19
+ export type ScheduleSkipReason = "dst_missing_local_time";
20
+
21
+ /** 一个真实可执行的本地时间轮次。 */
22
+ export interface ScheduledOccurrence {
23
+ kind: "scheduled";
24
+ /** 跨重算、重启都稳定的轮次身份,供宿主做幂等键。 */
25
+ occurrenceId: string;
26
+ /** 计划执行的 UTC 瞬时。 */
27
+ scheduledAt: number;
28
+ /** 任务时区下的本地日期 `YYYY-MM-DD`。 */
29
+ localDate: string;
30
+ /** 任务时区下的本地钟点 `HH:mm`。 */
31
+ localTime: string;
32
+ /** 含边界的最迟可启动瞬时 = `scheduledAt + latenessWindowMs`。 */
33
+ latestStartAt: number;
34
+ }
35
+
36
+ /** 因夏令时缺失钟点而被跳过的轮次;没有可执行瞬时,只留身份与原因。 */
37
+ export interface SkippedOccurrence {
38
+ kind: "skipped";
39
+ occurrenceId: string;
40
+ localDate: string;
41
+ localTime: string;
42
+ reason: ScheduleSkipReason;
43
+ }
44
+
45
+ export type ScheduleOccurrence = ScheduledOccurrence | SkippedOccurrence;
46
+
47
+ /** 一条按任务时区解释的当地时间规则及其生效窗口。 */
48
+ export interface ScheduleRuleSpec {
49
+ rule: LocalScheduleRule;
50
+ /** IANA 时区名,如 `Asia/Shanghai`;绝不退化成固定 UTC offset。 */
51
+ timeZone: string;
52
+ /** 含当天的最早生效本地日期 `YYYY-MM-DD`。 */
53
+ startDate?: string;
54
+ /** 含当天的最后生效本地日期 `YYYY-MM-DD`。 */
55
+ endDate?: string;
56
+ /** 覆盖默认迟到窗口。 */
57
+ latenessWindowMs?: number;
58
+ }
59
+
60
+ /** `ScheduleSpec.trigger` 里 `local` 变体的形状。 */
61
+ export type LocalScheduleTrigger = Extract<
62
+ ScheduleSpec["trigger"],
63
+ { kind: "local" }
64
+ >;
65
+
66
+ /**
67
+ * 把合同的 `local` 触发条件投影成时间规则计算规格。
68
+ *
69
+ * 宿主与路由共用同一映射,避免各自拼一份 `startDate`/`endDate`,改合同字段时漏改。
70
+ */
71
+ export function toScheduleRuleSpec(
72
+ trigger: LocalScheduleTrigger,
73
+ timeZone: string,
74
+ ): ScheduleRuleSpec {
75
+ return {
76
+ rule: trigger.rule,
77
+ timeZone,
78
+ ...(trigger.startDate ? { startDate: trigger.startDate } : {}),
79
+ ...(trigger.endDate ? { endDate: trigger.endDate } : {}),
80
+ };
81
+ }
82
+
83
+ export type ScheduleRuleErrorCode =
84
+ | "invalid_rule"
85
+ | "invalid_time_zone"
86
+ | "invalid_date"
87
+ | "invalid_time"
88
+ | "empty_weekdays"
89
+ | "invalid_weekday"
90
+ | "invalid_window"
91
+ | "invalid_limit"
92
+ | "no_future_occurrence";
93
+
94
+ export interface ScheduleRuleError {
95
+ code: ScheduleRuleErrorCode;
96
+ message: string;
97
+ }
98
+
99
+ export type ScheduleOccurrenceResolution =
100
+ | { ok: true; occurrences: ScheduleOccurrence[] }
101
+ | { ok: false; error: ScheduleRuleError };
102
+
103
+ interface LocalClock {
104
+ hour: number;
105
+ minute: number;
106
+ }
107
+
108
+ /** 校验规则形状与时区,不判断是否存在未来时点。 */
109
+ export function validateScheduleRuleSpec(
110
+ spec: ScheduleRuleSpec,
111
+ ): ScheduleRuleError | null {
112
+ if (typeof spec.timeZone !== "string" || !isValidTimeZone(spec.timeZone)) {
113
+ return { code: "invalid_time_zone", message: `未知时区:${spec.timeZone}` };
114
+ }
115
+ const rule = spec.rule as unknown;
116
+ if (!isRecord(rule) ||
117
+ (rule.kind !== "once" && rule.kind !== "daily" && rule.kind !== "weekly")) {
118
+ return { code: "invalid_rule", message: "不支持的当地时间规则" };
119
+ }
120
+ const time = typeof rule.time === "string" ? parseTime(rule.time) : null;
121
+ if (!time) {
122
+ return { code: "invalid_time", message: `非法钟点:${String(rule.time ?? "")}` };
123
+ }
124
+ if (rule.kind === "once" &&
125
+ (typeof rule.date !== "string" || parseDate(rule.date) === null)) {
126
+ return { code: "invalid_date", message: `非法日期:${String(rule.date ?? "")}` };
127
+ }
128
+ if (rule.kind === "weekly") {
129
+ if (!Array.isArray(rule.weekdays) || rule.weekdays.length === 0) {
130
+ if (Array.isArray(rule.weekdays)) {
131
+ return { code: "empty_weekdays", message: "每周规则至少需要一个星期" };
132
+ }
133
+ return { code: "invalid_weekday", message: "星期必须是数字数组" };
134
+ }
135
+ if (
136
+ rule.weekdays.some(
137
+ (weekday) => !Number.isInteger(weekday) || weekday < 0 || weekday > 6,
138
+ )
139
+ ) {
140
+ return {
141
+ code: "invalid_weekday",
142
+ message: `非法星期:${rule.weekdays.join(",")}`,
143
+ };
144
+ }
145
+ }
146
+ const start = optionalDate(spec.startDate);
147
+ if (spec.startDate !== undefined && start === null) {
148
+ return { code: "invalid_date", message: `非法开始日期:${spec.startDate}` };
149
+ }
150
+ const end = optionalDate(spec.endDate);
151
+ if (spec.endDate !== undefined && end === null) {
152
+ return { code: "invalid_date", message: `非法结束日期:${spec.endDate}` };
153
+ }
154
+ if (start !== null && end !== null && end < start) {
155
+ return { code: "invalid_window", message: "结束日期不能早于开始日期" };
156
+ }
157
+ return null;
158
+ }
159
+
160
+ /**
161
+ * 从 `after`(UTC 瞬时,不含)起枚举轮次,含被跳过的缺失钟点。
162
+ *
163
+ * 预览、下一轮计算和恢复枚举都走这里,保证三者语义一致;输出为空时返回可识别的
164
+ * `no_future_occurrence`,而不是伪造一个执行时间。
165
+ */
166
+ export function resolveScheduleOccurrences(
167
+ spec: ScheduleRuleSpec,
168
+ options: { after: number; limit: number },
169
+ ): ScheduleOccurrenceResolution {
170
+ const error = validateScheduleRuleSpec(spec);
171
+ if (error) return { ok: false, error };
172
+ if (!Number.isSafeInteger(options.limit) || options.limit <= 0) {
173
+ return {
174
+ ok: false,
175
+ error: { code: "invalid_limit", message: "轮次数量必须是正整数" },
176
+ };
177
+ }
178
+ const occurrences = enumerate(spec, options.after, options.limit);
179
+ if (occurrences.length === 0) {
180
+ return {
181
+ ok: false,
182
+ error: {
183
+ code: "no_future_occurrence",
184
+ message: "该规则在给定时间之后没有有效执行时点",
185
+ },
186
+ };
187
+ }
188
+ return { ok: true, occurrences };
189
+ }
190
+
191
+ /** 下一个可执行瞬时;缺失钟点被跳过,不伪造、不顺延补跑。 */
192
+ export function nextScheduledOccurrence(
193
+ spec: ScheduleRuleSpec,
194
+ after: number,
195
+ ): ScheduledOccurrence | null {
196
+ if (validateScheduleRuleSpec(spec) !== null) return null;
197
+ for (let limit = 1; limit <= 512; limit *= 2) {
198
+ const occurrences = enumerate(spec, after, limit);
199
+ const scheduled = occurrences.find(
200
+ (occurrence): occurrence is ScheduledOccurrence =>
201
+ occurrence.kind === "scheduled",
202
+ );
203
+ if (scheduled) return scheduled;
204
+ if (occurrences.length < limit) return null;
205
+ }
206
+ return null;
207
+ }
208
+
209
+ /** 含边界判断某个已计划轮次是否仍在迟到窗口内。 */
210
+ export function isWithinLatenessWindow(
211
+ occurrence: ScheduledOccurrence,
212
+ now: number,
213
+ ): boolean {
214
+ return now >= occurrence.scheduledAt && now <= occurrence.latestStartAt;
215
+ }
216
+
217
+ function enumerate(
218
+ spec: ScheduleRuleSpec,
219
+ after: number,
220
+ limit: number,
221
+ ): ScheduleOccurrence[] {
222
+ const time = parseTime(spec.rule.time);
223
+ if (!time) return [];
224
+ const windowMs = spec.latenessWindowMs ?? DEFAULT_LATENESS_WINDOW_MS;
225
+ const wallAfter = after + zoneOffsetMs(after, spec.timeZone);
226
+ const dayOfAfter = Math.floor(wallAfter / DAY_MS) * DAY_MS;
227
+ const startDay = optionalDate(spec.startDate) ?? dayOfAfter;
228
+ const endDay = optionalDate(spec.endDate);
229
+ const occurrences: ScheduleOccurrence[] = [];
230
+
231
+ const onceDay = spec.rule.kind === "once" ? parseDate(spec.rule.date) : null;
232
+ // 一次性规则直接跳到目标日期,不能让重复规则的扫描保险上限拒绝远期任务。
233
+ let day = Math.max(startDay, dayOfAfter, onceDay ?? Number.NEGATIVE_INFINITY);
234
+ const maxScanDays = onceDay === null ? MAX_SCAN_DAYS : 0;
235
+ for (let scanned = 0; scanned <= maxScanDays; scanned += 1, day += DAY_MS) {
236
+ if (endDay !== null && day > endDay) break;
237
+ if (!matchesRule(spec.rule, day)) continue;
238
+
239
+ const wall = day + time.hour * HOUR_MS + time.minute * MINUTE_MS;
240
+ const localDate = formatDate(day);
241
+ const localTime = formatTime(time);
242
+ const occurrenceId = `${localDate}T${localTime}`;
243
+ const instants = wallTimeInstants(wall, spec.timeZone);
244
+ if (instants.length === 0) {
245
+ // 拨快抹掉的钟点:没有可执行瞬时,只在游标之后才作为跳过轮次报告。
246
+ if (wall <= wallAfter) continue;
247
+ occurrences.push({
248
+ kind: "skipped",
249
+ occurrenceId,
250
+ localDate,
251
+ localTime,
252
+ reason: "dst_missing_local_time",
253
+ });
254
+ } else {
255
+ const scheduledAt = instants[0];
256
+ if (scheduledAt <= after) continue;
257
+ occurrences.push({
258
+ kind: "scheduled",
259
+ occurrenceId,
260
+ scheduledAt,
261
+ localDate,
262
+ localTime,
263
+ latestStartAt: scheduledAt + windowMs,
264
+ });
265
+ }
266
+ if (occurrences.length >= limit) break;
267
+ }
268
+ return occurrences;
269
+ }
270
+
271
+ function matchesRule(rule: LocalScheduleRule, day: number): boolean {
272
+ switch (rule.kind) {
273
+ case "once":
274
+ return day === parseDate(rule.date);
275
+ case "daily":
276
+ return true;
277
+ case "weekly":
278
+ return rule.weekdays.includes(new Date(day).getUTCDay());
279
+ }
280
+ }
281
+
282
+ function parseTime(value: string): LocalClock | null {
283
+ const match = /^([01]\d|2[0-3]):([0-5]\d)$/.exec(value);
284
+ if (!match) return null;
285
+ return { hour: Number(match[1]), minute: Number(match[2]) };
286
+ }
287
+
288
+ function formatTime({ hour, minute }: LocalClock): string {
289
+ return `${String(hour).padStart(2, "0")}:${String(minute).padStart(2, "0")}`;
290
+ }
291
+
292
+ /** `YYYY-MM-DD` → UTC 零点毫秒;非法日历日期返回 null。 */
293
+ function parseDate(value: string): number | null {
294
+ const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(value);
295
+ if (!match) return null;
296
+ const year = Number(match[1]);
297
+ const month = Number(match[2]);
298
+ const day = Number(match[3]);
299
+ const ms = Date.UTC(year, month - 1, day);
300
+ const date = new Date(ms);
301
+ if (
302
+ date.getUTCFullYear() !== year ||
303
+ date.getUTCMonth() !== month - 1 ||
304
+ date.getUTCDate() !== day
305
+ ) {
306
+ return null;
307
+ }
308
+ return ms;
309
+ }
310
+
311
+ function optionalDate(value: string | undefined): number | null {
312
+ if (value === undefined) return null;
313
+ return parseDate(value);
314
+ }
315
+
316
+ function formatDate(day: number): string {
317
+ return new Date(day).toISOString().slice(0, 10);
318
+ }
319
+
320
+ function isRecord(value: unknown): value is Record<string, unknown> {
321
+ return typeof value === "object" && value !== null && !Array.isArray(value);
322
+ }
@@ -55,7 +55,8 @@ export function nextCronOccurrence(
55
55
  for (const second of parsed.seconds) {
56
56
  const wall = day + hour * 3_600_000 + minute * 60_000 + second * 1_000;
57
57
  if (wall <= wallAfter) continue;
58
- const resolved = wallTimeCandidates(wall, timeZone, after)[0];
58
+ const resolved = wallTimeInstants(wall, timeZone)
59
+ .find((instant) => instant > after);
59
60
  if (resolved !== undefined) return resolved;
60
61
  }
61
62
  }
@@ -73,18 +74,22 @@ export function isValidTimeZone(timeZone: string): boolean {
73
74
  }
74
75
  }
75
76
 
76
- function wallTimeCandidates(wallUtc: number, timeZone: string, after: number): number[] {
77
+ /**
78
+ * 所有本地墙上时间恰好等于 `wallUtc` 的 UTC 瞬时,升序。
79
+ *
80
+ * `wallUtc` 是"把本地钟点当作 UTC"的伪瞬时(见 {@link zoneOffsetMs})。
81
+ * 返回空数组 = 该钟点在当地被夏令时拨快抹掉;返回两个 = 拨慢导致同一钟点出现两次。
82
+ * 这是 `nextCronOccurrence` 与本地规则计算共用的唯一钟点解析边界。
83
+ */
84
+ export function wallTimeInstants(wallUtc: number, timeZone: string): number[] {
77
85
  const offsets = new Set([
78
- zoneOffsetMs(after, timeZone),
79
86
  zoneOffsetMs(wallUtc - 2 * 86_400_000, timeZone),
80
87
  zoneOffsetMs(wallUtc, timeZone),
81
88
  zoneOffsetMs(wallUtc + 2 * 86_400_000, timeZone),
82
89
  ]);
83
90
  return [...offsets]
84
91
  .map((offset) => wallUtc - offset)
85
- .filter((instant) =>
86
- instant > after && instant + zoneOffsetMs(instant, timeZone) === wallUtc
87
- )
92
+ .filter((instant) => instant + zoneOffsetMs(instant, timeZone) === wallUtc)
88
93
  .sort((left, right) => left - right);
89
94
  }
90
95
 
@@ -19,8 +19,6 @@ export function initializeSchema(sql: SqlTaggedTemplate): void {
19
19
  error TEXT,
20
20
  created_at INTEGER NOT NULL,
21
21
  completed_at INTEGER,
22
- assembly_revision TEXT NOT NULL,
23
- assembly_descriptor TEXT NOT NULL,
24
22
  assistant_message_id TEXT NOT NULL,
25
23
  abort_reason TEXT
26
24
  )`;
@@ -28,6 +26,12 @@ export function initializeSchema(sql: SqlTaggedTemplate): void {
28
26
  sql<{ name: string }>`PRAGMA table_info(pi_submissions)`
29
27
  .map((column) => column.name),
30
28
  );
29
+ if (submissionColumns.has("assembly_revision")) {
30
+ sql`ALTER TABLE pi_submissions DROP COLUMN assembly_revision`;
31
+ }
32
+ if (submissionColumns.has("assembly_descriptor")) {
33
+ sql`ALTER TABLE pi_submissions DROP COLUMN assembly_descriptor`;
34
+ }
31
35
  if (!submissionColumns.has("runtime_key")) {
32
36
  sql`ALTER TABLE pi_submissions ADD COLUMN runtime_key TEXT NOT NULL DEFAULT ''`;
33
37
  }
@@ -51,6 +55,18 @@ export function initializeSchema(sql: SqlTaggedTemplate): void {
51
55
  if (!submissionColumns.has("regenerate_message_id")) {
52
56
  sql`ALTER TABLE pi_submissions ADD COLUMN regenerate_message_id TEXT`;
53
57
  }
58
+ if (!submissionColumns.has("origin")) {
59
+ sql`ALTER TABLE pi_submissions ADD COLUMN origin TEXT NOT NULL DEFAULT 'user'`;
60
+ }
61
+ if (!submissionColumns.has("scheduled_max_execution_level")) {
62
+ sql`ALTER TABLE pi_submissions ADD COLUMN scheduled_max_execution_level TEXT`;
63
+ }
64
+ if (!submissionColumns.has("unattended_execution_grant_json")) {
65
+ sql`ALTER TABLE pi_submissions ADD COLUMN unattended_execution_grant_json TEXT`;
66
+ }
67
+ if (!submissionColumns.has("unattended_failure")) {
68
+ sql`ALTER TABLE pi_submissions ADD COLUMN unattended_failure TEXT`;
69
+ }
54
70
  sql`CREATE UNIQUE INDEX IF NOT EXISTS pi_submissions_one_running
55
71
  ON pi_submissions(status)
56
72
  WHERE status = 'running'`;
@@ -156,6 +172,9 @@ export function initializeSchema(sql: SqlTaggedTemplate): void {
156
172
  .map((column) => column.name),
157
173
  );
158
174
  let columns = approvalColumns();
175
+ if (!columns.has("confirmation_json")) {
176
+ sql`ALTER TABLE pi_approvals ADD COLUMN confirmation_json TEXT`;
177
+ }
159
178
  if (!columns.has("execution_level")) {
160
179
  // 旧 Session 没有持久化当时的授权档位,safe 失败关闭;只影响旧 pending 回执展示,不会静默放行。
161
180
  sql`ALTER TABLE pi_approvals
@@ -7,12 +7,12 @@ import {
7
7
  type FiberRecoveryContext,
8
8
  type FiberRecoveryResult,
9
9
  } from "agents";
10
- import type { HarnessEvent } from "@earendil-works/pi-agent-core";
10
+ import type { AgentEvent as HarnessEvent } from "@earendil-works/pi-durable";
11
11
  import type { UIMessageChunk } from "ai";
12
12
  import {
13
13
  AGENT_TYPES,
14
- } from "../../../tools/subagent-types/registry";
15
- import type { AgentType } from "../../../tools/subagent-types/contract";
14
+ } from "../../../tools/subagent/types/registry";
15
+ import type { AgentType } from "../../../tools/subagent/types/contract";
16
16
  import type {
17
17
  RuntimeProviderPort,
18
18
  WorkspacePort,
@@ -30,8 +30,8 @@ import {
30
30
  decodeSubagentCompletion,
31
31
  emptySubagentUsage,
32
32
  encodeSubagentCompletion,
33
- } from "../../../tools/subagent-runtime";
34
- import type { RuntimeSubagentUsageMetrics } from "../../../tools/subagent-authority";
33
+ } from "../../../tools/subagent/runtime";
34
+ import type { RuntimeSubagentUsageMetrics } from "../../../tools/subagent/authority";
35
35
  import { createCloudflareSubAgentTools } from "./tools";
36
36
 
37
37
  type RunStatus =
@@ -96,25 +96,27 @@ function stringifyJson(value: unknown): string {
96
96
  function progressFor(
97
97
  event: HarnessEvent,
98
98
  ): Omit<AgentToolProgressSnapshot, "at"> | null {
99
- if (event.type === "run_start" || event.type === "run_resume") {
99
+ if (event.type === "run_start") {
100
100
  return { phase: "running", message: "SubAgent started" };
101
101
  }
102
- if (event.type === "tool_start") {
102
+ if (event.type === "tool_execution_start") {
103
103
  return {
104
104
  phase: "tool",
105
105
  message: `Running ${event.toolName}`,
106
106
  };
107
107
  }
108
- if (event.type === "tool_update") {
108
+ if (event.type === "tool_execution_update") {
109
109
  return {
110
110
  phase: "tool",
111
111
  message: `${event.toolName} is still running`,
112
112
  };
113
113
  }
114
- if (event.type === "tool_end") {
114
+ if (event.type === "tool_execution_end") {
115
+ const result = event.entry?.model?.[0];
116
+ const isError = result?.role !== "toolResult" || result.isError;
115
117
  return {
116
- phase: event.isError ? "tool-error" : "tool",
117
- message: event.isError
118
+ phase: isError ? "tool-error" : "tool",
119
+ message: isError
118
120
  ? `${event.toolName} failed`
119
121
  : `${event.toolName} completed`,
120
122
  };
@@ -414,14 +416,24 @@ export abstract class CloudflareSubAgent<
414
416
  tools,
415
417
  signal,
416
418
  onEvent: (event) => {
417
- if (event.type === "message_end" && event.message.role === "assistant") {
418
- addSubagentUsage(usage, event.message.usage);
419
+ if (event.type === "message_end" && event.entry.model?.[0]?.role === "assistant") {
420
+ addSubagentUsage(usage, event.entry.model[0].usage);
419
421
  this.persistUsage(runId, usage);
420
422
  }
421
423
  publish(projection!.event(event));
422
424
  const progress = progressFor(event);
423
425
  if (progress) this.persistProgress(runId, progress);
424
426
  },
427
+ // Live frames can stop short of the last response; the canonical entries are the billed total.
428
+ onEntries: (entries) => {
429
+ const canonical = emptySubagentUsage();
430
+ for (const entry of entries) {
431
+ const message = entry.model?.[0];
432
+ if (message?.role === "assistant") addSubagentUsage(canonical, message.usage);
433
+ }
434
+ Object.assign(usage, canonical);
435
+ this.persistUsage(runId, usage);
436
+ },
425
437
  });
426
438
  publish(projection.finish({ status: "completed", endedAt: Date.now() }));
427
439
  this.completeRun(runId, output, usage);
@@ -1,18 +1,14 @@
1
- import {
2
- type AgentHarnessTool,
3
- type HarnessEvent,
4
- type Session,
5
- type StreamFn,
6
- } from "@earendil-works/pi-agent-core";
1
+ import type { StreamFn } from "@earendil-works/pi-agent-core";
2
+ import type { AgentEvent as HarnessEvent, AgentEventStream, EntryRecord, Storage as Session, ToolRegistration } from "@earendil-works/pi-durable";
7
3
  import {
8
4
  awaitWithContext,
9
5
  BACKGROUND_CONTEXT,
10
6
  withAbortSignal,
11
- } from "@earendil-works/pi-agent-core/harness/context";
7
+ } from "@earendil-works/chord/context";
12
8
  import type { Api, AssistantMessage, Model, Models } from "@earendil-works/pi-ai";
13
9
  import { z } from "zod";
14
10
  import { assembleSubagentPrompt } from "../../../lib/prompt";
15
- import type { AgentType } from "../../../tools/subagent-types/contract";
11
+ import type { AgentType } from "../../../tools/subagent/types/contract";
16
12
  import { structuredOutputStream } from "../../../models";
17
13
  import {
18
14
  openSessionHarness,
@@ -39,11 +35,24 @@ export interface CloudflareSubAgentRun {
39
35
  openSession(): Session | Promise<Session>;
40
36
  operationId?: string;
41
37
  /** Tools compiled by the package's single Harness adapter. */
42
- tools?: AgentHarnessTool<object | undefined>[];
38
+ tools?: ToolRegistration<any, any>[];
43
39
  signal?: AbortSignal;
44
40
  timeoutMs?: number;
45
- /** Native operation events for projections that must survive model retries. */
41
+ /** Native operation events for live projections; they may stop short of the final frame. */
46
42
  onEvent?: (event: HarnessEvent, signal: AbortSignal) => void | Promise<void>;
43
+ /** This operation's canonical entries after each settle or release, the source of its final usage. */
44
+ onEntries?: (entries: readonly EntryRecord[]) => void | Promise<void>;
45
+ }
46
+
47
+ /** The first failed Tool result among an operation's canonical entries. */
48
+ function toolFailureOf(entries: readonly EntryRecord[]): { name: string; error: string } | undefined {
49
+ for (const entry of entries) {
50
+ const result = entry.model?.[0];
51
+ if (result?.role !== "toolResult" || !result.isError) continue;
52
+ const diagnostics = (entry.data as { diagnostics?: Array<{ severity: string; message: string }> } | undefined)?.diagnostics;
53
+ return { name: result.toolName, error: diagnostics?.find((item) => item.severity === "error")?.message ?? toolErrorText(result) };
54
+ }
55
+ return undefined;
47
56
  }
48
57
 
49
58
  function textOf(message: AssistantMessage): string {
@@ -131,9 +140,15 @@ export async function runCloudflareSubAgent(run: CloudflareSubAgentRun): Promise
131
140
  streamOptions: { maxRetries: 0 },
132
141
  toolExecution: "parallel",
133
142
  compaction: { enabled: false, reserveTokens: 1, keepRecentTokens: 1 },
134
- } satisfies Omit<SessionHarnessOptions<object | undefined>, "session">;
135
- let runtime: SessionHarness<object | undefined> | undefined;
143
+ } satisfies Omit<SessionHarnessOptions, "storage">;
144
+ let runtime: SessionHarness | undefined;
145
+ let events: AgentEventStream | undefined;
136
146
  let toolFailure: { name: string; error: string } | undefined;
147
+ const operationEntries = async (active: SessionHarness): Promise<EntryRecord[]> => {
148
+ const submission = await active.submissionFor(operationId, BACKGROUND_CONTEXT);
149
+ const entries = await active.entries(BACKGROUND_CONTEXT);
150
+ return submission?.entry === undefined ? [] : entries.filter((entry) => entry.id >= submission.entry!);
151
+ };
137
152
 
138
153
  let timedOut = false;
139
154
  const timeout = setTimeout(() => {
@@ -141,61 +156,43 @@ export async function runCloudflareSubAgent(run: CloudflareSubAgentRun): Promise
141
156
  controller.abort(new Error("SubAgent run timed out"));
142
157
  }, run.timeoutMs ?? DEFAULT_TIMEOUT_MS);
143
158
  try {
144
- let settled: Awaited<ReturnType<SessionHarness<object | undefined>["run"]>>;
159
+ let settled: Awaited<ReturnType<SessionHarness["run"]>>;
145
160
  while (true) {
146
161
  signal.throwIfAborted();
147
162
  if (!runtime) {
148
163
  runtime = await openSessionHarness({
149
164
  ...options,
150
- session: await run.openSession(),
165
+ storage: await run.openSession(),
151
166
  }, context);
152
167
  if (runtime.openOperation && runtime.openOperation.operationId !== operationId) {
153
168
  throw new Error("SubAgent Session has an unrelated open operation");
154
169
  }
155
- const harness = runtime.harness;
156
- const onEvent = run.onEvent;
157
- if (onEvent) {
158
- for (const type of ["run_start", "run_resume", "run_end", "turn_start", "message_start", "message_update", "message_end", "tool_start", "tool_update", "tool_end"] as const) {
159
- harness.events.on(type, (event) => onEvent(event, signal));
160
- }
161
- }
162
- harness.events.on("tool_end", (event) => {
163
- if (event.isError) {
164
- toolFailure ??= { name: event.toolName, error: toolErrorText(event.result) };
165
- }
170
+ events = await runtime.watch(context);
171
+ events.start(async (batch) => {
172
+ for (const event of batch) await run.onEvent?.(event, signal);
166
173
  });
167
174
  }
168
175
  const result = await runtime.run(
169
176
  !runtime.openOperation
170
177
  ? {
171
- kind: "prompt",
172
178
  operationId,
173
179
  prompt: taskPrompt(input.data, structuredStream ? undefined : outputSchema),
174
180
  }
175
181
  : undefined,
176
182
  context,
177
- { waitForRetry: true, pollDeferred: true },
178
183
  );
184
+ await events?.stop();
185
+ events = undefined;
179
186
  if (result.kind === "released") {
180
187
  await runtime.close(BACKGROUND_CONTEXT);
181
188
  runtime = undefined;
182
189
  continue;
183
190
  }
184
- if (result.kind === "waiting") {
185
- const delay = result.reason === "deferred"
186
- ? result.deferred.pollAfterMs ?? 1_000
187
- : Math.max(0, result.notBefore - Date.now());
188
- let timer: ReturnType<typeof setTimeout> | undefined;
189
- try {
190
- await awaitWithContext(new Promise<void>((resolve) => {
191
- timer = setTimeout(resolve, delay);
192
- }), context);
193
- } finally {
194
- clearTimeout(timer);
195
- }
196
- continue;
197
- }
198
191
  settled = result;
192
+ // Pi's stop may drop the last frames; failures and usage come from the canonical record.
193
+ const owned = await operationEntries(runtime);
194
+ toolFailure = toolFailureOf(owned);
195
+ await run.onEntries?.(owned);
199
196
  break;
200
197
  }
201
198
  if (timedOut) throw new Error("SubAgent run timed out");
@@ -212,14 +209,9 @@ export async function runCloudflareSubAgent(run: CloudflareSubAgentRun): Promise
212
209
  if (toolFailure) {
213
210
  throw new Error(`SubAgent tool "${toolFailure.name}" failed: ${toolFailure.error}`);
214
211
  }
215
- const assistant = (await runtime.lane.findEntries(
216
- { type: "message", order: "newestFirst" },
217
- context,
218
- )).find((entry) => entry.type === "message" && entry.message.role === "assistant");
219
- if (!assistant || assistant.type !== "message" || assistant.message.role !== "assistant") {
220
- throw new Error("SubAgent produced no structured output");
221
- }
222
- const text = textOf(assistant.message);
212
+ const assistant = [...await runtime.entries(context)].reverse().flatMap((entry) => entry.model ?? []).find((message) => message.role === "assistant");
213
+ if (assistant?.role !== "assistant") throw new Error("SubAgent produced no structured output");
214
+ const text = textOf(assistant);
223
215
  if (!text) throw new Error("SubAgent produced no structured output");
224
216
  let output: unknown;
225
217
  try {
@@ -239,9 +231,10 @@ export async function runCloudflareSubAgent(run: CloudflareSubAgentRun): Promise
239
231
  } finally {
240
232
  clearTimeout(timeout);
241
233
  if (signal.aborted && runtime?.openOperation) {
242
- await runtime.requestAbort(operationId, BACKGROUND_CONTEXT).catch(() => undefined);
234
+ await runtime.abort(BACKGROUND_CONTEXT).catch(() => undefined);
243
235
  await runtime.run(undefined, BACKGROUND_CONTEXT).catch(() => undefined);
244
236
  }
237
+ await events?.stop();
245
238
  await runtime?.close(BACKGROUND_CONTEXT).catch(() => undefined);
246
239
  }
247
240
  }