Goal and relationship to A06
This is one small, behavior-preserving slice of A06 in #4726: extract the existing tool-call argument snapshot/projection rules and consolidate the common call fields used by the start event and persisted call message. No new tool, execution mode, or product capability is proposed.
The broader direction is to give call data, admission, execution, result projection, and durability explicit ownership. This issue implements only the call-data boundary; it does not implement that entire architecture.
Code size and exact scope of this slice
Baseline: commit c963073cb729484395748366facbb9efc51fb86c, checked on 2026-09-06. Counts below are physical source lines, including comments and blank lines. The parent issue lists an earlier A06 total of 6,881 lines; this checkout has 7,063.
| Level |
Existing code |
Size |
Relationship to this issue |
| Entire A06 |
tool-runtime.ts + computer-use-tools.ts |
4,192 + 2,871 = 7,063 lines |
Background only |
| Containing method |
tool-runtime.ts, executeTool(), L1105-L2200 |
1,096 lines |
Contains the current call-data logic and downstream consumers; this issue does not rewrite the whole method |
| Argument preparation region |
tool-runtime.ts, L1121-L1194 |
74 lines |
Raw snapshot, validation/permission projection, persisted/model-facing views; guards and ordering in this region must remain intact |
| Snapshot helpers |
tool-runtime.ts, L4172-L4192 |
21 lines |
snapshotToolArgs() and snapshotJsonValue(); reuse/extract their existing behavior |
| Two call-record constructions |
tool-runtime.ts, L1252-L1266 and L1284-L1300 |
15 + 17 = 32 lines |
Consolidate shared call fields while preserving event/message-specific fields and separate mutable output copies |
The core review area is therefore 127 existing physical lines across four regions, plus downstream wiring changes. Excluding blank lines and full-line // comments, those regions contain 88 lines; this is a simple count, not an AST-based complexity measurement. The 127 lines are neither a promised deletion count nor the final diff size: they include comments, necessary guards, and record-specific code that must remain.
Downstream wiring includes managed mutation admission (L1577-L1582), durable preparation (L1608-L1619 and its input type at L2202-L2212), tool invocation/result projection (L1747-L1770), and existing telemetry/artifact consumers. These consumers should read the named call data without changing their algorithms. computer-use-tools.ts is context and regression coverage for this slice, not a file to split here. The existing core Computer Use projection helper is reused.
All line references above are pinned to the measured tool-runtime.ts revision.
What is the current problem?
1. The safety rules already exist, but their ownership is embedded in the execution method
The current code already snapshots and freezes input. It also already computes persistedArgs once and assigns modelFacingArgs = persistedArgs. This issue does not claim that cloning is missing or that the four views are independently recalculated everywhere.
The current relationship, condensed from L1121-L1194, is:
const rawExecutionArgs = snapshotToolArgs(args);
// Synchronous direct-only / step admission registration.
const executionArgs = rawExecutionArgs;
let permissionArgs = executionArgs;
// Unless direct-only rejection applies:
// validate declared args, then run permissionArgs on a private clone.
const persistedArgs = tool.categoryHint === 'computer_use'
? snapshotToolArgs(computerUseModelCallArgs(permissionArgs))
: permissionArgs;
const modelFacingArgs = persistedArgs;
These values have different jobs:
| Value |
Existing purpose |
| executionArgs |
Canonical execution input; a private mutable clone is passed to the implementation or managed transform |
| permissionArgs |
The tool's permission-oriented projection; it is also used by existing downstream policy/signature logic |
| persistedArgs |
Arguments written into the call message, start event and durable call data under the current rules |
| modelFacingArgs |
The call arguments replayed to the model; currently the same value as persistedArgs |
A maintainer changing this policy must currently work inside a method that also owns guards, client preparation, managed mutations, subagent limits, T1, invocation, T2, publication and state updates. There is no small named internal contract that records which view each consumer should use. The refactor should make that existing contract explicit and separately reviewable, without adding another interpretation of the arguments.
2. Common call metadata is assembled twice
callEventFacts and callMsg both assemble turnId, timestamp, toolName, activity identity, optional activityKind/displayName, cloned persisted args, optional providerOptions, and stepId. Their surrounding shapes differ, but the policy for those common fields is repeated.
For example, both currently contain:
args: structuredClone(persistedArgs),
...(ctx.providerOptions !== undefined
? { providerOptions: structuredClone(ctx.providerOptions) }
: {}),
...(stepId !== undefined ? { stepId } : {}),
This is a concrete consolidation target: two handwritten common-field recipes should become one shared internal recipe used to create two separate outputs. A future change to common metadata should not require editing both recipes. The outputs must remain independently owned; sharing one mutable args object would break the existing isolation guarantee.
3. Argument views are observably different, so the boundary matters
The source comment at L1159-L1193 documents a previous Computer Use history problem: using the host approval summary in model history renamed window_id to windowId, added approval-only fields, and omitted execution fields such as the key text. The model then repeated an invalid call shape.
The current implementation already addresses that problem with computerUseModelCallArgs and a shared persisted/model-facing projection. This is historical motivation and a regression invariant, not a claim of a currently reproduced bug to fix in this cleanup. The refactor must preserve both the accepted argument names and the existing privacy reduction of screen-derived/user-typed values.
Proposed implementation
Keep the public ToolRuntime API unchanged. Introduce a small internal call-data module under packages/runtime/src (working name: tool-call-snapshot.ts), with the existing snapshot helpers, a named argument-view construction operation, and a shared common-call-field constructor. Avoid a generic execution framework or a family of new service classes in this slice.
An illustrative internal shape is:
interface ToolCallSnapshot {
readonly sessionId: string;
readonly runId?: string;
readonly invocationId?: string;
readonly turnId: string;
readonly stepId?: string;
readonly toolCallId: string;
readonly toolName: string;
readonly origin: 'provider' | 'code_mode';
readonly parentToolCallId?: string;
readonly parentOperationId?: string;
readonly executionArgs: unknown;
readonly permissionArgs: unknown;
readonly persistedArgs: unknown;
readonly modelFacingArgs: unknown;
}
This is an interface sketch, not a requirement to replace existing repository types or invent new identities. readonly alone does not provide deep runtime immutability: preserve the current recursive snapshot behavior and private clones at owner boundaries.
The intended flow is:
executeTool()
-> snapshot raw input synchronously
-> perform existing synchronous direct-only / step registration
-> derive argument views at the existing validation/projection point
-> construct the internal call snapshot
-> construct start-event and call-message common fields through one recipe
-> continue existing admission / preparation / T1 / execution / T2 flow
using the appropriate named view and existing private output copies
The snapshot may be constructed in stages. Do not hide all preparation behind a new initial await: synchronous registration must still happen before the first await. Preserve these existing distinctions explicitly:
- Direct-only nested rejection skips permission projection and implementation.
- Ordinary step-admission rejection still performs the current permission projection.
- An unavailable sandbox-boundary surface retains its current validation exception.
- Validation/projection failures still enter the current refusal path with the same result/message/event behavior.
- Event dispatch identity is selected at the actual dispatch/preflight path, as today. The snapshot must not predict T1 admission or assign an operationId to a refusal.
The shared common-field constructor should allocate private args/providerOptions copies for each output. Event type/id/toolUseId/operationId and message type/id remain in their existing adapters. Publication timing and once-only flags remain in executeTool().
Expected reduction and how it will be measured
This slice is small. The 95 lines of argument-preparation region plus snapshot helpers contain existing behavior and extensive explanatory comments; they cannot all be deleted. A new module and type declarations also cost lines. There is no verified implementation diff yet, so claiming a specific net line reduction now would be unsupported.
The concrete target is to remove the second common-call-field recipe, extract the existing call-data responsibility from the 4,192-line module, and reduce the amount of projection/metadata policy that executeTool() spells out. Projection construction is already centralized locally; its construction count should remain one, with explicit ownership after extraction.
The PR must report these separately:
| Measurement |
Before |
Required after/report |
| Common call-field recipes |
2 |
1 shared recipe; delete the duplicate recipe |
| Argument-view construction |
1 inline region |
1 named internal operation; no duplicated projection work |
| tool-runtime.ts size |
4,192 physical lines |
Actual after count |
| executeTool() size |
1,096 physical lines |
Actual after count |
| New internal module(s) |
0 for this extraction |
Actual added lines |
| Total affected production code |
Baseline files at the pinned commit |
Net physical and non-comment line delta, including new files |
| Tests |
Existing behavior coverage |
Retained; test additions counted separately |
A reduction in tool-runtime.ts caused by relocating code must be reported as extraction, not as net deletion. The goal is a smaller implementation with one owner for each rule. If the new wrappers/types outweigh the consolidation, simplify the design before merging; naming a new snapshot object by itself is not sufficient cleanup evidence. This follows #4726's requirement for a measured result rather than a qualitative claim of simplification.
Scope and validation
- Extract/reuse existing cloning and projection behavior; consolidate the two common-field recipes and wire the existing consumers to the named data.
- Keep public types, provider/tool behavior, event/message formats, telemetry and artifacts compatible.
- Keep T1/T2, managed mutation admission, permission/sandbox decisions, loop gates, deferred tools, child agents and interactions unchanged. No Computer Use session/frame/dispatch redesign.
- Preserve tool-runtime-argument-ownership.test.ts, tool-runtime-settlement.test.ts, computer-use-model-loop.test.ts and computer-use-privacy-boundary.test.ts coverage. Include relevant durable-boundary tests for the touched wiring.
- Verify mutation isolation across caller input, permission projection, implementation, event, storage and artifact consumers. Add only missing regression coverage, including mutation while execution is suspended if not already covered.
- Verify direct-only rejection, ordinary admission failure and invalid-argument behavior through the real settlement entry point; these paths must preserve the current projection order and call count.
- Run the repository's applicable typecheck, lint, format and Runtime test gates. Publish actual commands/results with the implementation PR; this issue does not assert that tests have been run for code that has not been implemented.
Later slices may address admission/result ownership, durability/interactions, and Computer Use session/frame responsibilities after separate review. Those are context only and are not acceptance requirements for this issue.
Goal and relationship to A06
This is one small, behavior-preserving slice of A06 in #4726: extract the existing tool-call argument snapshot/projection rules and consolidate the common call fields used by the start event and persisted call message. No new tool, execution mode, or product capability is proposed.
The broader direction is to give call data, admission, execution, result projection, and durability explicit ownership. This issue implements only the call-data boundary; it does not implement that entire architecture.
Code size and exact scope of this slice
Baseline: commit c963073cb729484395748366facbb9efc51fb86c, checked on 2026-09-06. Counts below are physical source lines, including comments and blank lines. The parent issue lists an earlier A06 total of 6,881 lines; this checkout has 7,063.
The core review area is therefore 127 existing physical lines across four regions, plus downstream wiring changes. Excluding blank lines and full-line // comments, those regions contain 88 lines; this is a simple count, not an AST-based complexity measurement. The 127 lines are neither a promised deletion count nor the final diff size: they include comments, necessary guards, and record-specific code that must remain.
Downstream wiring includes managed mutation admission (L1577-L1582), durable preparation (L1608-L1619 and its input type at L2202-L2212), tool invocation/result projection (L1747-L1770), and existing telemetry/artifact consumers. These consumers should read the named call data without changing their algorithms. computer-use-tools.ts is context and regression coverage for this slice, not a file to split here. The existing core Computer Use projection helper is reused.
All line references above are pinned to the measured tool-runtime.ts revision.
What is the current problem?
1. The safety rules already exist, but their ownership is embedded in the execution method
The current code already snapshots and freezes input. It also already computes persistedArgs once and assigns modelFacingArgs = persistedArgs. This issue does not claim that cloning is missing or that the four views are independently recalculated everywhere.
The current relationship, condensed from L1121-L1194, is:
These values have different jobs:
A maintainer changing this policy must currently work inside a method that also owns guards, client preparation, managed mutations, subagent limits, T1, invocation, T2, publication and state updates. There is no small named internal contract that records which view each consumer should use. The refactor should make that existing contract explicit and separately reviewable, without adding another interpretation of the arguments.
2. Common call metadata is assembled twice
callEventFacts and callMsg both assemble turnId, timestamp, toolName, activity identity, optional activityKind/displayName, cloned persisted args, optional providerOptions, and stepId. Their surrounding shapes differ, but the policy for those common fields is repeated.
For example, both currently contain:
This is a concrete consolidation target: two handwritten common-field recipes should become one shared internal recipe used to create two separate outputs. A future change to common metadata should not require editing both recipes. The outputs must remain independently owned; sharing one mutable args object would break the existing isolation guarantee.
3. Argument views are observably different, so the boundary matters
The source comment at L1159-L1193 documents a previous Computer Use history problem: using the host approval summary in model history renamed window_id to windowId, added approval-only fields, and omitted execution fields such as the key text. The model then repeated an invalid call shape.
The current implementation already addresses that problem with computerUseModelCallArgs and a shared persisted/model-facing projection. This is historical motivation and a regression invariant, not a claim of a currently reproduced bug to fix in this cleanup. The refactor must preserve both the accepted argument names and the existing privacy reduction of screen-derived/user-typed values.
Proposed implementation
Keep the public ToolRuntime API unchanged. Introduce a small internal call-data module under packages/runtime/src (working name: tool-call-snapshot.ts), with the existing snapshot helpers, a named argument-view construction operation, and a shared common-call-field constructor. Avoid a generic execution framework or a family of new service classes in this slice.
An illustrative internal shape is:
This is an interface sketch, not a requirement to replace existing repository types or invent new identities. readonly alone does not provide deep runtime immutability: preserve the current recursive snapshot behavior and private clones at owner boundaries.
The intended flow is:
The snapshot may be constructed in stages. Do not hide all preparation behind a new initial await: synchronous registration must still happen before the first await. Preserve these existing distinctions explicitly:
The shared common-field constructor should allocate private args/providerOptions copies for each output. Event type/id/toolUseId/operationId and message type/id remain in their existing adapters. Publication timing and once-only flags remain in executeTool().
Expected reduction and how it will be measured
This slice is small. The 95 lines of argument-preparation region plus snapshot helpers contain existing behavior and extensive explanatory comments; they cannot all be deleted. A new module and type declarations also cost lines. There is no verified implementation diff yet, so claiming a specific net line reduction now would be unsupported.
The concrete target is to remove the second common-call-field recipe, extract the existing call-data responsibility from the 4,192-line module, and reduce the amount of projection/metadata policy that executeTool() spells out. Projection construction is already centralized locally; its construction count should remain one, with explicit ownership after extraction.
The PR must report these separately:
A reduction in tool-runtime.ts caused by relocating code must be reported as extraction, not as net deletion. The goal is a smaller implementation with one owner for each rule. If the new wrappers/types outweigh the consolidation, simplify the design before merging; naming a new snapshot object by itself is not sufficient cleanup evidence. This follows #4726's requirement for a measured result rather than a qualitative claim of simplification.
Scope and validation
Later slices may address admission/result ownership, durability/interactions, and Computer Use session/frame responsibilities after separate review. Those are context only and are not acceptance requirements for this issue.