JSON Mode SubAgents

Copy Markdown View Source

JSON mode returns structured data directly from the LLM without executing PTC-Lisp code.

When to Use JSON Mode

Use JSON mode for tasks that need structured output but not computation or tool calls:

Task TypeModeWhy
ClassificationJSONDirect structured response
Entity extractionJSONNo computation needed
Summarization with structureJSONSimple output mapping
Multi-step reasoningPTC-LispNeeds tool calls
Data transformationPTC-LispNeeds computation
External API callsPTC-LispNeeds tools

Basic Usage

{:ok, step} = SubAgent.run(
  "Classify the sentiment of: {{text}}",
  context: %{text: "I love this product!"},
  output: :json,
  signature: "(text :string) -> {sentiment :string, score :float}",
  llm: my_llm
)

step.return  #=> %{"sentiment" => "positive", "score" => 0.95}

Constraints: JSON mode requires a signature, cannot use tools, and doesn't support compression or firewall fields.

Mustache Templates

JSON mode embeds data directly in the prompt using Mustache syntax. All signature parameters must appear in the prompt.

Simple Variables

Reference context values with {{variable}}:

SubAgent.new(
  prompt: "Analyze the sentiment of: {{text}}",
  output: :json,
  signature: "(text :string) -> {sentiment :string}"
)

Nested access uses dot notation: {{user.name}}, {{order.items.count}}.

Sections for Lists

Iterate over lists with {{#section}}...{{/section}}:

SubAgent.new(
  prompt: """
  Categorize these products:
  {{#products}}
  - {{name}}: ${{price}}
  {{/products}}
  """,
  output: :json,
  signature: "(products [{name :string, price :float}]) -> {categories [{name :string, category :string}]}"
)

With context %{products: [%{name: "Widget", price: 9.99}, %{name: "Gadget", price: 19.99}]}, the prompt expands to:

Categorize these products:
- Widget: $9.99
- Gadget: $19.99

Scalar Lists with Dot Notation

For lists of primitives, use {{.}} to reference the current element:

SubAgent.new(
  prompt: "Classify these tags: {{#tags}}{{.}}, {{/tags}}",
  output: :json,
  signature: "(tags [:string]) -> {primary_tag :string}"
)

Inverted Sections

Use {{^section}} to render content when a value is falsy or empty:

SubAgent.new(
  prompt: """
  {{#items}}Process items...{{/items}}
  {{^items}}No items to process.{{/items}}
  """,
  output: :json,
  signature: "(items [:string]) -> {status :string}"
)

Validation Rules

JSON mode enforces strict validation at agent construction time.

All Parameters Must Be Used

Every signature parameter must appear in the prompt (as a variable or section):

# Valid - both params used
SubAgent.new(
  prompt: "Analyze {{text}} for {{user}}",
  output: :json,
  signature: "(text :string, user :string) -> {result :string}"
)

# Invalid - 'user' not used
SubAgent.new(
  prompt: "Analyze {{text}}",
  output: :json,
  signature: "(text :string, user :string) -> {result :string}"
)
# => ArgumentError: JSON mode requires all signature params in prompt. Unused: ["user"]

Section Fields Must Match Signature

Fields inside sections are validated against the element type:

# Valid - 'name' exists in element type
SubAgent.new(
  prompt: "{{#items}}{{name}}{{/items}}",
  output: :json,
  signature: "(items [{name :string, price :float}]) -> {count :int}"
)

# Invalid - 'unknown' not in element type
SubAgent.new(
  prompt: "{{#items}}{{unknown}}{{/items}}",
  output: :json,
  signature: "(items [{name :string}]) -> {count :int}"
)
# => ArgumentError: {{unknown}} inside {{#items}} not found in element type

Dot Notation Requires Scalar Lists

Use {{.}} only for lists of primitives, not lists of maps:

# Valid - tags is [:string]
SubAgent.new(
  prompt: "{{#tags}}{{.}}{{/tags}}",
  output: :json,
  signature: "(tags [:string]) -> {count :int}"
)

# Invalid - items is [{name :string}], use {{name}} instead
SubAgent.new(
  prompt: "{{#items}}{{.}}{{/items}}",
  output: :json,
  signature: "(items [{name :string}]) -> {count :int}"
)
# => ArgumentError: {{.}} inside {{#items}} - use {{field}} instead (list contains maps)

JSON Mode vs PTC-Lisp Mode

AspectJSON ModePTC-Lisp Mode
Data in promptEmbedded via MustacheShown in Data Inventory
Template syntaxFull Mustache (sections)Simple {{var}} only
LLM outputJSON objectPTC-Lisp code
ToolsNot supportedSupported
CompressionNot supportedSupported
Use caseClassification, extractionComputation, orchestration

Piping Between Modes

JSON mode returns the standard Step struct, enabling seamless piping:

# JSON mode extracts data
extract_agent = SubAgent.new(
  prompt: "Extract entities from: {{text}}",
  output: :json,
  signature: "(text :string) -> {entities [:string], topic :string}"
)

# PTC-Lisp mode processes with tools
process_agent = SubAgent.new(
  prompt: "Look up details for {{topic}}",
  signature: "(entities [:string], topic :string) -> {details [:map]}",
  tools: %{lookup: &MyApp.lookup/1}
)

{:ok, step1} = SubAgent.run(extract_agent, context: %{text: "..."}, llm: llm)
{:ok, step2} = SubAgent.run(process_agent, context: step1, llm: llm)

See Also