@akagilnc/pi-workflow-roles 0.1.3236 → 0.1.3249

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.
@@ -18,9 +18,11 @@ import type { CollectorGitHubTransport } from "./collector-github.ts";
18
18
  import {
19
19
  COLLECTOR_OBSERVE_TOOL,
20
20
  COLLECTOR_OUTPUT_TOOL,
21
+ COLLECTOR_READ_TOOL,
21
22
  COLLECTOR_REQUEST_TOOL,
22
23
  COLLECTOR_WAIT_TOOL,
23
24
  createCollectorLedger,
25
+ projectEvidenceEntryView,
24
26
  type CollectorLedger,
25
27
  } from "./collector-ledger.ts";
26
28
  import {
@@ -30,32 +32,55 @@ import {
30
32
  import {
31
33
  collectorObserveArgsSchema,
32
34
  collectorOutputArgsSchema,
35
+ collectorReadArgsSchema,
33
36
  collectorRequestArgsSchema,
34
37
  collectorWaitArgsSchema,
35
38
  } from "./collector-tool-schemas.ts";
36
39
  import { COLLECTOR_ACCEPTED_TEXT } from "./package-contracts/collector-output.ts";
40
+ import { CorrectableSubmissionError, isCorrectableExecuteError } from "./submission-correctable-error.ts";
41
+ import { CollectorUnknownEvidenceError } from "./collector-identity.ts";
42
+
43
+ export { CollectorUnknownEvidenceError } from "./collector-identity.ts";
44
+
45
+ /**
46
+ * #641 chain②: normal completion must never declare `infrastructureFailure`.
47
+ * When the runtime can machine-verify a lawful receipt assembly, a declaration
48
+ * is model misuse — bounce it as correctable guidance instead of host failure.
49
+ */
50
+ export class CollectorNormalCompletionDeclarationError extends CorrectableSubmissionError {
51
+ constructor() {
52
+ super("runtime 已按机器状态核验本局为正常完工(回执可合法组装):正常完工的交件不得填 infrastructureFailure,请省略该字段后重新提交。");
53
+ this.name = "CollectorNormalCompletionDeclarationError";
54
+ }
55
+ }
37
56
 
38
- export { COLLECTOR_FIXED_KICKOFF } from "./collector-config.ts";
57
+ export {
58
+ COLLECTOR_FIXED_KICKOFF,
59
+ } from "./collector-config.ts";
39
60
  export {
40
61
  COLLECTOR_OBSERVE_TOOL,
41
62
  COLLECTOR_OUTPUT_TOOL,
63
+ COLLECTOR_READ_TOOL,
42
64
  COLLECTOR_REQUEST_TOOL,
43
65
  COLLECTOR_WAIT_TOOL,
44
66
  };
45
67
 
46
68
  export const COLLECTOR_REQUIRED_TOOLS = [
47
69
  COLLECTOR_OBSERVE_TOOL,
70
+ COLLECTOR_READ_TOOL,
48
71
  COLLECTOR_REQUEST_TOOL,
49
72
  COLLECTOR_WAIT_TOOL,
50
73
  COLLECTOR_OUTPUT_TOOL,
51
74
  ] as const;
52
75
 
53
76
  const observeSchema = collectorObserveArgsSchema;
77
+ const readSchema = collectorReadArgsSchema;
54
78
  const requestSchema = collectorRequestArgsSchema;
55
79
  const waitSchema = collectorWaitArgsSchema;
56
80
  const outputSchema = collectorOutputArgsSchema;
57
81
 
58
82
  type RequestParams = Static<typeof requestSchema>;
83
+ type ReadParams = Static<typeof readSchema>;
59
84
  type WaitParams = Static<typeof waitSchema>;
60
85
  type OutputParams = Static<typeof outputSchema>;
61
86
 
@@ -260,7 +285,7 @@ export function createCollectorRoleRuntime(
260
285
  pi.registerTool({
261
286
  name: COLLECTOR_OBSERVE_TOOL,
262
287
  label: "通进司观察",
263
- description: "抓取配置目标的完整 GitHub PR 证据,存不可变快照入卷。",
288
+ description: "抓取配置目标的完整 GitHub PR 证据,存不可变快照入卷。正文在上下文中只给头部摘录加指针;需要头部之外的正文时,用 ak_collector_read 按 evidenceId 开卷;findings 的拆分与归类由你在交件时完成。",
264
289
  promptSnippet: "抓取配置目标 PR 证据",
265
290
  parameters: observeSchema,
266
291
  async execute(toolCallId: string, _params: unknown, signal: AbortSignal | undefined, _onUpdate: unknown, ctx: HostContext) {
@@ -269,7 +294,7 @@ export function createCollectorRoleRuntime(
269
294
  }
270
295
  try {
271
296
  activation.ledger.beginOperational(COLLECTOR_OBSERVE_TOOL, toolCallId);
272
- const { snapshot, modelView } = await activation.ledger.observe(
297
+ const { snapshot, contextView } = await activation.ledger.observe(
273
298
  activation.transport,
274
299
  activation.clock,
275
300
  signal,
@@ -282,12 +307,55 @@ export function createCollectorRoleRuntime(
282
307
  return {
283
308
  content: [{
284
309
  type: "text" as const,
285
- text: JSON.stringify(modelView),
310
+ // #641 chain①: model context carries bounded body heads + pointers only.
311
+ text: JSON.stringify(contextView),
312
+ }],
313
+ // #641 the bounded projection is the only provider-visible face on every host
314
+ // (Grok/ACP relays tool details as MCP structuredContent); full bodies stay
315
+ // in the ledger volume and enter context only by explicit ak_collector_read.
316
+ details: contextView,
317
+ };
318
+ } catch (error) {
319
+ hostActions.failInfrastructure(error, ctx, toolCallId);
320
+ }
321
+ },
322
+ });
323
+
324
+ pi.registerTool({
325
+ name: COLLECTOR_READ_TOOL,
326
+ label: "通进司开卷",
327
+ description: "按 evidenceId 开卷读取一条已观测材料的全量正文与指针;只在观察头部摘录不足以判读时调用。",
328
+ promptSnippet: "按指针开卷读材料",
329
+ parameters: readSchema,
330
+ async execute(toolCallId: string, params: ReadParams, _signal: AbortSignal | undefined, _onUpdate: unknown, ctx: HostContext) {
331
+ if (activation === undefined) {
332
+ throw new Error("通进司未激活");
333
+ }
334
+ try {
335
+ activation.ledger.beginOperational(COLLECTOR_READ_TOOL, toolCallId);
336
+ const record = activation.ledger.getEvidence(params.evidenceId);
337
+ if (
338
+ record === undefined ||
339
+ (record.kind !== "review" && record.kind !== "issue_comment" && record.kind !== "review_comment" && record.kind !== "reaction") ||
340
+ typeof record.body !== "string"
341
+ ) {
342
+ throw new CollectorUnknownEvidenceError(params.evidenceId);
343
+ }
344
+ const material = projectEvidenceEntryView(record);
345
+ activation.ledger.completeOperational(toolCallId);
346
+ return {
347
+ content: [{
348
+ type: "text" as const,
349
+ // #641 chain①: full bodies enter provider context only by explicit pointer.
350
+ text: JSON.stringify(material),
286
351
  }],
287
- details: modelView,
352
+ details: material,
288
353
  };
289
354
  } catch (error) {
290
- hostActions.failInfrastructure(error, ctx);
355
+ if (isCorrectableExecuteError(error)) throw error;
356
+ // typed toolCallId books the shared infrastructure fact so settlement can
357
+ // tell a read tool's real host failure from a correctable pointer bounce.
358
+ hostActions.failInfrastructure(error, ctx, toolCallId);
291
359
  }
292
360
  },
293
361
  });
@@ -319,7 +387,7 @@ export function createCollectorRoleRuntime(
319
387
  details,
320
388
  };
321
389
  } catch (error) {
322
- hostActions.failInfrastructure(error, ctx);
390
+ hostActions.failInfrastructure(error, ctx, toolCallId);
323
391
  }
324
392
  },
325
393
  });
@@ -350,7 +418,7 @@ export function createCollectorRoleRuntime(
350
418
  details,
351
419
  };
352
420
  } catch (error) {
353
- hostActions.failInfrastructure(error, ctx);
421
+ hostActions.failInfrastructure(error, ctx, toolCallId);
354
422
  }
355
423
  },
356
424
  });
@@ -358,8 +426,20 @@ export function createCollectorRoleRuntime(
358
426
  pi.registerTool({
359
427
  name: COLLECTOR_OUTPUT_TOOL,
360
428
  label: "通进司输出",
361
- description: "观察完成后提交;回执由 runtime 组装。",
429
+ description: "观察完成后提交;回执由 runtime 组装。正常完工提交空对象 {}(如需报 finding,填 findings 指针数组);仅在基础设施真实失败时才可填 infrastructureFailure,无失败时必须省略该字段。",
362
430
  promptSnippet: "提交通进司回执",
431
+ // #641 chain②: the seat owns the normal-completion decision. The probe
432
+ // runs the exact receipt assembly (validation only, no accept side
433
+ // effects); success ⇒ machine-verified normal completion ⇒ bounce.
434
+ bounceInfrastructureDeclaration(params) {
435
+ if (activation === undefined) return undefined;
436
+ try {
437
+ buildCollectorReceipt(activation.ledger, params, activation.clock);
438
+ } catch {
439
+ return undefined;
440
+ }
441
+ return new CollectorNormalCompletionDeclarationError();
442
+ },
363
443
  parameters: outputSchema,
364
444
  async execute(toolCallId: string, params: OutputParams, _signal: AbortSignal | undefined, _onUpdate: unknown, ctx: HostContext) {
365
445
  if (activation === undefined) {
@@ -7,16 +7,39 @@ export const collectorRequestArgsSchema = Type.Object({
7
7
  requestId: Type.String({ minLength: 1, description: "配置请求身份" }),
8
8
  snapshotId: Type.String({ minLength: 1, description: "最新留存观察快照" }),
9
9
  }, { additionalProperties: false });
10
+ export const collectorReadArgsSchema = Type.Object({
11
+ evidenceId: Type.String({ minLength: 1, description: "observe 返回的材料证据 id(evidenceId)" }),
12
+ }, { additionalProperties: false });
10
13
  export const collectorWaitArgsSchema = Type.Object({
11
14
  durationMs: Type.Integer({ minimum: 1, maximum: COLLECTOR_ELIGIBILITY_MS, description: "等待毫秒;单次上限五分钟且不超剩余资格" }),
12
15
  }, { additionalProperties: false });
13
- /** Runtime owns the observed groups; the model only signals sole-final submission. */
16
+
17
+ /**
18
+ * #641 chain①: the collector LLM does the splitting/classification (#245:
19
+ * 他自己就是llm为什么不能读不能拆). Findings are submitted as pointer refs into
20
+ * the stored evidence; the runtime enriches each with the machine pointer
21
+ * (repo/PR/comment id/url/author/kind/时间) and validates resolvability.
22
+ * Category is a short LLM classification label, never a body transcription.
23
+ */
24
+ export const collectorFindingArgsSchema = Type.Object({
25
+ evidenceId: Type.String({ minLength: 1, description: "observe 返回的材料证据 id(evidenceId)" }),
26
+ category: Type.Optional(Type.String({ minLength: 1, maxLength: 200, description: "该 finding 的简短归类标签;需要头部之外的正文时先用 ak_collector_read 开卷再判读,不得誊写评论正文" })),
27
+ }, { additionalProperties: false });
28
+
29
+ export const collectorOutputBaseSchema = Type.Object({
30
+ findings: Type.Optional(Type.Array(collectorFindingArgsSchema, {
31
+ description: "本次收集到的逐条 findings;零 finding 的模板通知不得进入。正常完工无 finding 时省略。",
32
+ })),
33
+ }, { additionalProperties: true, description: "正常完工提交空对象 {};仅在基础设施真实失败时才填写 infrastructureFailure,无失败时必须省略该字段。" });
34
+
35
+ /** Runtime owns the observed evidence; the model submits findings and signals sole-final submission. */
14
36
  export const collectorOutputArgsSchema = withInfrastructureFailureDeclaration(
15
- Type.Object({}, { additionalProperties: true }),
37
+ collectorOutputBaseSchema,
16
38
  );
17
39
  (collectorOutputArgsSchema as unknown as { required: string[] }).required = [];
18
40
 
19
41
  export type CollectorObserveArgs = Static<typeof collectorObserveArgsSchema>;
20
42
  export type CollectorRequestArgs = Static<typeof collectorRequestArgsSchema>;
43
+ export type CollectorReadArgs = Static<typeof collectorReadArgsSchema>;
21
44
  export type CollectorWaitArgs = Static<typeof collectorWaitArgsSchema>;
22
45
  export type CollectorOutputArgs = Static<typeof collectorOutputArgsSchema>;
@@ -1,4 +1,5 @@
1
1
  import { Type, type Static, type TLiteral, type TSchema } from "typebox";
2
+ import type { CorrectableSubmissionError } from "./submission-correctable-error.ts";
2
3
 
3
4
  type HostContentPart = { type: "text"; text: string } | { type: "toolCall"; id: string; name: string; arguments?: unknown } | { type: string };
4
5
  type HostMessage = { role: string; content?: unknown; toolName?: string; isError?: boolean; stopReason?: string };
@@ -209,7 +210,12 @@ type HostSessionManager = { getLeafEntry(): HostSessionEntry | undefined; getLea
209
210
  /** Context supplied by a host for one activation and its interceptable events. */
210
211
  export type HostContext = { cwd: string; mode: string; model: { readonly provider: string } | undefined; sessionManager: HostSessionManager; signal?: AbortSignal | undefined; ui?: { notify?(message: string, type?: "info" | "warning" | "error"): void }; transcript?(): string; abort(): void; };
211
212
 
212
- export type HostToolDefinition<S extends TSchema = TSchema, D = unknown, C = HostContext> = { name: string; label: string; description: string; promptSnippet?: string; parameters: S; execute( toolCallId: string, params: Static<S>, signal: AbortSignal | undefined, update: ((result: HostToolResult<D>) => void) | undefined, context: C, ): Promise<HostToolResult<D>>; };
213
+ export type HostToolDefinition<S extends TSchema = TSchema, D = unknown, C = HostContext> = { name: string; label: string; description: string; promptSnippet?: string; parameters: S; execute( toolCallId: string, params: Static<S>, signal: AbortSignal | undefined, update: ((result: HostToolResult<D>) => void) | undefined, context: C, ): Promise<HostToolResult<D>>; /**
214
+ * #641 chain② opt-in: when the output params carry the shared infrastructure
215
+ * declaration but the seat can machine-verify a lawful normal completion, the
216
+ * registration may bounce the submission as a correctable error instead of
217
+ * failing the host. Return the correctable error to bounce, or undefined to
218
+ * keep the shared host-failure path. */ bounceInfrastructureDeclaration?(params: unknown, toolCallId: string, context: C): CorrectableSubmissionError | undefined; };
213
219
 
214
220
  type BeforeAgentStartEvent = { prompt: string; systemPrompt: string; systemPromptOptions: { skills?: readonly unknown[]; contextFiles?: readonly unknown[]; appendSystemPrompt?: string } };
215
221
  type InputEvent = { text: string; images?: Array<{ type: "image"; data: string; mimeType: string }>; source?: string };
@@ -42,13 +42,21 @@ export type CollectorSnapshot = {
42
42
  normalizedByteLength: number;
43
43
  };
44
44
 
45
+ /**
46
+ * #641 chain① receipt evidence record: pointer/integrity facts only. Bodies and
47
+ * raw payloads stay in the volume (observe details + GitHub pointer) and are
48
+ * never transcribed into the receipt.
49
+ */
45
50
  export type CollectorEvidenceRecord = {
46
51
  evidenceId: string;
47
52
  kind: string;
48
53
  versionId: string;
49
54
  contentDigest: string;
50
55
  firstObservedAt: string;
51
- raw: unknown;
56
+ githubId?: number;
57
+ authorLogin?: string;
58
+ htmlUrl?: string;
59
+ authoritativeTime?: string | null;
52
60
  };
53
61
 
54
62
  export type CollectorReceipt = {
@@ -104,6 +112,6 @@ export function validateAcceptedCollectorReceipt(value: unknown): CollectorRecei
104
112
  snapshots: records(safeGet(value, "snapshots")).map((snapshot) => ({
105
113
  snapshotId: safeGet(snapshot, "snapshotId"), observedAt: safeGet(snapshot, "observedAt"), completedAt: safeGet(snapshot, "completedAt"), completedMono: safeGet(snapshot, "completedMono"), host: safeGet(snapshot, "host"), repository: safeGet(snapshot, "repository"), prNumber: safeGet(snapshot, "prNumber"), prState: safeGet(snapshot, "prState"), headOid: safeGet(snapshot, "headOid"), complete: safeGet(snapshot, "complete"), evidenceIds: strings(safeGet(snapshot, "evidenceIds")), pageDiagnostics: records(safeGet(snapshot, "pageDiagnostics")), normalizedByteLength: safeGet(snapshot, "normalizedByteLength"),
106
114
  } as CollectorSnapshot)),
107
- evidenceRecords: records(safeGet(value, "evidenceRecords")).map((record) => ({ evidenceId: safeGet(record, "evidenceId"), kind: safeGet(record, "kind"), versionId: safeGet(record, "versionId"), contentDigest: safeGet(record, "contentDigest"), firstObservedAt: safeGet(record, "firstObservedAt"), raw: safeGet(record, "raw") } as CollectorEvidenceRecord)),
115
+ evidenceRecords: records(safeGet(value, "evidenceRecords")).map((record) => ({ evidenceId: safeGet(record, "evidenceId"), kind: safeGet(record, "kind"), versionId: safeGet(record, "versionId"), contentDigest: safeGet(record, "contentDigest"), firstObservedAt: safeGet(record, "firstObservedAt"), githubId: safeGet(record, "githubId"), authorLogin: safeGet(record, "authorLogin"), htmlUrl: safeGet(record, "htmlUrl"), authoritativeTime: safeGet(record, "authoritativeTime") } as CollectorEvidenceRecord)),
108
116
  };
109
117
  }
@@ -12,6 +12,7 @@
12
12
  * Error so settlement keeps the original cause (kind=failure, exit 1).
13
13
  */
14
14
  import { Type, type TSchema } from "typebox";
15
+ import type { CorrectableSubmissionError } from "../submission-correctable-error.ts";
15
16
 
16
17
  export const INFRASTRUCTURE_FAILURE_DECLARATION_KEY =
17
18
  "infrastructureFailure" as const;
@@ -115,15 +116,26 @@ function infrastructureFailureError(diagnostic: string): Error {
115
116
  * The one early call for every terminating output `execute`: if the parameters
116
117
  * carry the infra declaration, hand the diagnostic error to the shared host
117
118
  * `failInfrastructure` seam (which aborts the run). No-op otherwise.
119
+ *
120
+ * #641 chain②: an optional seat-owned bounce hook may intercept the
121
+ * declaration first — when the seat can machine-verify a lawful normal
122
+ * completion, it returns a correctable error and the declaration is treated as
123
+ * model misuse (交件契约封驳) instead of a host failure. Returning undefined
124
+ * keeps the shared failure path.
118
125
  */
119
126
  export function failOnInfrastructureFailureDeclaration<C>(
120
127
  parameters: unknown,
121
128
  hostActions: TerminatingInfrastructureHostActions<C>,
122
129
  ctx: C,
123
130
  toolCallId: string,
131
+ bounceInfrastructureDeclaration?: (params: unknown, toolCallId: string, ctx: C) => CorrectableSubmissionError | undefined,
124
132
  ): void {
125
133
  const diagnostic = infrastructureFailureDiagnostic(parameters);
126
134
  if (diagnostic === undefined) return;
135
+ if (bounceInfrastructureDeclaration !== undefined) {
136
+ const bounce = bounceInfrastructureDeclaration(parameters, toolCallId, ctx);
137
+ if (bounce !== undefined) throw bounce;
138
+ }
127
139
  hostActions.failInfrastructure(
128
140
  infrastructureFailureError(diagnostic),
129
141
  ctx,
@@ -44,6 +44,7 @@ import {
44
44
  // interval binding on historical session bytes). Provider-stop retain authority is Sitian.
45
45
  import {
46
46
  COLLECTOR_OBSERVE_TOOL,
47
+ COLLECTOR_READ_TOOL,
47
48
  COLLECTOR_REQUEST_TOOL,
48
49
  COLLECTOR_WAIT_TOOL,
49
50
  } from "../collector-ledger.ts";
@@ -109,6 +110,7 @@ import {
109
110
  import {
110
111
  classifyPackagedRoleTerminalResult,
111
112
  findLatestDurablePackagedRoleTerminal,
113
+ hasNavigatorInfrastructureFailureBase,
112
114
  isAcceptedPackagedRoleTerminalResult,
113
115
  isReceiptSettlementBindingClear,
114
116
  NAVIGATOR_INVOCATION_ENTRY,
@@ -1604,6 +1606,7 @@ function boundErroredToolCandidate(
1604
1606
  /** Collector operational tools that fail closed via host infrastructure abort. */
1605
1607
  const COLLECTOR_INFRASTRUCTURE_TOOLS = new Set<string>([
1606
1608
  COLLECTOR_OBSERVE_TOOL,
1609
+ COLLECTOR_READ_TOOL,
1607
1610
  COLLECTOR_REQUEST_TOOL,
1608
1611
  COLLECTOR_WAIT_TOOL,
1609
1612
  ]);
@@ -1613,12 +1616,22 @@ type InfrastructureFailureSpec = Readonly<{
1613
1616
  matchTool: (toolName: string) => boolean;
1614
1617
  cause: ControlledFailureCause;
1615
1618
  identityName: string;
1619
+ /**
1620
+ * Errored results only count as infrastructure when the durable details carry
1621
+ * the typed navigator fact. ak_collector_read also rejects known correctable
1622
+ * misuses (CollectorUnknownEvidenceError pointer bounces) as errored tool
1623
+ * results — those must not surface as CollectorInfrastructureError.
1624
+ */
1625
+ requireInfrastructureFact?: (toolName: string) => boolean;
1616
1626
  }>;
1617
1627
 
1618
1628
  const COLLECTOR_INFRASTRUCTURE_FAILURE_SPEC: InfrastructureFailureSpec = {
1619
1629
  matchTool: (toolName) => COLLECTOR_INFRASTRUCTURE_TOOLS.has(toolName),
1620
1630
  cause: "activation",
1621
1631
  identityName: "CollectorInfrastructureError",
1632
+ // read alone has a correctable rejection mode (unknown/non-openable pointers);
1633
+ // only its typed infrastructure-failure fact counts as a real host failure.
1634
+ requireInfrastructureFact: (toolName) => toolName === COLLECTOR_READ_TOOL,
1622
1635
  };
1623
1636
 
1624
1637
  const ENGINE_DETOUR_INFRASTRUCTURE_FAILURE_SPEC: InfrastructureFailureSpec = {
@@ -1648,6 +1661,9 @@ function extractInfrastructureToolFailure(
1648
1661
  ) {
1649
1662
  continue;
1650
1663
  }
1664
+ if (spec.requireInfrastructureFact?.(message.toolName) === true) {
1665
+ if (!hasNavigatorInfrastructureFailureBase(message.details)) continue;
1666
+ }
1651
1667
  const diagnostic = toolResultText(message);
1652
1668
  if (diagnostic.length === 0) continue;
1653
1669
  return {
@@ -45,6 +45,7 @@ import type { CollectorGitHubTransport } from "./collector-github.ts";
45
45
  import {
46
46
  COLLECTOR_OBSERVE_TOOL,
47
47
  COLLECTOR_OUTPUT_TOOL,
48
+ COLLECTOR_READ_TOOL,
48
49
  COLLECTOR_REQUEST_TOOL,
49
50
  COLLECTOR_WAIT_TOOL,
50
51
  createCollectorRoleRuntime,
@@ -388,6 +389,7 @@ export type { ComplianceDecision } from "./compliance-transport.ts";
388
389
  export {
389
390
  COLLECTOR_OBSERVE_TOOL,
390
391
  COLLECTOR_OUTPUT_TOOL,
392
+ COLLECTOR_READ_TOOL,
391
393
  COLLECTOR_REQUEST_TOOL,
392
394
  COLLECTOR_WAIT_TOOL,
393
395
  } from "./collector-role.ts";
@@ -283,19 +283,25 @@ export function createSubmissionLedgerHost(
283
283
  if (state.sealed) throw new Error("提交账已封账");
284
284
  // #541 / #575: shared infra-declaration fail lives on the ledger seam,
285
285
  // before any role execute or sole-final work (one owner for every seat).
286
- failOnInfrastructureFailureDeclaration(
287
- params,
288
- {
289
- failInfrastructure(error, ctx) {
290
- failInfrastructure(error, ctx);
291
- },
292
- },
293
- context,
294
- toolCallId,
295
- );
286
+ // #641 chain②: seats may opt into bouncing a machine-verified normal
287
+ // completion misdeclared as infrastructure failure — the bounce throws a
288
+ // correctable error from the declaration call, so it lands in the same
289
+ // correctable-rejection path as execute-thrown rejections below. The
290
+ // candidate is booked first so the bounced attempt stays observable.
296
291
  append({ type: "candidate", attemptId, toolCallId, toolName: tool.name, sequence: ++state.sequence });
297
292
  let result: HostToolResult<unknown>;
298
293
  try {
294
+ failOnInfrastructureFailureDeclaration(
295
+ params,
296
+ {
297
+ failInfrastructure(error, ctx) {
298
+ failInfrastructure(error, ctx);
299
+ },
300
+ },
301
+ context,
302
+ toolCallId,
303
+ tool.bounceInfrastructureDeclaration,
304
+ );
299
305
  result = await tool.execute(toolCallId, params, signal, update, context);
300
306
  } catch (error) {
301
307
  if (isCorrectableExecuteError(error)) {