Skip to content

ReferenceTurnRunner omits tool_result on the permission-denied path, diverging from replay_vectors.json #449

Description

Summary

ReferenceTurnRunner.resolveAndExecuteTool's permission-denied branch returns a HostToolResult without first recording a tool_result journal event, so the emitted journal diverges from spec/vectors/harness/replay_vectors.json.

This is the single remaining Go failure and one of the remaining TypeScript failures.

The vector requires the event

spec/vectors/harness/replay_vectors.json, scenario permission_denied:

json "expected": [ "session:session_start:session-1:turn-1", "turn:turn_start:0", "turn:llm_start:0", "turn:llm_complete:0", "session:checkpoint_created:session-1:turn-1", "turn:permission_requested:0:exec-1-permission", "turn:permission_completed:0:false", "turn:tool_result:0:add:false:permission_denied", "turn:messages_updated:0", ... ]

turn:tool_result:0:add:false:permission_denied must follow turn:permission_completed:0:false.

Compare the tool_success scenario in the same file, which records turn:tool_execution_startturn:tool_execution_completeturn:tool_result. The denied path is expected to emit tool_result without the two execution events, since nothing executed.

The code

runtime/go/prompty/model/turn_runner.go, in resolveAndExecuteTool:

go if err := r.recordTurn(TurnEventTypePermissionCompleted, turnId, iteration, decision.Save(NewSaveContext())); err != nil { return HostToolResult{}, err } if !decision.Approved { errorKind := "permission_denied" message := "Permission denied" if decision.Reason != nil { message = *decision.Reason } result := interface{}(map[string]interface{}{"message": message}) return HostToolResult{ // <-- returns without recording TurnEventTypeToolResult RequestId: toolRequest.RequestId, ToolCallId: toolRequest.ToolCallId, ToolName: toolRequest.ToolName, Success: false, ErrorKind: &errorKind, Result: &result, }, nil } if err := r.recordTurn(TurnEventTypeToolExecutionStart, turnId, iteration, toolRequest.Save(NewSaveContext())); err != nil {

The approved path continues on and records TurnEventTypeToolResult; the denied path returns early and skips it.

Observed

--- FAIL: TestReferenceTurnRunnerMatchesSharedGoldenReplayVectors/permission_denied expected: turn:tool_result:0:add:false:permission_denied

and, in TypeScript, tests/harness/turn-runner.test.ts > ReferenceTurnRunner > matches shared golden replay journal vectors.

This is handwritten code, not emitter output

Worth stating explicitly, because it was initially unclear whether this belonged to sethjuarez/typra:

  • turn_runner.go has no // <auto-generated by typra-emitter> banner; it begins directly with package prompty.
  • Of 299 .go files in runtime/go/prompty/model, 287 carry the generator banner and 12 do not. turn_runner.go is one of the 12.
  • A full npm run generate leaves it untouched — git status --porcelain reports nothing for it afterwards.

runtime/typescript/packages/core/src/harness/turn-runner.ts is likewise handwritten.

Suggested fix

Record TurnEventTypeToolResult on the denied branch before returning, in both runtimes, then re-run against the shared vectors.

Not verified

I have not checked the Rust, Python, C#, Swift, or Java reference runners for the same omission.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions