phi-core 0.10.0

Simple, effective agent loop with tool execution and event streaming
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
<!-- Last verified: 2026-04-05 by Claude Code -->
# Invocation Layer

> **Scope note:** The invocation layer is split between phi-core (library-level config parsing and agent construction) and baby-phi (platform-level UI, CLI adapter, WASM plugins). This spec covers the full vision; the phi-core portion is largely implemented.

The config-driven invocation layer is the single entry point for constructing and running agents. All agent features — profile, session config, tools, callbacks, compaction, execution limits — are expressed as config and resolved through one pipeline.

**Status:** `[EXISTS]` — Phase 1 config pipeline (parse → build → agent) is fully implemented in phi-core with multi-agent support, tool registry, system prompt strategies, `file:` resolution, per-instance workspace, and `{{...}}` reference protocol. **Remaining (baby-phi scope):** WASM plugins, CLI adapter, UI adapter.

**Why:** Features exist but are invoked through scattered Rust builder APIs. There is no unified config → CLI → UI pipeline. The invocation layer makes every feature discoverable and configurable without writing Rust code.

## Concept Overview

```
Invocation Layer [EXISTS — phi-core Phase 1]
├── Config Schema [EXISTS] — TOML/JSON/YAML format defining all agent parameters
│   ├── [agent.profile] → AgentProfile
│   ├── [[agent.instances]] → Named agent variations
│   ├── [provider] → Default ModelConfig
│   ├── [[provider.instances]] → Named provider configurations
│   ├── [session] → Session configuration (scope)
│   ├── [tools] → built-in names + plugin references
│   ├── [skills] → Skill directory paths
│   ├── [sub_agents] → Default sub-agent template
│   ├── [[sub_agents.instances]] → Named sub-agent configurations
│   ├── [callbacks] → lifecycle hooks (built-in + Phase 2 plugin refs)
│   ├── [compaction] → compaction policy + callbacks
│   ├── [execution] → limits, retry, cache, tool strategy
│   └── [hooks] → context transformation (Phase 2 plugin refs)
├── Config Parser [EXISTS] — TOML/JSON/YAML → typed Rust structs (src/config/parser.rs)
├── Agent Constructor [EXISTS] — agent_from_config() / agents_from_config() (src/config/builder.rs)
└── Resolution Order [EXISTS] — config file → env vars → defaults
```

---

## Config Design Patterns

### Pattern 1: Override Order

Every field that can be set at multiple levels has an explicit override annotation. Higher levels override lower levels. `null` / absent means "inherit from lower level."

Override order key (highest wins, left to right):
- **S** = Session override
- **AI** = Agent Instance override
- **A** = Agent Profile default
- **PI** = Provider Instance override
- **P** = Provider default
- **C** = Config-level default (top-level section)

### Pattern 2: Default + Instances

Entities with multiple instances (providers, agents, sub-agents) use:
- A `[section]` block defining the **default** configuration
- An `[[section.instances]]` array defining **named variants** with overrides

The default is used when no instance is specified. Instances inherit from the default and override specific fields. This pattern applies recursively.

---

## Config Schema

The config schema is a TOML document that fully describes an agent's identity, capabilities, and runtime parameters. Every field has a sensible default — a minimal config specifying only `[provider]` is sufficient to create a working agent.

### `[agent.profile]` — Agent Identity (Default)

Maps to `AgentProfile` (G3). Override order: AI→A→C.

| Field | Type | Default | Override | Description |
|-------|------|---------|----------|-------------|
| `profile_id` | string | auto-generated UUID || Stable identity for profile sharing |
| `name` | string | `null` | AI→A→C | Human-readable agent name |
| `description` | string | `null` | AI→A→C | Purpose and capabilities |
| `system_prompt` | string | `""` | AI→A→C | Static system prompt. Future: replaced by SystemPromptStrategy |
| `thinking_level` | string | `"off"` | S→AI→A→C | Default reasoning depth: off, minimal, low, medium, high |
| `temperature` | float | `null` | S→AI→A→C | Default sampling temperature |
| `max_tokens` | integer | `null` | AI→A→C | Default max output tokens |
| `config_id` | string | `null` | AI→A→C | Stable config identity for loop_id derivation |
| `skills` | list of strings | `null` | AI→A→C | Skill directory paths (overrides `[skills].paths`) |

### `[[agent.instances]]` — Named Agent Variations

Each instance inherits all fields from `[agent.profile]` and overrides specific ones.

```toml
[agent.profile]
name = "Assistant"
system_prompt = "You are helpful."
thinking_level = "off"

# Override order: session → agent instance → agent profile → config default

[[agent.instances]]
name = "Code Reviewer"
system_prompt = "You are a code reviewer. Focus on correctness."
thinking_level = "high"
provider = "Anthropic"  # references provider instance by name
model = "claude-opus-4-6"

[[agent.instances]]
name = "Quick Helper"
system_prompt = "Be brief and direct."
provider = "OpenAI"
model = "gpt-4o-mini"
```

### `[provider]` — Model & Provider (Default)

Maps to `ModelConfig`. Override order: PI→P.

| Field | Type | Default | Override | Description |
|-------|------|---------|----------|-------------|
| `name` | string | inferred | PI→P | Display name for logging |
| `model` | string | required | S→AI→A→PI→P | Model ID (e.g., `"claude-opus-4-6"`, `"gpt-4o"`) |
| `api` | string | inferred from model | PI→P | API protocol: `anthropic`, `openai`, `google`, `bedrock`, `azure`, `vertex` |
| `api_key` | string | `null` | PI→P | API key. Supports `${ENV_VAR}` substitution |
| `base_url` | string | provider default | PI→P | Custom API endpoint |
| `context_window` | integer | model default | PI→P | Model's context window size |
| `max_tokens` | integer | model default | PI→P | Model's max output tokens |
| `headers` | table | `{}` | PI→P | Custom HTTP headers: `{ "X-Custom" = "value" }` |

### `[provider.cost]` — Per-Token Cost Rates

Maps to `CostConfig`. Override order: PI→P.

| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `input_per_million` | float | `0.0` | Cost per million input tokens |
| `output_per_million` | float | `0.0` | Cost per million output tokens |
| `cache_read_per_million` | float | `0.0` | Cost per million cached-read tokens |
| `cache_write_per_million` | float | `0.0` | Cost per million cache-write tokens |

### `[provider.compat]` — OpenAI-Compatible Provider Quirks

Maps to `OpenAiCompat`. Only relevant for providers using `api = "openai"`. Override order: PI→P.

| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `auth_style` | string | `"bearer"` | Authentication style: `bearer`, `x-api-key`, `query` |
| `reasoning_format` | string | `"standard"` | Reasoning token format: `standard`, `o1`, `deepseek` |
| `max_tokens_field` | string | `"max_tokens"` | Field name for max tokens in API request |
| `supports_streaming` | bool | `true` | Provider supports SSE streaming |
| `supports_tools` | bool | `true` | Provider supports tool/function calling |
| `supports_thinking` | bool | `false` | Provider supports extended thinking |
| `supports_system_prompt` | bool | `true` | Provider supports system prompt role |
| `supports_images` | bool | `false` | Provider supports image content |

### `[[provider.instances]]` — Named Provider Configurations

Each instance overrides the default `[provider]` fields. Referenced by name from agent instances and sub-agents.

```toml
[provider]
name = "Anthropic"
model = "claude-sonnet-4-6"
api_key = "${ANTHROPIC_API_KEY}"

[[provider.instances]]
name = "Anthropic"
models = ["claude-opus-4-6", "claude-sonnet-4-6", "claude-haiku-4-5"]
api_key = "${ANTHROPIC_API_KEY}"

[[provider.instances]]
name = "OpenAI"
models = ["gpt-4o", "gpt-4o-mini", "o3"]
api_key = "${OPENAI_API_KEY}"
base_url = "https://api.openai.com/v1"

[[provider.instances]]
name = "Local"
models = ["llama-3.1-70b"]
base_url = "http://localhost:8080/v1"
api_key = ""
```

### `[session]` — Session Overrides

Maps to Session fields (G4, G7, G9). These override agent profile defaults for the current session/task. Override order: S→AI→A→C.

| Field | Type | Default | Override | Description |
|-------|------|---------|----------|-------------|
| `scope` | string | `"ephemeral"` | S | `ephemeral` (logs stored, no introspection) or `persistent` (introspection fires during save) |
| `model` | string | `null` | S→AI→A→PI→P | Model override for this session |
| `thinking_level` | string | `null` | S→AI→A→C | Thinking override for this session |
| `temperature` | float | `null` | S→AI→A→C | Temperature override for this session |

**Resolution:** Session values override Agent Instance values, which override Agent Profile values. `null` means "inherit from next level."

### `[tools]` — Tool Registry

| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `builtin` | list of strings | `["bash", "read_file", "write_file", "edit_file", "list_files", "search"]` | Built-in tools to enable. `"all"` enables all. `"none"` disables all. |
| `mcp` | list of tables | `[]` | MCP server connections. Each: `{ command, args, transport }` |
| `plugins` | list of tables | `[]` | WASM plugin tools. Each: `{ name, path }` (Phase 2) |

### `[skills]` — Skill Directories

Maps to `SkillSet`. Override order: AI→A→C.

| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `paths` | list of strings | `[]` | Directories to load SKILL.md files from. Agent instances can override. |

### `[sub_agents]` — Sub-Agent Default Template

Default configuration for sub-agents. Acts as a template — the spawning agent can dynamically alter or override any field at runtime. If no overrides are provided, the sub-agent runs with these defaults.

| Field | Type | Default | Override | Description |
|-------|------|---------|----------|-------------|
| `max_turns` | integer | `10` | instances→default | Maximum turns for sub-agent |
| `thinking_level` | string | `"off"` | instances→default | Default thinking depth |
| `tools` | list of strings | inherits parent | instances→default | Tools available to sub-agent |

### `[[sub_agents.instances]]` — Named Sub-Agent Configurations

Each instance is a tool the parent agent can invoke. Inherits from `[sub_agents]` default.

```toml
[sub_agents]
max_turns = 10
thinking_level = "medium"

# Sub-agent instances — each becomes a tool in the parent's registry.
# The spawning agent can dynamically override any field at runtime.
# The config serves as a template/baseline.

[[sub_agents.instances]]
name = "researcher"
description = "Deep research on topics"
system_prompt = "Research thoroughly."
provider = "Anthropic"
model = "claude-opus-4-6"
tools = ["search", "read_file"]

[[sub_agents.instances]]
name = "code_writer"
description = "Writes and tests code"
system_prompt = "Write clean, tested code."
tools = ["bash", "read_file", "write_file", "edit_file"]
# inherits max_turns=10 and thinking_level="medium" from default
```

**Two patterns — both supported:**
- **Static sub-agents (config-defined):** Predefined in TOML as default templates. Registered at agent construction. The LLM decides when to invoke them. The spawning agent can override any field at runtime.
- **Dynamic sub-agents (runtime-created):** Built from the static config as a starting point, then altered programmatically. The config serves as a template/baseline; runtime code modifies fields as needed before spawning.

### `[callbacks]` — Lifecycle Hooks

Initially: references to built-in callback identifiers. Phase 2: WASM plugin references. All callbacks are Code-only (closures today, WASM plugins in Phase 2).

| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `before_turn` | string | `null` | `[Code-only: Phase 2]` Before each LLM turn. Return false to abort. |
| `after_turn` | string | `null` | `[Code-only: Phase 2]` After each LLM turn. |
| `before_loop` | string | `null` | `[Code-only: Phase 2]` Before agent loop starts. Return false to abort. |
| `after_loop` | string | `null` | `[Code-only: Phase 2]` After agent loop ends. |
| `before_tool_execution` | string | `null` | `[Code-only: Phase 2]` Before each tool call. Return false to skip. |
| `after_tool_execution` | string | `null` | `[Code-only: Phase 2]` After each tool call. |
| `before_tool_execution_update` | string | `null` | `[Code-only: Phase 2]` Before each streaming tool update. |
| `after_tool_execution_update` | string | `null` | `[Code-only: Phase 2]` After each streaming tool update. |
| `on_error` | string | `null` | `[Code-only: Phase 2]` When LLM returns StopReason::Error. |
| `before_compaction_start` | string | `null` | `[Code-only: Phase 2]` (G1) Before compaction fires. |
| `after_compaction_end` | string | `null` | `[Code-only: Phase 2]` (G1) After compaction completes. |
| `before_task` | string | `null` | `[Code-only: Phase 2]` (G2) Session-level lifecycle hook. |
| `after_task` | string | `null` | `[Code-only: Phase 2]` (G2) Session-level lifecycle hook. |

### `[hooks]` — Context Transformation

Code-only hooks for message pipeline manipulation. Phase 2: WASM plugin references.

| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `convert_to_llm` | string | `null` | `[Code-only: Phase 2]` AgentMessage[] → Message[] before each LLM call |
| `transform_context` | string | `null` | `[Code-only: Phase 2]` Prune/reorder/inject before convert_to_llm |

### `[[filters]]` — Input Filters

Code-only filters for message validation. Phase 2: WASM plugin references.

| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `plugin` | string || `[Code-only: Phase 2]` Plugin path or identifier |

### `[compaction]` — Context Management

Maps to `CompactionConfig` (G5 consolidation target). Override order: C only.

| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `max_context_tokens` | integer | `100000` | Model's context window |
| `system_prompt_tokens` | integer | `4000` | Tokens reserved for system prompt |
| `compact_at_pct` | float | `0.90` | Fraction of context that triggers compaction |
| `budget_threshold_pct` | float | `0.05` | Minimum headroom before compaction fires |
| `scope` | string | `"fixed_count(3)"` | Compaction scope: `fixed_count(N)` or `token_budget` |
| `keep_first_turns` | integer | `2` | Turns kept verbatim from start |
| `keep_recent_turns` | integer | `10` | Recent turns kept with truncated tool output |
| `tool_output_max_lines` | integer | `50` | Max lines per tool output in recent section |
| `max_summary_tokens` | integer | `4000` | Token budget for summarized sections |
| `strategy` | string | `null` | `[Code-only: Phase 2]` Custom compaction strategy plugin |

### `[execution]` — Limits & Safety

Maps to `ExecutionLimits` + `ToolExecutionStrategy`. Override order: C only.

| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `max_turns` | integer | `50` | Maximum turns per loop |
| `max_tokens` | integer | `1000000` | Maximum total tokens per loop |
| `max_duration_secs` | integer | `600` | Maximum wall-clock time per loop |
| `max_cost` | float | `null` | Maximum dollar cost per loop |
| `tool_strategy` | string | `"parallel"` | Tool execution: `parallel`, `sequential`, `batched(N)` |

### `[execution.retry]` — Retry Configuration

Maps to `RetryConfig`. Override order: C only.

| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `max_attempts` | integer | `3` | Maximum retry attempts for transient errors |
| `initial_delay_ms` | integer | `1000` | Delay before first retry (milliseconds) |
| `backoff_multiplier` | float | `2.0` | Multiplier applied each attempt |
| `max_delay_ms` | integer | `30000` | Maximum delay cap (milliseconds) |

### `[execution.cache]` — Prompt Caching

Maps to `CacheConfig`. Override order: C only.

| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `enabled` | bool | `true` | Master switch for caching |
| `strategy` | string | `"auto"` | Cache strategy: `auto`, `disabled`, `manual` |
| `system` | bool | `true` | (manual only) Cache the system prompt |
| `tools` | bool | `true` | (manual only) Cache tool definitions |
| `messages` | bool | `true` | (manual only) Cache conversation history |

---

## Complete Field Audit

### A. Config-Representable Fields — Full Override Mapping

| Field | Struct | Override Order | Config Section |
|-------|--------|---------------|----------------|
| `model` | `ModelConfig` | S→AI→A→PI→P | `[provider].model` / `[session].model` |
| `thinking_level` | `ThinkingLevel` | S→AI→A→C | `[agent.profile]` / `[session]` |
| `temperature` | `Option<f32>` | S→AI→A→C | `[agent.profile]` / `[session]` |
| `system_prompt` | `String` | AI→A→C | `[agent.profile]` |
| `max_tokens` | `Option<u32>` | AI→A→C | `[agent.profile]` |
| `name` (display) | `ModelConfig` | PI→P | `[provider].name` |
| `context_window` | `ModelConfig` | PI→P | `[provider].context_window` |
| `api_key` | `ModelConfig` | PI→P | `[provider].api_key` |
| `base_url` | `ModelConfig` | PI→P | `[provider].base_url` |
| `cost.*` | `CostConfig` | PI→P | `[provider.cost]` |
| `headers` | `ModelConfig` | PI→P | `[provider].headers` |
| `compat.*` | `OpenAiCompat` | PI→P | `[provider.compat]` |
| `tool_execution` | `ToolExecutionStrategy` | C | `[execution].tool_strategy` |
| `max_turns` | `ExecutionLimits` | C | `[execution].max_turns` |
| `max_total_tokens` | `ExecutionLimits` | C | `[execution].max_tokens` |
| `max_duration` | `ExecutionLimits` | C | `[execution].max_duration_secs` |
| `max_cost` | `ExecutionLimits` | C | `[execution].max_cost` |
| `retry.*` | `RetryConfig` | C | `[execution.retry]` |
| `cache.*` | `CacheConfig` | C | `[execution.cache]` |
| `compaction.*` | `CompactionConfig` | C | `[compaction]` |
| Skills | `SkillSet` | AI→A→C | `[skills].paths` |
| Sub-agents | `SubAgentTool` | instances→default | `[[sub_agents.instances]]` |
| `config_id` | `AgentLoopConfig` | AI→A→C | `[agent.profile].config_id` |
| `scope` | `SessionScope` | S | `[session].scope` |
| `builtin` tools | `Vec<AgentTool>` | C | `[tools].builtin` |
| MCP servers | `Vec<McpToolAdapter>` | C | `[tools].mcp` |

### B. Code-Only Fields — Phase 2 WASM Plugin References

These require executable code (closures/trait impls today). Phase 2 maps them to WASM plugin references in config.

| Field | Struct | Config Representation (Phase 2) |
|-------|--------|--------------------------------|
| `before_turn` | `AgentLoopConfig` | `[callbacks].before_turn = "plugin:path.wasm"` |
| `after_turn` | `AgentLoopConfig` | `[callbacks].after_turn = "plugin:..."` |
| `before_loop` | `AgentLoopConfig` | `[callbacks].before_loop = "..."` |
| `after_loop` | `AgentLoopConfig` | `[callbacks].after_loop = "..."` |
| `before_tool_execution` | `AgentLoopConfig` | `[callbacks].before_tool_execution = "..."` |
| `after_tool_execution` | `AgentLoopConfig` | `[callbacks].after_tool_execution = "..."` |
| `before_tool_execution_update` | `AgentLoopConfig` | `[callbacks].before_tool_execution_update = "..."` |
| `after_tool_execution_update` | `AgentLoopConfig` | `[callbacks].after_tool_execution_update = "..."` |
| `on_error` | `AgentLoopConfig` | `[callbacks].on_error = "..."` |
| `before_compaction_start` | `AgentLoopConfig` (G1) | `[callbacks].before_compaction_start = "..."` |
| `after_compaction_end` | `AgentLoopConfig` (G1) | `[callbacks].after_compaction_end = "..."` |
| `convert_to_llm` | `AgentLoopConfig` | `[hooks].convert_to_llm = "..."` |
| `transform_context` | `AgentLoopConfig` | `[hooks].transform_context = "..."` |
| `input_filters` | `AgentLoopConfig` | `[[filters]]` with plugin refs |
| `compaction_strategy` | `AgentLoopConfig` | `[compaction].strategy = "plugin:..."` |
| `block_compaction_strategy` | `AgentLoopConfig` | Same as above |
| `provider_override` | `BasicAgent` | Not in config — test/injection escape hatch only |

### C. Runtime Fields — NOT in Config

Populated during execution, not at construction. Not configurable.

| Field | Struct | Why Runtime |
|-------|--------|------------|
| `messages` | `AgentContext` | Accumulated conversation history |
| `tools` (instances) | `AgentContext` | Instantiated tool objects (config specifies *which* tools by name) |
| `session` | `AgentContext` | Managed by SessionRecorder from events |
| `steering_queue` | `BasicAgent` | Populated via `Agent::steer()` at runtime |
| `follow_up_queue` | `BasicAgent` | Populated via `Agent::follow_up()` at runtime |
| `steering_mode` | `BasicAgent` | Runtime control via `Agent::set_steering_mode()` |
| `follow_up_mode` | `BasicAgent` | Runtime control |
| `is_streaming` | `BasicAgent` | Guard flag during execution |
| `cancel` | `BasicAgent` | Created per-loop for cancellation |
| `get_steering_messages` | `AgentLoopConfig` | Closure wrapping queue access |
| `get_follow_up_messages` | `AgentLoopConfig` | Closure wrapping queue access |

### D. Internal/Derived Fields — NOT in Config

Auto-generated or derived. Not user-configurable.

| Field | Struct | Why Internal |
|-------|--------|-------------|
| `agent_id` | `BasicAgent` | Auto-generated UUID at construction |
| `session_id` | `BasicAgent` | Auto-generated UUID, rotated via `new_session()` |
| `loop_id` | `AgentContext` | Derived from `session_id.config_id.counter` |
| `parent_loop_id` | `AgentContext` | Set automatically for continuations |
| `continuation_kind` | `AgentContext` | Set by `continue_loop()` call semantics |
| `first_turn_trigger` | `AgentLoopConfig` | Auto-set: `User` for origin, `SubAgent` for sub-agents |
| `loop_counters` | `BasicAgent` | Per-config counter for loop_id generation |
| `last_loop_id` | `BasicAgent` | Tracked automatically per-prompt |
| `last_active_at` | `BasicAgent` | Updated automatically per-prompt |
| All `LoopRecord` fields | `LoopRecord` | Populated by SessionRecorder from events |
| All `Turn` fields | `Turn` | Populated by SessionRecorder from events |
| All `Session` identity fields | `Session` | Managed by SessionRecorder |

### `[system_prompt_strategy]` -- System Prompt Strategy Template

Defines a reusable strategy for assembling system prompts from ordered blocks. Each block has a name, order, and optional max length. Strategies are referenced by prompt instances.

```toml
[[system_prompt_strategy.instances]]
id = "coding-assistant"
description = "Strategy for coding assistant prompts"

[[system_prompt_strategy.instances.blocks]]
name = "identity"
order = 1
max_length = 500

[[system_prompt_strategy.instances.blocks]]
name = "capabilities"
order = 2
max_length = 1000

[[system_prompt_strategy.instances.blocks]]
name = "instructions"
order = 3
max_length = 2000
```

| Field | Type | Description |
|-------|------|-------------|
| `id` | string | Unique strategy identifier |
| `description` | string | Human-readable description |
| `blocks[].name` | string | Block name (referenced by prompt instances) |
| `blocks[].order` | integer | Assembly order (lower = earlier in prompt) |
| `blocks[].max_length` | integer | Maximum token length for this block |

### `[system_prompt]` -- System Prompt Instances

Defines concrete prompt instances that reference a strategy and provide content for each block. Block content fields are flattened at the instance level using the block name.

```toml
[[system_prompt.instances]]
id = "code-reviewer-prompt"
description = "Prompt for code review tasks"
type = "coding-assistant"  # references strategy id
identity = "You are a senior code reviewer."
capabilities = "You can read files, search code, and run tests."
instructions = "Focus on correctness, security, and maintainability."
```

| Field | Type | Description |
|-------|------|-------------|
| `id` | string | Unique prompt instance identifier |
| `description` | string | Human-readable description |
| `type` | string | References a `system_prompt_strategy` instance by id |
| *(block names)* | string | Flattened block content fields matching the strategy's block names |

### `default_workspace` and `[agent].workspace` -- Workspace Configuration

| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `default_workspace` | `Option<String>` | `null` | Top-level default workspace directory for all agents |
| `[agent.profile].workspace` | `Option<String>` | `null` | Agent-level workspace override; takes precedence over `default_workspace` |

Agent instances can also override workspace:

```toml
default_workspace = "./workspace"

[agent.profile]
workspace = "./agent-workspace"  # overrides default_workspace

[[agent.instances]]
name = "researcher"
workspace = "./research-workspace"  # overrides agent profile workspace
```

---

## Resolution Order

```
Config file values
  → override with env vars (${VAR} substitution in string fields)
  → override with CLI flags (when CLI adapter exists — external)
  → fill remaining with defaults
```

For multi-level fields (model, thinking_level, temperature):
```
Session override → Agent Instance → Agent Profile default → Provider Instance → Provider default → Config default
```

All secrets (`api_key`, credentials) support `${ENV_VAR}` substitution to avoid plaintext in config files.

---

## Agent Construction

### Entry Point

```rust
/// Construct an Agent from a parsed config.
/// This is the single entry point for config-driven agent creation.
pub fn agent_from_config(config: AgentConfig) -> Result<BasicAgent, ConfigError>
```

`AgentConfig` is the typed Rust struct deserialized from the TOML config. It maps directly to the config sections above.

### Construction Flow

```
TOML file → parse → AgentConfig struct
  → resolve env vars
  → build AgentProfile from [agent.profile] + [provider]
  → build Session overrides from [session]
  → register tools from [tools] (built-in + MCP + plugins)
  → register skills from [skills]
  → register sub-agents from [[sub_agents.instances]]
  → register callbacks from [callbacks] (Phase 2: load WASM plugins)
  → build CompactionConfig from [compaction]
  → build ExecutionLimits from [execution]
  → build RetryConfig from [execution.retry]
  → build CacheConfig from [execution.cache]
  → assemble BasicAgent (or custom Agent impl)
```

### Config Hierarchy (Agent-Centric)

```
┌──────────────────────────────────────────────────┐
│                    CONFIG FILE                     │
│  (TOML — all static values flow from here)        │
├──────────────────────────────────────────────────┤
│  [agent.profile]     → AgentProfile              │
│  [[agent.instances]] → Named agent variations     │
│  [provider]          → Default ModelConfig        │
│  [[provider.instances]] → Named providers         │
│  [provider.cost]     → CostConfig                │
│  [provider.compat]   → OpenAiCompat              │
│  [session]           → Session overrides          │
│  [tools]             → Tool registry (by name)    │
│  [skills]            → SkillSet paths             │
│  [sub_agents]        → Sub-agent default template │
│  [[sub_agents.instances]] → Named sub-agents      │
│  [callbacks]         → Phase 2 WASM plugin refs   │
│  [hooks]             → Phase 2 WASM plugin refs   │
│  [[filters]]         → Phase 2 WASM plugin refs   │
│  [compaction]        → CompactionConfig           │
│  [execution]         → ExecutionLimits            │
│  [execution.retry]   → RetryConfig               │
│  [execution.cache]   → CacheConfig               │
└───────────────────────┬──────────────────────────┘
              ┌─────────────────┐
              │  AgentProfile   │ ← agent identity + defaults
              └────────┬────────┘
              ┌────────▼────────┐
              │  Session        │ ← per-task overrides
              │  (model, thinking, temp, scope)
              └────────┬────────┘
                       │ resolve_*()
              ┌────────▼────────┐
              │  Agent          │ ← runtime: tools, queues, state
              │  (BasicAgent)   │
              └────────┬────────┘
                       │ build_config()
              ┌────────▼────────┐
              │ AgentLoopConfig │ ← resolved, static, borrowed
              └────────┬────────┘
              ┌────────▼────────┐
              │  agent_loop()   │ ← execution engine
              └─────────────────┘
```

---

## Config Examples

### Minimal Config

```toml
# Smallest possible config — everything else uses defaults
[provider]
model = "claude-sonnet-4-6"
api_key = "${ANTHROPIC_API_KEY}"
```

### Full Config

```toml
[agent.profile]
name = "Code Reviewer"
description = "Reviews pull requests for correctness and style"
system_prompt = """
You are a senior code reviewer. Focus on correctness, security, and maintainability.
"""
thinking_level = "high"

[provider]
name = "Anthropic"
model = "claude-opus-4-6"
api_key = "${ANTHROPIC_API_KEY}"

[provider.cost]
input_per_million = 15.0
output_per_million = 75.0

[[provider.instances]]
name = "OpenAI"
model = "gpt-4o"
api_key = "${OPENAI_API_KEY}"

[session]
scope = "persistent"

[tools]
builtin = ["bash", "read_file", "search", "edit_file"]

[tools.mcp.github]
command = "npx"
args = ["-y", "@modelcontextprotocol/server-github"]
transport = "stdio"

[skills]
paths = ["./skills", "~/.phi/skills"]

[sub_agents]
max_turns = 10
thinking_level = "medium"

[[sub_agents.instances]]
name = "researcher"
description = "Deep research on topics"
system_prompt = "Research thoroughly and cite sources."
tools = ["search", "read_file"]

[compaction]
max_context_tokens = 200000
keep_recent_turns = 15
max_summary_tokens = 6000

[execution]
max_turns = 30
max_duration_secs = 300
tool_strategy = "parallel"

[execution.retry]
max_attempts = 3
initial_delay_ms = 1000

[execution.cache]
strategy = "auto"
```

---

## Relationship to Core Gaps (G1-G9)

The config schema subsumes the P1 gaps — they become config sections rather than standalone refactors:

| Gap | Config Section | Implementation |
|-----|---------------|---------------|
| G1: Compaction callbacks | `[callbacks].before_compaction_start` | Callback types + config mapping |
| G3: Agent Profile | `[agent.profile]` + `[[agent.instances]]` | `AgentProfile` struct |
| ~~G4: Session model override~~ | ~~`[session].model`~~ | Removed — model config now per-loop in `LoopConfigSnapshot` |
| G5: Compaction config consolidation | `[compaction]` | Unified `CompactionConfig` |
| G7: Session scope | `[session].scope` | `SessionScope` enum |
| ~~G9: Session task attributes~~ | ~~`[session].thinking_level`, `[session].temperature`~~ | Removed — now per-loop in `LoopConfigSnapshot` |

---

## Core vs External Boundary

| Component | Boundary | Rationale |
|-----------|----------|-----------|
| Config schema (TOML structure) | **Core** | Contract — all consumers share one schema |
| Config parser | **Core** | Cross-cutting, all agents need it |
| `agent_from_config()` | **Core** | Single entry point |
| Env var substitution | **Core** | Universal deployment pattern |
| CLI adapter | **External** | App-specific CLI needs |
| UI adapter | **External** | App-specific UI choices |
| Config file discovery | **External** | App-specific paths |

---

## Code Reference

| File | What it will contain |
|------|---------------------|
| `src/config/schema.rs` | `AgentConfig` struct (deserialized from TOML) |
| `src/config/parser.rs` | TOML parsing + env var resolution |
| `src/config/builder.rs` | `agent_from_config()` constructor |
| `src/config/mod.rs` | Module root + re-exports |
| `src/agents/profile.rs` | `AgentProfile` struct with `resolve_*` methods |

---

## Design Decisions

- **TOML over YAML/JSON:** TOML is the Rust ecosystem standard (Cargo.toml). It's readable, has good error messages, and maps naturally to nested structs. JSON lacks comments. YAML has whitespace footguns.
- **Config is additive, not exclusive:** The programmatic Rust API (`BasicAgent::new().with_*()`) remains fully supported. Config is an additional entry point for users who don't want to write Rust.
- **No runtime config reloading:** Config is read once at agent construction. Hot-reload is a future enhancement, not in scope.
- **Default + Instances pattern:** Enables multi-model, multi-agent setups. Instances inherit from defaults and override specific fields. Recursive application (provider instances, agent instances, sub-agent instances).
- **Explicit override annotations:** Every multi-level field documents its full override chain. No implicit precedence rules — the config spec is the source of truth.