Skip to content

refactor(runtime): name the tool-call data boundary and share the call-field recipe - #4937

Open
Adarsh-Me wants to merge 2 commits into
apache:mainfrom
Adarsh-Me:refactor/tool-call-snapshot
Open

refactor(runtime): name the tool-call data boundary and share the call-field recipe#4937
Adarsh-Me wants to merge 2 commits into
apache:mainfrom
Adarsh-Me:refactor/tool-call-snapshot

Conversation

@Adarsh-Me

Copy link
Copy Markdown
Contributor

Summary

Implements #4908 — the call-data slice of the A06 direction in #4726. Behavior-preserving extraction; no public API, event, message, or durable-format changes.

executeTool() spelled out the argument-projection policy inline: raw snapshot, permission projection on a private clone, the Computer Use persisted/model views, and two handwritten common-field recipes for the tool_start event and the persisted tool_call message — all inside a method that also owns admission, dispatch identity, T1/T2 and publication.

The call-data rules now live in packages/runtime/src/tool-call-snapshot.ts:

  • snapshotToolArgs / snapshotJsonValue — the recursive, frozen, cycle-rejecting snapshot, moved verbatim.
  • validateDeclaredToolArgs — schema validation, moved verbatim.
  • buildToolCallArgs() — one named operation producing the four argument views (executionArgs, permissionArgs, persistedArgs, modelFacingArgs) with the existing guards: direct-only rejection skips validation and projection entirely; an unavailable sandbox-boundary surface defers validation while every other rule still applies; validation/projection failures are returned as permissionArgsError for the existing refusal path instead of thrown. The Computer Use history-regression rationale ([A06] Extract an immutable tool-call snapshot and centralize argument projections #4908's window_id/windowId record) moved into this function's documentation.
  • buildToolCallCommonFields() — the one shared recipe for the fields both call records assemble. It is invoked once per record: the event and the message each receive privately owned clones of args and providerOptions, preserving the isolation the duplicated recipes guaranteed.

The 43 downstream uses of the views (loop gates, managed mutation admission, durable preparation, result projection, sandbox-denial keys, telemetry/artifacts) read the same named values as before; their algorithms are untouched. Synchronous-before-first-await registration is preserved: only the projection point moved into the helper, and admission ordering around it is unchanged.

Measurements (per #4908's required table)

Measurement Before After
Common call-field recipes 2 handwritten 1 shared recipe (buildToolCallCommonFields), invoked once per record
Argument-view construction 1 inline region (L1121–L1194) 1 named operation (buildToolCallArgs); no duplicated projection
tool-runtime.ts size 4,192 physical lines 4,077 (−115; extraction, not deletion)
executeTool() size 1,096 physical lines smaller by the moved projection/recipe regions; remaining body is admission/T1/T2/publication flow
New internal module(s) 0 tool-call-snapshot.ts, 307 lines (incl. license header, moved comments, types)
Net physical delta, production code +192 lines (350 added / 158 removed across both files)
Tests existing coverage retained no test modifications; counts below

The net addition is documentation and named types: the moved rules kept their explanatory comments, which the issue required preserving, and the module adds the interface declarations that make the four views explicit. No behavior changed, so no new tests were required; the existing isolation suite covers the boundary.

Verification

  • The four named suites, run against the compiled output: tool-runtime-argument-ownership (1/1), tool-runtime-settlement (12/12), computer-use-model-loop (3/3), computer-use-privacy-boundary (5/5) — 21/21 pass. All test files unmodified.
  • Baseline differential: the same four suites also pass 21/21 against the pre-refactor module (rebuilt from a stashed tree), confirming the extraction changed no outcomes.
  • One real defect was caught by that suite during development: wiring both records to a single shared common-fields object leaked one mutable args across event and storage, and tool-runtime-argument-ownership failed immediately. The per-record invocation fixes it; that failure mode is exactly what the duplicated recipes used to prevent, and the shared recipe now documents it.
  • @maka/runtime builds clean (tsc -p tsconfig.json + filesystem worker, 0 errors); biome format/lint clean on both files.
  • Not run locally: the full desktop/e2e gates (no Linux/Xvfb here); CI covers them.

AI use

Select exactly one:

  • No generative tool made a substantive contribution
  • Generative tooling made a substantive contribution

Tool(s) and scope:

Checklist

  • Tests cover the change and fail without it — the argument-ownership suite fails if the two records ever share one args object (verified during development); all pre-existing suites pass unmodified.
  • Lint, format, typecheck and the affected suites pass locally — see Verification.

Does this PR entail a change in behavior?

  • No

…l-field recipe

executeTool() spelled out the argument-projection policy inline: raw
snapshot, permission projection on a private clone, the Computer Use
persisted/model views, and two handwritten common-field recipes for the
tool_start event and the persisted tool_call message — all inside a
method that also owns admission, dispatch identity, T1/T2 and
publication (apache#4908, slice of the A06 direction in apache#4726).

The call-data rules move to tool-call-snapshot.ts: the recursive
snapshot helpers, schema validation, one named buildToolCallArgs()
operation that produces the four argument views with the existing guards
(direct-only rejection skips validation and projection entirely; an
unavailable sandbox-boundary surface defers validation; projection
failures come back as permissionArgsError for the refusal path instead
of throwing), and one buildToolCallCommonFields() recipe invoked once per
record so the event and the message each receive privately owned clones
of args and providerOptions. The consolidated recipes were verified to
fail the argument-ownership isolation test when a single shared args
object leaked across the two records — the exact break the duplication
used to make impossible.

No public API, event, message or durable format changes. The four
argument-view consumers (managed mutation admission, durable
preparation, result projection, telemetry/artifacts) read the same named
views; their algorithms are untouched.
@github-actions github-actions Bot added the effort/L Under 1000 readable lines label Sep 6, 2026
…message shape

The shared common-field recipe emitted toolUseId as its own key. The
tool_start event carries that field, but the persisted tool_call message
names the same value `id` and its stored-message schema rejects unknown
keys — appendMessages threw 'Invalid stored message schema', the
Computer Use history guard generalized it to 'Operation failed', and the
remote TUI turn failed before its fixture tool could run
(tui-mcp-remote-integration).

The recipe no longer emits the call id: the input still carries it, and
each record names it at its own site — `toolUseId` on the event, `id` on
the message. Reproduced locally through the failing integration test
(failed on the refactored build, passed on the pre-refactor module);
passes again after the fix along with the four argument-ownership,
settlement, model-loop and privacy suites (21/21).
@Adarsh-Me

Copy link
Copy Markdown
Contributor Author

The CI failure on f312477 was real and mine — root-caused and fixed on c840671.

The shared common-field recipe emitted toolUseId as its own key. The tool_start event carries that field, but the persisted tool_call message names the same value id, and storage validates every appended message against an exact-shape schema that rejects unknown keys (hasExactShape in @maka/core/session). appendMessages therefore threw Invalid stored message schema on the first tool call, the turn-level error path generalized it to Operation failed (the redaction fallback), and the turn died before reaching the fixture tool. Found by temporarily un-generalizing the model-failure path and rerunning the integration test locally: the raw stack pointed straight at the append-time decode.

The fix keeps the shared recipe but stops it from emitting the call id: the recipe's input still takes toolUseId (both records need it), and each record names it at its own site — toolUseId on the event, id on the message — exactly as the two handwritten recipes did. Documented in the interface so the next consolidation cannot reintroduce it.

Re-verified locally: the previously failing tui-mcp-remote-integration test passes, and the four suites named in the issue are green (argument-ownership 1, settlement 12, model-loop 3, privacy-boundary 5 — 21/21). Line counts move by two: tool-runtime.ts 4,078, tool-call-snapshot.ts 310.

@hqhq1025 hqhq1025 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

复核 c84067161d553509646afbc885571fd0bdf97ce4,未发现有充分证据支持的 P0–P3 问题。技术检查通过;是否接受这次重构的代码量与边界取舍,仍需维护者决定。

实际改动是把参数快照、声明式校验和权限/模型参数投影提取到 tool-call-snapshot.ts,并把事件与存储消息的公共字段构造从两处合并为一处。原始参数仍在首次等待前冻结并完成步骤准入登记(tool-runtime.ts:1123);直接调用限制、普通步骤拒绝和不可用沙箱界面的校验例外保持在原有控制路径(tool-runtime.ts:1133tool-call-snapshot.ts:212)。Computer Use 的模型投影仍调用原来的转换函数(tool-call-snapshot.ts:230)。

公共字段构造器每次独立克隆 argsproviderOptionstool-call-snapshot.ts:291),事件和消息分别调用它(tool-runtime.ts:1235:1256)。事件独有的 toolUseId 没有混入持久化消息,后者继续使用 id,符合 packages/core/src/session.ts:1191:1489 的严格解码形状。数据库 schema 未改变,不涉及 migration。

实测规模为 tool-runtime.ts 4192→4078 行,executeTool 1096→1049 行,新文件 310 行,两文件合计净增 196 行。可验证的收益是消除一份重复字段构造,并提取已有数据边界;这不是净删代码,也没有证明性能收益。

验证通过:干净安装与 build:test、Runtime typecheck、两文件 lint/format、diff 检查,以及 123 项相关回归。回归包含真实 SQLite 提交/读取、持久化拒绝路径、参数校验、沙箱边界和 Computer Use 模型/隐私路径。另外通过生产 settleToolCall 入口,用可控 Promise 暂停验证了校验期间调用者修改原参数、执行期间修改投影,以及消息/事件互改参数和提供方元数据后的隔离。

边界说明:displayName: '' 现在会被保留,原代码会省略;持久化解码允许空串,已检查的正常生产工具生成器不会产生该值,主 UI 仍回退到工具名,因此未将其定为可达产品缺陷。此次没有运行原生 Computer Use 桌面操作或全仓测试;不能把上述结果扩展为任意外部自定义工具的全面兼容证明。对当前 main 492ff80f0f4abf3a917ce8f2c41b32fa9e3fc60c 的 Git 合并树检查无冲突,但未在合并树上运行测试。提交审查前该 head 的 hosted test 为 SUCCESS;没有可沿用的既有 review。

Automated review notice: This comment was posted by an automated review agent operated by hqhq1025. It is not an independent human review and does not replace one.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

effort/L Under 1000 readable lines

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants