Skip to content

feat: Add object operation to construct maps with evaluated values #253

@andreasronge

Description

@andreasronge

Summary

Add an object operation to PTC-JSON that constructs maps where values can be evaluated expressions. This enables the memory contract (returning maps to persist data) which is currently impossible to express correctly.

Context

Architecture reference: Memory contract is documented in lib/ptc_runner/schema.ex to_prompt/0 function (lines 478-491)
Specification: docs/ptc-json-specification.md - will need to document the new object operation
Dependencies: None
Related issues: None

Current State

The PTC-JSON DSL cannot construct a map with computed values. The merge operation requires each element to be a complete operation with an "op" field:

{"op":"merge","objects":[
  {"op":"literal","value":{"key":null}},
  {"key":{"op":"var","name":"x"}}
]}

Verified: Running the above pattern returns {:error, {:validation_error, "Missing required field 'op'"}}.

The system prompt's memory example (schema.ex:488) teaches this invalid pattern, causing LLMs to fail on memory-related tests (observed in demo test runner with deepseek model: tests 13 & 14 failed with "Missing required field 'op'" errors).

Current workarounds and why they fail:

  • literal wraps values as data, not evaluated expressions
  • merge requires all objects to have "op" field (validated via {:list, :expr} type at validator.ex:251-255)
  • No operation exists to build {"key": <evaluated-expr>}

Acceptance Criteria

  • New object operation defined in schema with fields parameter (type :map)
  • Validator validates object operation, recursively validating values that have "op" field
  • Interpreter evaluates object, evaluating values with "op" field, passing through literals
  • Memory contract works: {"op":"object","fields":{"my-key":{"op":"var","name":"x"},"result":42}} stores my-key and returns map
  • System prompt updated with working memory example using object
  • E2E test demonstrates multi-turn memory usage
  • Existing tests pass

Implementation Hints

Files to modify:

  • lib/ptc_runner/schema.ex - Add object operation definition (~line 78 near merge)
  • lib/ptc_runner/json/validator.ex - Add validate_object/2 for recursive field validation (~line 65 near validate_let/3)
  • lib/ptc_runner/json/operations.ex - Add eval("object", ...) implementation (~line 125 near eval("merge", ...))
  • lib/ptc_runner/schema.ex - Update to_prompt/0 memory example (line 488) to use object
  • test/ptc_runner/json/operations/collection_test.exs - Add object operation tests
  • docs/ptc-json-specification.md - Document the new object operation

Patterns to follow:

  • Schema definition pattern: see merge operation at lib/ptc_runner/schema.ex:78
  • Custom validation pattern: see validate_let/3 at lib/ptc_runner/json/validator.ex:65
  • Operation eval pattern: see eval("merge", ...) at lib/ptc_runner/json/operations.ex:125

Proposed schema definition:

"object" => %{
  "description" => "Construct object with evaluated values. Example: {op:'object', fields:{count:{op:'var', name:'n'}, name:'test'}}",
  "fields" => %{
    "fields" => %{"type" => :map, "required" => true}
  }
}

Proposed evaluation logic:

def eval("object", node, context, _eval_fn) do
  fields = Map.get(node, "fields", %{})

  Enum.reduce_while(fields, {:ok, %{}, context.memory}, fn {key, value}, {:ok, acc, memory} ->
    ctx = %{context | memory: memory}

    case evaluate_field_value(value, ctx) do
      {:ok, result, new_memory} -> {:cont, {:ok, Map.put(acc, key, result), new_memory}}
      {:error, _} = err -> {:halt, err}
    end
  end)
end

defp evaluate_field_value(value, ctx) when is_map(value) and is_map_key(value, "op") do
  Interpreter.eval(value, ctx)
end

defp evaluate_field_value(value, context) do
  {:ok, value, context.memory}
end

Edge cases to consider:

  • Empty fields map: {"op":"object","fields":{}} → returns {}
  • Mixed literal and expression values
  • Nested object operations
  • Error in one field evaluation should halt and return error

Test Plan

Unit tests:

  • object with all literal values returns map
  • object with expression values evaluates them
  • object with mixed literal/expression values
  • object with nested object operation
  • object with var reference to memory
  • object with empty fields returns empty map
  • Error in field expression propagates error

E2E test:
Multi-turn memory scenario:

  1. Turn 1: Store count using object: {"op":"let","name":"cnt","value":{...count...},"in":{"op":"object","fields":{"delivered-count":{"op":"var","name":"cnt"},"result":{"op":"var","name":"cnt"}}}}
  2. Turn 2: Read from memory: {"op":"var","name":"delivered-count"}
  3. Verify stored value matches computed value

Out of Scope

  • Changing merge behavior (it continues to require "op" on all objects)
  • Dynamic keys (keys must be string literals in the JSON)
  • Spread/rest syntax for including other objects

Documentation Updates

  • lib/ptc_runner/schema.ex - Update to_prompt/0 memory example to use object
  • docs/ptc-json-specification.md - Add object to Combine operations section (~line 774)

Metadata

Metadata

Assignees

No one assigned

    Labels

    claude-approvedMaintainer-approved for Claude automationenhancementNew feature or requestready-for-implementationIssue is approved and ready to implement

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions