# `PtcRunner.SubAgent.SystemPrompt`
[🔗](https://github.com/andreasronge/ptc_runner/blob/main/lib/ptc_runner/sub_agent/system_prompt.ex#L1)

System prompt generation for SubAgent LLM interactions.

Orchestrates prompt generation by combining sections from:
- `Namespace` modules - Compact Lisp-style format for tools and data
- `SystemPrompt.Output` - Expected return format from signature

## Prompt Caching Architecture

To enable efficient prompt caching (e.g., Anthropic's cache_control), the prompt
is split into **static** and **dynamic** sections:

- **Static (system prompt)**: `generate_system/2` returns language reference and output
  format - these rarely change and benefit from caching across different questions.
- **Dynamic (user message)**: `generate_context/2` returns data inventory, tool schemas,
  and expected output - these vary per agent configuration but not per question.

The mission is placed only in the user message (not duplicated in system prompt).

## Customization

The `system_prompt` field on `SubAgent` accepts:
- **Map** - `:prefix`, `:suffix`, `:language_spec`, `:output_format`
- **Function** - `fn default_prompt -> modified_prompt end`
- **String** - Complete override

## Language Spec

The `:language_spec` option can be:
- **Atom** - Resolved via `PtcRunner.Lisp.LanguageSpec.get!/1`
- **String** - Used as-is
- **Callback** - `fn ctx -> "prompt" end`

Default: `:single_shot` for `max_turns: 1`, `:explicit_return` otherwise.

## Examples

    iex> agent = PtcRunner.SubAgent.new(prompt: "Add {{x}} and {{y}}")
    iex> context = %{x: 5, y: 3}
    iex> prompt = PtcRunner.SubAgent.SystemPrompt.generate(agent, context: context)
    iex> prompt =~ "<return_rules>"
    true
    iex> prompt =~ "data/x"
    true

# `apply_customization`

```elixir
@spec apply_customization(
  String.t(),
  PtcRunner.SubAgent.Definition.system_prompt_opts() | nil
) ::
  String.t()
```

Apply system prompt customization (string override, function, or map with prefix/suffix).

## Examples

    iex> PtcRunner.SubAgent.SystemPrompt.apply_customization("base", nil)
    "base"

    iex> PtcRunner.SubAgent.SystemPrompt.apply_customization("base", "override")
    "override"

    iex> PtcRunner.SubAgent.SystemPrompt.apply_customization("base", fn p -> "PREFIX\n" <> p end)
    "PREFIX\nbase"

# `combined_mode_reference_card`

```elixir
@spec combined_mode_reference_card(PtcRunner.SubAgent.Definition.t()) ::
  String.t() | nil
```

Combined-mode (`output: :text, ptc_transport: :tool_call`) PTC-Lisp
reference card.

Returns the static compact card from `priv/prompts/` plus a dynamically
rendered tool-inventory section for tools whose effective `expose:` is
`:ptc_lisp` or `:both`.

Per Addendum #19: included even when zero PTC-callable tools exist —
the static portion documents `lisp_eval` itself, which is
always callable.

Returns `nil` for non-combined-mode agents (used by callers to filter
the section out of their prompt assembly).

Tier 3.5 Fix 2: exported as a public helper so `Loop.TextMode` can
append it to the tool-calling system prompt at runtime — the runtime
text-mode path uses `Prompts.tool_calling_system()` rather than
`generate_system/2`, so this is the integration point.

# `generate`

```elixir
@spec generate(
  PtcRunner.SubAgent.Definition.t(),
  keyword()
) :: String.t()
```

Generate a complete system prompt for a SubAgent.

Options: `context` (map), `error_context` (map for recovery prompts).

## Examples

    iex> agent = PtcRunner.SubAgent.new(prompt: "Process data")
    iex> prompt = PtcRunner.SubAgent.SystemPrompt.generate(agent, context: %{user: "Alice"})
    iex> prompt =~ "<return_rules>" and prompt =~ "<output_format>"
    true

# `generate_context`

```elixir
@spec generate_context(
  PtcRunner.SubAgent.Definition.t(),
  keyword()
) :: String.t()
```

Generate dynamic context sections (prepended to user message).

Returns data inventory, tool schemas, and expected output - these sections vary
per agent configuration but not per individual question.

Note: The mission is NOT included here - it's already in the user message.

## Options

- `:context` - Map of context variables for the data inventory
- `:received_field_descriptions` - Field descriptions from upstream agent

## Examples

    iex> agent = PtcRunner.SubAgent.new(prompt: "Test", tools: %{"search" => fn _ -> [] end})
    iex> context_prompt = PtcRunner.SubAgent.SystemPrompt.generate_context(agent, context: %{x: 1})
    iex> context_prompt =~ ";; === data/ ===" and context_prompt =~ ";; === tools ==="
    true
    iex> context_prompt =~ "<mission>"
    false

# `generate_error_recovery_prompt`

```elixir
@spec generate_error_recovery_prompt(map()) :: String.t()
```

Generate error recovery prompt for parse failures.

## Examples

    iex> error = %{type: :parse_error, message: "Unexpected token"}
    iex> PtcRunner.SubAgent.SystemPrompt.generate_error_recovery_prompt(error) =~ "<previous_error>"
    true

# `generate_static`

```elixir
@spec generate_static(
  PtcRunner.SubAgent.Definition.t(),
  keyword()
) :: String.t()
```

Alias for `generate_system/2` for semantic clarity.

See `generate_system/2` for documentation.

# `generate_system`

```elixir
@spec generate_system(
  PtcRunner.SubAgent.Definition.t(),
  keyword()
) :: String.t()
```

Generate static system prompt sections (cacheable).

Returns only the language reference and output format - these sections rarely
change across different questions and benefit from prompt caching.

This function has an alias `generate_static/2` for semantic clarity.

## Options

- `:resolution_context` - Map with turn/model/memory/messages for language_spec callbacks

## Examples

    iex> agent = PtcRunner.SubAgent.new(prompt: "Test")
    iex> system = PtcRunner.SubAgent.SystemPrompt.generate_system(agent)
    iex> system =~ "<return_rules>" and system =~ "<output_format>"
    true
    iex> system =~ "# Data Inventory"
    false

# `render_mission_log`

```elixir
@spec render_mission_log(map()) :: String.t()
```

Render a Mission Log section from a journal map.

Shows completed tasks with truncated values (~200 chars).
Used to inject journal state into the system prompt so the LLM
knows which tasks have already been completed.

# `resolve_language_spec`

```elixir
@spec resolve_language_spec(
  String.t()
  | atom()
  | {:profile, atom()}
  | {:profile, atom(), keyword()}
  | (map() -&gt; String.t()),
  map()
) :: String.t()
```

Resolve a language_spec value to a string.

## Examples

    iex> PtcRunner.SubAgent.SystemPrompt.resolve_language_spec("custom prompt", %{})
    "custom prompt"

    iex> spec = PtcRunner.SubAgent.SystemPrompt.resolve_language_spec(:single_shot, %{})
    iex> is_binary(spec) and String.contains?(spec, "<single_shot>")
    true

    iex> callback = fn ctx -> if ctx.turn > 1, do: "multi", else: "single" end
    iex> PtcRunner.SubAgent.SystemPrompt.resolve_language_spec(callback, %{turn: 1})
    "single"

# `truncate_if_needed`

```elixir
@spec truncate_if_needed(String.t(), map() | nil) :: String.t()
```

Truncate prompt if it exceeds the configured character limit.

## Examples

    iex> PtcRunner.SubAgent.SystemPrompt.truncate_if_needed("short", nil)
    "short"

    iex> result = PtcRunner.SubAgent.SystemPrompt.truncate_if_needed(String.duplicate("x", 1000), %{max_chars: 100})
    iex> result =~ "truncated"
    true

---

*Consult [api-reference.md](api-reference.md) for complete listing*
