@springbrand/agent-runtime 0.2.0-alpha.15 → 0.2.0-alpha.17
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/sandbox/adapter.ts +61 -36
- package/src/adapter/cloudflare/universal-agent/preparation.ts +0 -2
- package/src/adapter/cloudflare/workspace/scoped-workspace.ts +23 -18
- package/src/db/index.ts +5 -0
- package/src/db/schema.ts +15 -0
- package/src/db/telemetry-outbox.repo.ts +151 -0
- package/src/index.ts +1 -0
- package/src/kernel/approval-lifecycle.ts +35 -3
- package/src/kernel/bindings.ts +2 -1
- package/src/kernel/interaction-lifecycle.ts +35 -6
- package/src/layers/orchestration/temporary-agent/workspace.ts +4 -4
- package/src/lib/prompt.ts +22 -15
- package/src/pi/assembly/context.ts +2 -2
- package/src/pi/message/conversion.ts +13 -1
- package/src/pi/runtime-adapter/assembly.ts +1 -0
- package/src/pi/runtime-adapter/execution.ts +63 -27
- package/src/pi/runtime-adapter/models.ts +144 -44
- package/src/pi/tool/ai-adapter.ts +2 -2
- package/src/pi/tool/base.ts +31 -25
- package/src/pi/tool/compiler.ts +6 -102
- package/src/pi/turn/tool-recovery.ts +11 -1
- package/src/runtime-agent.ts +24 -0
- package/src/runtime-assembler.ts +38 -19
- package/src/runtime-definition.ts +2 -0
- package/src/runtime.ts +362 -20
- package/src/telemetry/contract.ts +389 -0
- package/src/telemetry/coordinator.ts +143 -0
- package/src/telemetry/delivery.ts +138 -0
- package/src/telemetry/ids.ts +60 -0
- package/src/telemetry/index.ts +7 -0
- package/src/telemetry/recorder.ts +61 -0
- package/src/telemetry/runtime-telemetry.ts +484 -0
- package/src/telemetry/sanitize.ts +97 -0
- package/src/tool-registry.ts +18 -11
- package/src/lib/telemetry-dev.ts +0 -47
|
@@ -43,10 +43,6 @@ import {
|
|
|
43
43
|
import {
|
|
44
44
|
ChatStreamStalledError,
|
|
45
45
|
} from "agents/chat";
|
|
46
|
-
import {
|
|
47
|
-
genericObservability,
|
|
48
|
-
type ObservabilityEvent,
|
|
49
|
-
} from "agents/observability";
|
|
50
46
|
import type {
|
|
51
47
|
RuntimeModelEndpoint,
|
|
52
48
|
RuntimeModelProtocol,
|
|
@@ -87,6 +83,35 @@ export interface ModelStreamStallDetails {
|
|
|
87
83
|
idleMs: number;
|
|
88
84
|
}
|
|
89
85
|
|
|
86
|
+
export type PiGenerationLifecycleEvent =
|
|
87
|
+
| {
|
|
88
|
+
readonly type: "started";
|
|
89
|
+
readonly generationId: string;
|
|
90
|
+
readonly timestamp: number;
|
|
91
|
+
readonly api: string;
|
|
92
|
+
readonly provider: string;
|
|
93
|
+
readonly model: string;
|
|
94
|
+
readonly url: string;
|
|
95
|
+
readonly input?: unknown;
|
|
96
|
+
}
|
|
97
|
+
| {
|
|
98
|
+
readonly type: "finished";
|
|
99
|
+
readonly generationId: string;
|
|
100
|
+
readonly timestamp: number;
|
|
101
|
+
readonly durationMs: number;
|
|
102
|
+
readonly outcome: "success" | "error" | "cancelled";
|
|
103
|
+
readonly stopReason?: string;
|
|
104
|
+
readonly errorName?: string;
|
|
105
|
+
readonly upstreamRequestId?: string;
|
|
106
|
+
readonly responseStatus?: number;
|
|
107
|
+
readonly usage?: AssistantMessage["usage"];
|
|
108
|
+
readonly output?: AssistantMessage;
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
export type PiGenerationLifecycleObserver = (
|
|
112
|
+
event: PiGenerationLifecycleEvent,
|
|
113
|
+
) => void;
|
|
114
|
+
|
|
90
115
|
export function isModelStreamStallMessage(message?: string): message is string {
|
|
91
116
|
return message === MODEL_STREAM_STALL_MESSAGE ||
|
|
92
117
|
message?.startsWith(MODEL_STREAM_STALL_DETAILS_PREFIX) === true;
|
|
@@ -194,11 +219,11 @@ function meaningfulModelProgress(
|
|
|
194
219
|
}
|
|
195
220
|
|
|
196
221
|
async function* stopStalledModelStream(
|
|
197
|
-
source: AsyncIterable<AssistantMessageEvent
|
|
222
|
+
source: Promise<AsyncIterable<AssistantMessageEvent>>,
|
|
198
223
|
watchdog: AbortController,
|
|
199
224
|
probe: (phase: string, details?: Record<string, unknown>) => void,
|
|
200
225
|
): AsyncGenerator<AssistantMessageEvent> {
|
|
201
|
-
|
|
226
|
+
let iterator: AsyncIterator<AssistantMessageEvent> | undefined;
|
|
202
227
|
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
203
228
|
let stalled = false;
|
|
204
229
|
let stallError: ChatStreamStalledError | undefined;
|
|
@@ -246,7 +271,13 @@ async function* stopStalledModelStream(
|
|
|
246
271
|
try {
|
|
247
272
|
while (true) {
|
|
248
273
|
const waitStartedAt = Date.now();
|
|
249
|
-
const nextPromise = iterator
|
|
274
|
+
const nextPromise = iterator
|
|
275
|
+
? iterator.next()
|
|
276
|
+
: source.then((stream) => {
|
|
277
|
+
const resolved = stream[Symbol.asyncIterator]();
|
|
278
|
+
iterator = resolved;
|
|
279
|
+
return resolved.next();
|
|
280
|
+
});
|
|
250
281
|
nextPromise.catch(() => {});
|
|
251
282
|
let next: IteratorResult<AssistantMessageEvent>;
|
|
252
283
|
try {
|
|
@@ -259,7 +290,7 @@ async function* stopStalledModelStream(
|
|
|
259
290
|
]);
|
|
260
291
|
} catch (error) {
|
|
261
292
|
if (stalled) throw stallError;
|
|
262
|
-
probe("iterator_error", {
|
|
293
|
+
probe(iterator ? "iterator_error" : "dispatch_error", {
|
|
263
294
|
errorName: error instanceof Error ? error.name : typeof error,
|
|
264
295
|
rawEventCount,
|
|
265
296
|
meaningfulEventCount,
|
|
@@ -284,6 +315,10 @@ async function* stopStalledModelStream(
|
|
|
284
315
|
stopReason: event.type === "done"
|
|
285
316
|
? event.message.stopReason
|
|
286
317
|
: event.error.stopReason,
|
|
318
|
+
usage: event.type === "done"
|
|
319
|
+
? event.message.usage
|
|
320
|
+
: event.error.usage,
|
|
321
|
+
output: event.type === "done" ? event.message : event.error,
|
|
287
322
|
rawEventCount,
|
|
288
323
|
meaningfulEventCount,
|
|
289
324
|
});
|
|
@@ -305,7 +340,7 @@ async function* stopStalledModelStream(
|
|
|
305
340
|
}
|
|
306
341
|
} finally {
|
|
307
342
|
clearTimeout(timer);
|
|
308
|
-
if (!stalled) await iterator
|
|
343
|
+
if (!stalled) await iterator?.return?.().catch(() => {});
|
|
309
344
|
}
|
|
310
345
|
probe("ended_without_terminal", {
|
|
311
346
|
rawEventCount,
|
|
@@ -354,23 +389,15 @@ export function modelRequestUrl(model: Model<Api>): string {
|
|
|
354
389
|
* 调用方可以覆盖默认的 2 次 Provider 重试;主 Turn 传 0,由 Submission
|
|
355
390
|
* 统一持有恢复预算。连续 {@link MODEL_STREAM_STALL_TIMEOUT_MS} 毫秒没有可展示进展时中止 provider。
|
|
356
391
|
*
|
|
357
|
-
*
|
|
358
|
-
*
|
|
359
|
-
*
|
|
360
|
-
* 关掉即零订阅 no-op;将来若接上 `tail_consumers`,这条也自动跟着走。
|
|
361
|
-
*
|
|
362
|
-
* 每条都带 `url`(而不是只在 dispatch 带一次):模型路由只存在于 secret 里,
|
|
363
|
-
* 线上排查时最需要回答的就是"这次打到哪个 URL",让每行自解释比省几十字节值。
|
|
364
|
-
* payload 只含路由与时序,不含 key 和消息内容。
|
|
365
|
-
*
|
|
366
|
-
* 注意这里用的是模块级 `genericObservability`,不是 Agent 实例的 `_emit` ——
|
|
367
|
-
* 纯模块拿不到实例,代价是事件不带 `agent` / `name` 字段;turn 的身份由
|
|
368
|
-
* payload 里的 `requestId` / `sessionId` 承担。
|
|
392
|
+
* 可选 observer 接收配对的 generation started/finished 事实及实际模型输入输出;响应头仅保留
|
|
393
|
+
* 白名单 request id,永不携带 key 或任意响应 header。正文由 telemetry 隐私模式统一脱敏。
|
|
394
|
+
* observer 的异常被隔离,不能改变模型执行结果。
|
|
369
395
|
*/
|
|
370
396
|
export function withProviderRetry(
|
|
371
397
|
streamFn: StreamFn,
|
|
372
398
|
defaultMaxRetries = PROVIDER_MAX_RETRIES,
|
|
373
399
|
defaultSessionId?: string,
|
|
400
|
+
onGeneration?: PiGenerationLifecycleObserver,
|
|
374
401
|
): StreamFn {
|
|
375
402
|
return (model, context, options) =>
|
|
376
403
|
lazyStream(model, async () => {
|
|
@@ -378,30 +405,102 @@ export function withProviderRetry(
|
|
|
378
405
|
const startedAt = Date.now();
|
|
379
406
|
const sessionId = options?.sessionId ?? defaultSessionId;
|
|
380
407
|
const url = modelRequestUrl(model);
|
|
381
|
-
const
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
}
|
|
400
|
-
|
|
408
|
+
const notify = (event: PiGenerationLifecycleEvent) => {
|
|
409
|
+
try {
|
|
410
|
+
onGeneration?.(event);
|
|
411
|
+
} catch {
|
|
412
|
+
// Observability must not change model execution.
|
|
413
|
+
}
|
|
414
|
+
};
|
|
415
|
+
notify({
|
|
416
|
+
type: "started",
|
|
417
|
+
generationId: requestId,
|
|
418
|
+
timestamp: startedAt,
|
|
419
|
+
api: model.api,
|
|
420
|
+
provider: model.provider,
|
|
421
|
+
model: model.id,
|
|
422
|
+
url,
|
|
423
|
+
input: {
|
|
424
|
+
...(context.systemPrompt ? { systemPrompt: context.systemPrompt } : {}),
|
|
425
|
+
messages: context.messages,
|
|
426
|
+
},
|
|
427
|
+
});
|
|
401
428
|
const watchdog = new AbortController();
|
|
402
429
|
let responseCount = 0;
|
|
430
|
+
let upstreamRequestId: string | undefined;
|
|
431
|
+
let responseStatus: number | undefined;
|
|
432
|
+
let finished = false;
|
|
433
|
+
const finish = (
|
|
434
|
+
outcome: "success" | "error" | "cancelled",
|
|
435
|
+
details: {
|
|
436
|
+
stopReason?: string;
|
|
437
|
+
errorName?: string;
|
|
438
|
+
usage?: AssistantMessage["usage"];
|
|
439
|
+
output?: AssistantMessage;
|
|
440
|
+
} = {},
|
|
441
|
+
) => {
|
|
442
|
+
if (finished) return;
|
|
443
|
+
finished = true;
|
|
444
|
+
notify({
|
|
445
|
+
type: "finished",
|
|
446
|
+
generationId: requestId,
|
|
447
|
+
timestamp: Date.now(),
|
|
448
|
+
durationMs: Math.max(0, Date.now() - startedAt),
|
|
449
|
+
outcome,
|
|
450
|
+
...details,
|
|
451
|
+
...(upstreamRequestId ? { upstreamRequestId } : {}),
|
|
452
|
+
...(responseStatus === undefined ? {} : { responseStatus }),
|
|
453
|
+
});
|
|
454
|
+
};
|
|
455
|
+
const probe = (phase: string, details: Record<string, unknown> = {}) => {
|
|
456
|
+
if (phase === "response_headers") {
|
|
457
|
+
if (typeof details.upstreamRequestId === "string") {
|
|
458
|
+
upstreamRequestId = details.upstreamRequestId;
|
|
459
|
+
}
|
|
460
|
+
if (typeof details.status === "number") {
|
|
461
|
+
responseStatus = details.status;
|
|
462
|
+
}
|
|
463
|
+
return;
|
|
464
|
+
}
|
|
465
|
+
if (phase === "done") {
|
|
466
|
+
finish("success", {
|
|
467
|
+
...(typeof details.stopReason === "string"
|
|
468
|
+
? { stopReason: details.stopReason }
|
|
469
|
+
: {}),
|
|
470
|
+
...(details.usage
|
|
471
|
+
? { usage: details.usage as AssistantMessage["usage"] }
|
|
472
|
+
: {}),
|
|
473
|
+
...(details.output
|
|
474
|
+
? { output: details.output as AssistantMessage }
|
|
475
|
+
: {}),
|
|
476
|
+
});
|
|
477
|
+
return;
|
|
478
|
+
}
|
|
479
|
+
if (
|
|
480
|
+
phase === "error" ||
|
|
481
|
+
phase === "stall" ||
|
|
482
|
+
phase === "iterator_error" ||
|
|
483
|
+
phase === "dispatch_error" ||
|
|
484
|
+
phase === "ended_without_terminal"
|
|
485
|
+
) {
|
|
486
|
+
finish(options?.signal?.aborted ? "cancelled" : "error", {
|
|
487
|
+
...(typeof details.stopReason === "string"
|
|
488
|
+
? { stopReason: details.stopReason }
|
|
489
|
+
: {}),
|
|
490
|
+
...(typeof details.errorName === "string"
|
|
491
|
+
? { errorName: details.errorName }
|
|
492
|
+
: {}),
|
|
493
|
+
...(details.usage
|
|
494
|
+
? { usage: details.usage as AssistantMessage["usage"] }
|
|
495
|
+
: {}),
|
|
496
|
+
...(details.output
|
|
497
|
+
? { output: details.output as AssistantMessage }
|
|
498
|
+
: {}),
|
|
499
|
+
});
|
|
500
|
+
}
|
|
501
|
+
};
|
|
403
502
|
try {
|
|
404
|
-
const
|
|
503
|
+
const sourcePromise = Promise.resolve(streamFn(model, context, {
|
|
405
504
|
...options,
|
|
406
505
|
signal: options?.signal
|
|
407
506
|
? AbortSignal.any([options.signal, watchdog.signal])
|
|
@@ -423,8 +522,9 @@ export function withProviderRetry(
|
|
|
423
522
|
});
|
|
424
523
|
await options?.onResponse?.(response, activeModel);
|
|
425
524
|
},
|
|
426
|
-
});
|
|
427
|
-
|
|
525
|
+
}));
|
|
526
|
+
sourcePromise.catch(() => {});
|
|
527
|
+
return stopStalledModelStream(sourcePromise, watchdog, probe);
|
|
428
528
|
} catch (error) {
|
|
429
529
|
probe("dispatch_error", {
|
|
430
530
|
errorName: error instanceof Error ? error.name : typeof error,
|
|
@@ -89,9 +89,9 @@ function modelContent(
|
|
|
89
89
|
*
|
|
90
90
|
* Workspace、Skill 和其他 ai-sdk 工具工厂在把候选项交给 `compilePiTools()` 前调用。
|
|
91
91
|
*
|
|
92
|
-
* 实现必须在一处同时对齐 schema、execute 参数和结果形状,否则 Pi 的校验、取消信号和 artifact
|
|
92
|
+
* 实现必须在一处同时对齐 schema、execute 参数和结果形状,否则 Pi 的校验、取消信号和 artifact 处理会绕过统一工具边界。
|
|
93
93
|
*
|
|
94
|
-
* @remarks `inputSchema` 经 ai@7 `asSchema()` 转为 JSON Schema;原始返回值保留在 `details`
|
|
94
|
+
* @remarks `inputSchema` 经 ai@7 `asSchema()` 转为 JSON Schema;原始返回值保留在 `details` 中。
|
|
95
95
|
*/
|
|
96
96
|
export function aiToolToPi(
|
|
97
97
|
name: string,
|
package/src/pi/tool/base.ts
CHANGED
|
@@ -276,31 +276,37 @@ export function basePiToolCandidates(
|
|
|
276
276
|
},
|
|
277
277
|
},
|
|
278
278
|
},
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
279
|
+
{
|
|
280
|
+
...candidate({
|
|
281
|
+
name: "suggest_followups",
|
|
282
|
+
label: "Suggest follow-ups",
|
|
283
|
+
description:
|
|
284
|
+
"Offer the user 2-4 optional follow-up directions after completing a substantive task (a report, an analysis, a multi-step job). Call this at most once, and it MUST be the very last thing you do: finish all of your prose FIRST, then call this tool and END the turn immediately — do NOT write any text after calling it. Do NOT call it for small talk, quick answers, or while a task is still in progress.",
|
|
285
|
+
parameters: suggestFollowupsParameters,
|
|
286
|
+
async execute(_toolCallId, input) {
|
|
287
|
+
return result({ noted: true, count: input.items.length });
|
|
288
|
+
},
|
|
289
|
+
}),
|
|
290
|
+
direct: true,
|
|
291
|
+
},
|
|
292
|
+
{
|
|
293
|
+
...candidate({
|
|
294
|
+
name: "update_plan",
|
|
295
|
+
label: "Update plan",
|
|
296
|
+
description:
|
|
297
|
+
"Maintain the user-visible plan for the current task. Call this whenever a task involves 2 or more steps, and again every time the plan or a step's status changes. Always pass the FULL plan — it replaces the previous plan entirely (idempotent overwrite), so omitted steps disappear.",
|
|
298
|
+
parameters: updatePlanParameters,
|
|
299
|
+
prepareArguments: normalizeUpdatePlanArguments,
|
|
300
|
+
async execute(_toolCallId, input) {
|
|
301
|
+
return result({
|
|
302
|
+
ok: true,
|
|
303
|
+
total: input.steps.length,
|
|
304
|
+
done: input.steps.filter((step) => step.status === "done").length,
|
|
305
|
+
});
|
|
306
|
+
},
|
|
307
|
+
}),
|
|
308
|
+
direct: true,
|
|
309
|
+
},
|
|
304
310
|
...(webSearch ? [webSearchPiToolCandidate(webSearch)] : []),
|
|
305
311
|
];
|
|
306
312
|
}
|
package/src/pi/tool/compiler.ts
CHANGED
|
@@ -46,6 +46,10 @@ export interface PiToolInteractionSpec {
|
|
|
46
46
|
export interface PiToolCandidate {
|
|
47
47
|
readonly owner: string;
|
|
48
48
|
readonly tool: AgentTool<any, any>;
|
|
49
|
+
/** Keep this Tool Direct-only instead of also offering it through Code Mode. */
|
|
50
|
+
readonly direct?: true;
|
|
51
|
+
/** @internal Tools also callable through this Code Mode candidate. */
|
|
52
|
+
readonly codeExecutionTools?: readonly PiToolCandidate[];
|
|
49
53
|
/** Conservative maximum used in the stable Runtime descriptor. */
|
|
50
54
|
readonly requiredExecutionLevel: ExecutionLevel;
|
|
51
55
|
/** Trusted parameter-level policy, evaluated before approval or dispatch. */
|
|
@@ -76,15 +80,6 @@ export interface CompilePiToolsOptions {
|
|
|
76
80
|
) => void | Promise<void>;
|
|
77
81
|
}
|
|
78
82
|
|
|
79
|
-
/** 记录一次 Pi 工具调用的耗时、结果大小和溢出状态。 */
|
|
80
|
-
export interface PiToolTelemetry {
|
|
81
|
-
readonly tool: string;
|
|
82
|
-
readonly ms: number;
|
|
83
|
-
readonly ok: boolean;
|
|
84
|
-
readonly bytes: number;
|
|
85
|
-
readonly spill: boolean;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
83
|
const governanceState = Symbol("PiToolGovernanceState");
|
|
89
84
|
|
|
90
85
|
/** 向 Pi Agent 提供工具后置钩子,并为编译器保留同一 Turn 的治理状态。 */
|
|
@@ -114,7 +109,6 @@ interface PiToolGovernanceState {
|
|
|
114
109
|
readonly terminalCalls: Set<string>;
|
|
115
110
|
totalFailures: number;
|
|
116
111
|
aborted: boolean;
|
|
117
|
-
readonly telemetry?: (event: PiToolTelemetry) => void;
|
|
118
112
|
}
|
|
119
113
|
|
|
120
114
|
// #endregion
|
|
@@ -161,57 +155,6 @@ function callKey(toolName: string, args: unknown): string {
|
|
|
161
155
|
return `${toolName}\0${(hash >>> 0).toString(36)}`;
|
|
162
156
|
}
|
|
163
157
|
|
|
164
|
-
// 估算工具结果给 Durable Object 带来的字节压力。
|
|
165
|
-
// `emitToolTelemetry()` 在每次成功、失败或阻断后调用它。
|
|
166
|
-
// artifact_ref 已把大结果外溢,因此必须使用 details 中的原始字节数而不是小引用自身的大小。
|
|
167
|
-
function resultPressure(result: AgentToolResult<unknown>): {
|
|
168
|
-
bytes: number;
|
|
169
|
-
spill: boolean;
|
|
170
|
-
} {
|
|
171
|
-
const details = result.details as {
|
|
172
|
-
kind?: string;
|
|
173
|
-
bytes?: number;
|
|
174
|
-
} | undefined;
|
|
175
|
-
if (
|
|
176
|
-
details?.kind === "artifact_ref" &&
|
|
177
|
-
typeof details.bytes === "number"
|
|
178
|
-
) {
|
|
179
|
-
return { bytes: details.bytes, spill: true };
|
|
180
|
-
}
|
|
181
|
-
try {
|
|
182
|
-
return {
|
|
183
|
-
bytes: JSON.stringify(result).length,
|
|
184
|
-
spill: false,
|
|
185
|
-
};
|
|
186
|
-
} catch {
|
|
187
|
-
return { bytes: 0, spill: false };
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
// 尽力把一次工具调用的计量事件交给可选遥测端口。
|
|
192
|
-
// 受治理 execute 方法在每条终止路径上调用。
|
|
193
|
-
// 遥测是观测而非业务逻辑,所以端口抛错必须被吞掉,不能改变工具结果。
|
|
194
|
-
function emitToolTelemetry(
|
|
195
|
-
state: PiToolGovernanceState | undefined,
|
|
196
|
-
tool: string,
|
|
197
|
-
startedAt: number,
|
|
198
|
-
ok: boolean,
|
|
199
|
-
result: AgentToolResult<unknown>,
|
|
200
|
-
): void {
|
|
201
|
-
if (!state?.telemetry) return;
|
|
202
|
-
const pressure = resultPressure(result);
|
|
203
|
-
try {
|
|
204
|
-
state.telemetry({
|
|
205
|
-
tool,
|
|
206
|
-
ms: Math.max(0, Math.round(performance.now() - startedAt)),
|
|
207
|
-
ok,
|
|
208
|
-
...pressure,
|
|
209
|
-
});
|
|
210
|
-
} catch {
|
|
211
|
-
// Observability must not change Tool execution.
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
|
|
215
158
|
// Most Pi adapters keep the original Tool value in details and its serialized
|
|
216
159
|
// model form in one text block. Spill that original once instead of duplicating
|
|
217
160
|
// it inside an AgentToolResult JSON envelope.
|
|
@@ -311,15 +254,12 @@ function blockReason(
|
|
|
311
254
|
*
|
|
312
255
|
* 状态用 symbol 绑定到钩子对象,让编译后的工具和 Pi 后置钩子共享一份 Turn 计数而不暴露公开可变 API。
|
|
313
256
|
*/
|
|
314
|
-
export function createPiToolGovernance(
|
|
315
|
-
telemetry?: (event: PiToolTelemetry) => void,
|
|
316
|
-
): PiToolGovernance {
|
|
257
|
+
export function createPiToolGovernance(): PiToolGovernance {
|
|
317
258
|
const state: PiToolGovernanceState = {
|
|
318
259
|
failures: new Map(),
|
|
319
260
|
terminalCalls: new Set(),
|
|
320
261
|
totalFailures: 0,
|
|
321
262
|
aborted: false,
|
|
322
|
-
telemetry,
|
|
323
263
|
};
|
|
324
264
|
const governance: PiToolGovernance = {
|
|
325
265
|
[governanceState]: state,
|
|
@@ -336,7 +276,7 @@ export function createPiToolGovernance(
|
|
|
336
276
|
return governance;
|
|
337
277
|
}
|
|
338
278
|
|
|
339
|
-
//
|
|
279
|
+
// 为一个候选工具包上输出限额、持久化结算和重试治理。
|
|
340
280
|
// `compilePiTools()` 对最终 Tool Surface 中的每个候选项调用。
|
|
341
281
|
// 一个共享包装边界可确保所有工具无论成功还是失败都经过 settle,不能由各工具自行选择是否持久化。
|
|
342
282
|
function governedTool(
|
|
@@ -353,7 +293,6 @@ function governedTool(
|
|
|
353
293
|
// Pi 工具循环选中编译后的工具时调用,调用方应传入稳定 toolCall id 供结算去重。
|
|
354
294
|
// 输出限额必须发生在 settle 之前,否则 Durable Object 会持久化一份与模型最终所见不同的过大结果。
|
|
355
295
|
async execute(toolCallId, args, signal, onUpdate) {
|
|
356
|
-
const startedAt = performance.now();
|
|
357
296
|
const blocked = blockReason(
|
|
358
297
|
state,
|
|
359
298
|
toolCallId,
|
|
@@ -378,22 +317,8 @@ function governedTool(
|
|
|
378
317
|
source: candidate.source,
|
|
379
318
|
});
|
|
380
319
|
} catch (settlementFailure) {
|
|
381
|
-
emitToolTelemetry(
|
|
382
|
-
state,
|
|
383
|
-
candidate.tool.name,
|
|
384
|
-
startedAt,
|
|
385
|
-
false,
|
|
386
|
-
result,
|
|
387
|
-
);
|
|
388
320
|
throw settlementFailure;
|
|
389
321
|
}
|
|
390
|
-
emitToolTelemetry(
|
|
391
|
-
state,
|
|
392
|
-
candidate.tool.name,
|
|
393
|
-
startedAt,
|
|
394
|
-
false,
|
|
395
|
-
result,
|
|
396
|
-
);
|
|
397
322
|
throw cause;
|
|
398
323
|
}
|
|
399
324
|
|
|
@@ -432,13 +357,6 @@ function governedTool(
|
|
|
432
357
|
candidate.tool.name,
|
|
433
358
|
args,
|
|
434
359
|
);
|
|
435
|
-
emitToolTelemetry(
|
|
436
|
-
state,
|
|
437
|
-
candidate.tool.name,
|
|
438
|
-
startedAt,
|
|
439
|
-
false,
|
|
440
|
-
result,
|
|
441
|
-
);
|
|
442
360
|
const boundedMessage = (result.content[0] as { text: string }).text;
|
|
443
361
|
throw cause instanceof Error &&
|
|
444
362
|
failure === cause &&
|
|
@@ -468,25 +386,11 @@ function governedTool(
|
|
|
468
386
|
candidate.tool.name,
|
|
469
387
|
args,
|
|
470
388
|
);
|
|
471
|
-
emitToolTelemetry(
|
|
472
|
-
state,
|
|
473
|
-
candidate.tool.name,
|
|
474
|
-
startedAt,
|
|
475
|
-
false,
|
|
476
|
-
result,
|
|
477
|
-
);
|
|
478
389
|
throw cause;
|
|
479
390
|
}
|
|
480
391
|
state?.failures.delete(
|
|
481
392
|
callKey(candidate.tool.name, args),
|
|
482
393
|
);
|
|
483
|
-
emitToolTelemetry(
|
|
484
|
-
state,
|
|
485
|
-
candidate.tool.name,
|
|
486
|
-
startedAt,
|
|
487
|
-
true,
|
|
488
|
-
result,
|
|
489
|
-
);
|
|
490
394
|
return result;
|
|
491
395
|
},
|
|
492
396
|
};
|
|
@@ -732,7 +732,17 @@ export function planPiToolRecovery(
|
|
|
732
732
|
}
|
|
733
733
|
|
|
734
734
|
// 已结算 Tool 先补齐续跑;未结算 Tool 只有声明幂等时才自动重试。
|
|
735
|
-
|
|
735
|
+
const tools = Object.values(state.toolCalls);
|
|
736
|
+
const codeModeParents = tools
|
|
737
|
+
.filter((tool) => tool.toolName === "execute")
|
|
738
|
+
.map((tool) => `${tool.toolCallId}:`);
|
|
739
|
+
for (const tool of tools) {
|
|
740
|
+
// 旧版把 Code Mode 内层调用写成独立 Pi attempt,但只有父
|
|
741
|
+
// execute 会产生 settlement。这些带父调用前缀的记录不是可
|
|
742
|
+
// 独立恢复的 Tool;父 execute 才是唯一恢复边界。
|
|
743
|
+
if (codeModeParents.some((prefix) => tool.toolCallId.startsWith(prefix))) {
|
|
744
|
+
continue;
|
|
745
|
+
}
|
|
736
746
|
if (tool.result) {
|
|
737
747
|
const key = `tool:${tool.toolCallId}:settled`;
|
|
738
748
|
const plan = tool.needsContinuation
|
package/src/runtime-agent.ts
CHANGED
|
@@ -56,6 +56,7 @@ import type {
|
|
|
56
56
|
RuntimeAgentRole,
|
|
57
57
|
RuntimeAgentToolResult,
|
|
58
58
|
} from "./runtime-agent-context";
|
|
59
|
+
import type { AgentTelemetryBinding } from "./telemetry/contract";
|
|
59
60
|
|
|
60
61
|
/**
|
|
61
62
|
* 本文件把应用提供的 Config Definition 与 Planner 接到 Cloudflare Agent 生命周期。
|
|
@@ -111,6 +112,11 @@ interface RuntimeAgentDefinitionBase<
|
|
|
111
112
|
| RuntimeAgentHooks<Env, Config, Command, Change>
|
|
112
113
|
| ((context: RuntimeAgentPlanningContext<Env, Command, Change>) =>
|
|
113
114
|
RuntimeAgentHooks<Env, Config, Command, Change>);
|
|
115
|
+
|
|
116
|
+
/** Prepare the optional telemetry consumer for this concrete Agent facet. */
|
|
117
|
+
readonly telemetry?: (
|
|
118
|
+
context: RuntimeAgentPlanningContext<Env, Command, Change>,
|
|
119
|
+
) => AgentTelemetryBinding | undefined;
|
|
114
120
|
}
|
|
115
121
|
|
|
116
122
|
export interface ReadonlyRuntimeAgentDefinition<
|
|
@@ -415,11 +421,23 @@ export function defineRuntimeAgent<
|
|
|
415
421
|
: hooks as RuntimeAgentHooks<Env, Config, Command, Change>;
|
|
416
422
|
};
|
|
417
423
|
|
|
424
|
+
const resolveDefinitionTelemetry = (
|
|
425
|
+
context: RuntimeAgentPlanningContext<Env, Command, Change>,
|
|
426
|
+
): AgentTelemetryBinding | undefined => {
|
|
427
|
+
const telemetry = definition.telemetry;
|
|
428
|
+
if (!telemetry) return undefined;
|
|
429
|
+
return (telemetry as (
|
|
430
|
+
context: RuntimeAgentPlanningContext<Env, Command, Change>,
|
|
431
|
+
) => AgentTelemetryBinding | undefined)(context);
|
|
432
|
+
};
|
|
433
|
+
|
|
418
434
|
const GeneratedRuntimeAgent = {
|
|
419
435
|
[definition.name]: class extends AgentToolRuntimeKernel<Env> {
|
|
420
436
|
private hasLoadedRuntime = false;
|
|
421
437
|
private loadedRuntimeKey?: string;
|
|
422
438
|
private loading?: Promise<void>;
|
|
439
|
+
private telemetryResolved = false;
|
|
440
|
+
private resolvedTelemetry?: AgentTelemetryBinding;
|
|
423
441
|
private temporaryLaunch?: TemporaryAgentLaunch<Config>;
|
|
424
442
|
private temporaryDispose?: () => Promise<void>;
|
|
425
443
|
private temporaryCleanup?: Promise<void>;
|
|
@@ -609,6 +627,11 @@ export function defineRuntimeAgent<
|
|
|
609
627
|
{ timeoutMs: RUNTIME_LOAD_TIMEOUT_MS },
|
|
610
628
|
));
|
|
611
629
|
let hooks = resolveDefinitionHooks(context);
|
|
630
|
+
if (!this.telemetryResolved) {
|
|
631
|
+
this.resolvedTelemetry = resolveDefinitionTelemetry(context);
|
|
632
|
+
this.telemetryResolved = true;
|
|
633
|
+
}
|
|
634
|
+
const telemetry = this.resolvedTelemetry;
|
|
612
635
|
if (this.role === "temporary") {
|
|
613
636
|
this.temporaryDispose = tools.dispose;
|
|
614
637
|
tools = this.applyTemporaryToolPolicy(tools);
|
|
@@ -620,6 +643,7 @@ export function defineRuntimeAgent<
|
|
|
620
643
|
tools,
|
|
621
644
|
resources: loaded.resources,
|
|
622
645
|
hooks,
|
|
646
|
+
...(telemetry ? { telemetry } : {}),
|
|
623
647
|
});
|
|
624
648
|
|
|
625
649
|
await this.initCandidate(candidate);
|