Skip to content

Commit e01ceaf

Browse files
authored
Merge pull request #71 from Sewer56/architecture-agents
Simplify agent build flow, task schema, and runtime docs
2 parents db833af + 34c19eb commit e01ceaf

20 files changed

Lines changed: 879 additions & 456 deletions

File tree

src/.cargo/verify.ps1

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ try {
7676
}
7777

7878
Write-Host "Formatting..."
79-
Invoke-LoggedCommand "cargo" @("fmt", "--all", "--check", "--quiet")
79+
Invoke-LoggedCommand "cargo" @("fmt", "--all", "--quiet")
8080

8181
Write-Host "Linux-only feature coverage..."
8282
if ($onLinux) {

src/.cargo/verify.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ DOC_ARGS=(--workspace --document-private-items --no-deps --quiet --exclude llm-c
6262
run_cmd env RUSTDOCFLAGS="-D warnings" cargo doc "${DOC_ARGS[@]}"
6363

6464
echo "Formatting..."
65-
run_cmd cargo fmt --all --check --quiet
65+
run_cmd cargo fmt --all --quiet
6666

6767
echo "Linux-only feature coverage..."
6868
if [ "$IS_LINUX" = true ]; then
Lines changed: 340 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,340 @@
1+
# Architecture: llm-coding-tools-agents
2+
3+
Framework-agnostic agent configuration loading, catalog management, model
4+
resolution, permission filtering, and runtime assembly.
5+
6+
Upstream integrations (e.g. `llm-coding-tools-serdesai`) consume the
7+
[`AgentRuntime`] produced here and adapt it to their framework's agent builder.
8+
9+
## Table of Contents
10+
11+
- [Quick Start](#quick-start)
12+
- [Phase 1: Loading](#phase-1-loading)
13+
- [Loading Pipeline](#loading-pipeline)
14+
- [File Discovery](#file-discovery)
15+
- [YAML Preprocessor](#yaml-preprocessor)
16+
- [Phase 2: Building](#phase-2-building)
17+
- [Building the Runtime](#building-the-runtime)
18+
- [AgentDefaults](#agentdefaults)
19+
- [Tool Catalog](#tool-catalog)
20+
- [Model Resolution](#model-resolution)
21+
- [Phase 3: Runtime Usage](#phase-3-runtime-usage)
22+
- [Permission Filtering](#permission-filtering)
23+
- [Allowed Tools](#allowed-tools)
24+
- [Callable Targets](#callable-targets)
25+
- [Reference](#reference)
26+
- [Error Model](#error-model)
27+
- [Testing](#testing)
28+
- [File Map](#file-map)
29+
30+
## Quick Start
31+
32+
Three steps from markdown files to a working runtime:
33+
34+
```rust
35+
use llm_coding_tools_agents::{AgentLoader, AgentCatalog, AgentRuntimeBuilder, AgentDefaults};
36+
use std::path::Path;
37+
38+
// 1. Load agents from directory
39+
let loader = AgentLoader::new();
40+
let mut catalog = AgentCatalog::new();
41+
loader.add_directory(&mut catalog, Path::new("agents"))?;
42+
43+
// 2. Build the runtime
44+
let runtime = AgentRuntimeBuilder::new()
45+
.catalog(catalog)
46+
.defaults(AgentDefaults::with_model("openai/gpt-4o"))
47+
.build();
48+
49+
// 3. Use the runtime (e.g., look up agents by name)
50+
let agent = runtime.catalog().by_name("code-reviewer").unwrap();
51+
// ... pass to your framework's agent builder
52+
```
53+
54+
## Phase 1: Loading
55+
56+
Agent definitions live in markdown files with YAML frontmatter:
57+
58+
```markdown
59+
---
60+
name: code-reviewer
61+
mode: subagent
62+
description: Reviews code and flags high-risk issues
63+
model: openrouter/openai/gpt-4o
64+
permission:
65+
read: allow
66+
bash: deny
67+
task:
68+
"*": deny
69+
review-*: allow
70+
---
71+
You are a careful code reviewer.
72+
```
73+
74+
### Loading Pipeline
75+
76+
```text
77+
.md file / string / bytes
78+
79+
│ AgentLoader::add_directory / add_file / add_from_str
80+
81+
┌─────────────────────────────────────────────────────────────────────┐
82+
│ 1. CRLF -> LF normalization crlf-to-lf-inplace │
83+
│ 2. Find frontmatter delimiters parser/mod.rs │
84+
│ 3. Preprocess YAML preprocessor │
85+
│ Rewrites colon-containing values to block scalars │
86+
│ 4. Parse YAML -> serde_yaml::Value serde_yaml │
87+
│ 5. Validate headless compatibility no "ask" in permission.task │
88+
│ 6. Deserialize Value -> RawFrontmatter serde_yaml │
89+
│ 7. Build AgentConfig from_raw │
90+
└──────────────────────────────┬──────────────────────────────────────┘
91+
92+
93+
┌─────────────┐
94+
│ AgentConfig │ name, mode, model, permissions, prompt
95+
└──────┬──────┘
96+
97+
98+
┌──────────────┐
99+
│ AgentCatalog │ AHashMap<String, AgentConfig>
100+
└──────────────┘ last-insert-wins on duplicate names
101+
```
102+
103+
### File Discovery
104+
105+
`AgentLoader::add_directory` walks the given root with `.gitignore` support
106+
(`ignore` crate), keeping only files matching:
107+
108+
```text
109+
agent/**/*.md
110+
agents/**/*.md
111+
```
112+
113+
Agent name is derived from the relative path by stripping the `agent/` or
114+
`agents/` prefix and `.md` suffix:
115+
116+
```text
117+
agent/code-reviewer.md -> "code-reviewer"
118+
agents/nested/deep.md -> "nested/deep"
119+
```
120+
121+
Frontmatter `name:` overrides the derived name when present.
122+
123+
### YAML Preprocessor
124+
125+
The preprocessor (`parser/preprocessor.rs`) rewrites lines where an unquoted
126+
value contains a bare `:` - a YAML ambiguity. For example:
127+
128+
```yaml
129+
model: provider/model:tag
130+
```
131+
132+
becomes:
133+
134+
```yaml
135+
model: |-
136+
provider/model:tag
137+
```
138+
139+
Already-safe forms (quoted, block scalars, flow syntax, comments, indented
140+
continuation lines) are left untouched.
141+
142+
## Phase 2: Building
143+
144+
Once you have an [`AgentCatalog`], you assemble an [`AgentRuntime`] that holds
145+
everything needed to run agents: the catalog, default settings, Task delegation
146+
settings, and the available tools.
147+
148+
### Building the Runtime
149+
150+
```text
151+
AgentRuntimeBuilder::new()
152+
.catalog(catalog)
153+
.defaults(AgentDefaults { model, temperature, top_p })
154+
.max_task_depth(n) // or .task_settings(TaskSettings::with_max_depth(n))
155+
.tools(vec![...]) // or default_tools() if omitted
156+
.build()
157+
158+
159+
┌──────────────┐
160+
│ AgentRuntime │ catalog + defaults + task_settings + tools
161+
└──────────────┘
162+
```
163+
164+
`AgentRuntime` is `Clone`, `Send`, `Sync`, and stores no async state.
165+
166+
### AgentDefaults
167+
168+
Fallback settings used when an individual agent doesn't specify them:
169+
170+
| Field | Meaning |
171+
| ------------- | ---------------------------------- |
172+
| `model` | Default `provider/model-id` |
173+
| `temperature` | Default sampling temperature |
174+
| `top_p` | Default nucleus sampling parameter |
175+
176+
### Tool Catalog
177+
178+
`default_tools()` returns 10 entries:
179+
180+
| Kind | Tool name |
181+
| --------- | ----------- |
182+
| Read | `read` |
183+
| Write | `write` |
184+
| Edit | `edit` |
185+
| Glob | `glob` |
186+
| Grep | `grep` |
187+
| Bash | `bash` |
188+
| WebFetch | `webfetch` |
189+
| TodoRead | `todoread` |
190+
| TodoWrite | `todowrite` |
191+
| Task | `task` |
192+
193+
### Model Resolution
194+
195+
When an agent needs to run, you resolve which model it should use:
196+
197+
```text
198+
resolve_model_with_catalog(model_catalog, defaults, agent)
199+
200+
│ 1. agent.model set? -> parse "provider/model-id"
201+
│ └─ malformed? -> MalformedModelIdentifier ("agent override")
202+
203+
│ 2. defaults.model? -> parse "provider/model-id"
204+
│ └─ malformed? -> MalformedModelIdentifier ("runtime default")
205+
206+
│ 3. neither set? -> MissingEffectiveModel
207+
208+
│ 4. provider in catalog? -> no -> UnknownProvider
209+
│ 5. model in catalog? -> no -> UnknownModel
210+
211+
┌────────────────┐
212+
│ ResolvedModel │ provider: Box<str>, model: Box<str>
213+
└────────────────┘
214+
```
215+
216+
Precedence: **agent override** wins over **runtime default**.
217+
218+
A malformed agent override does **not** fall back to the default - it errors.
219+
220+
## Phase 3: Runtime Usage
221+
222+
With a built [`AgentRuntime`], you can query what an agent is allowed to do
223+
and which other agents it can delegate to.
224+
225+
### Permission Filtering
226+
227+
Agent frontmatter may include a `permission` map:
228+
229+
```yaml
230+
permission:
231+
read: allow
232+
bash: deny
233+
task:
234+
"*": deny
235+
"review-*": allow
236+
```
237+
238+
`RulesetExt::from_permission_config` converts this into a `Ruleset` (from
239+
`llm-coding-tools-core::permissions`):
240+
241+
```text
242+
PermissionRule::Action(Allow) -> Rule { key: "read", pattern: "*", action: Allow }
243+
PermissionRule::Action(Deny) -> Rule { key: "bash", pattern: "*", action: Deny }
244+
PermissionRule::Pattern({ .. }) -> Rule { key: "task", pattern: "*", action: Deny }
245+
Rule { key: "task", pattern: "review-*", action: Allow }
246+
```
247+
248+
Evaluation uses **last-match-wins** semantics.
249+
250+
### Allowed Tools
251+
252+
`AgentRuntime::allowed_tools(caller_name)` filters the tool catalog:
253+
254+
```text
255+
runtime.tools()
256+
257+
│ for each entry:
258+
│ Task -> only if >= 1 callable subagent target exists
259+
│ other -> is_allowed(entry.name, "*") per Ruleset
260+
261+
Vec<ToolCatalogEntry>
262+
```
263+
264+
### Callable Targets
265+
266+
`callable_targets(catalog, caller_name)` returns agents the caller may delegate
267+
to via the Task tool:
268+
269+
```text
270+
all agents (sorted by name)
271+
272+
│ filter:
273+
│ mode != Primary (only All + Subagent are callable)
274+
│ AND
275+
│ if caller defines permission.task:
276+
│ ruleset.is_allowed("task", target.name)
277+
│ else (no explicit permission.task):
278+
│ default-allow all non-Primary targets
279+
280+
Vec<&AgentConfig>
281+
```
282+
283+
OpenCode compatibility: omitting `permission.task` defaults to allowing
284+
delegation to all non-Primary agents.
285+
286+
## Reference
287+
288+
### Error Model
289+
290+
```text
291+
AgentLoadError
292+
├── Io { path, source } file read / directory scan failure
293+
├── Parse { path, source } frontmatter YAML parse failure
294+
│ source: AgentParseError
295+
│ ├── MissingFrontmatter
296+
│ ├── InvalidYaml { message }
297+
│ └── SchemaValidation { message }
298+
└── SchemaValidation { path, message } invalid mode, empty name, "ask" permission
299+
300+
ModelResolutionError
301+
├── MalformedModelIdentifier missing "/" or empty segments
302+
├── MissingEffectiveModel neither agent nor default specifies a model
303+
├── UnknownProvider provider not in ModelCatalog
304+
└── UnknownModel provider found but model not listed
305+
```
306+
307+
All loader errors carry an optional `path: Option<PathBuf>` (`None` for
308+
in-memory sources, displayed as `<memory>`).
309+
310+
### Testing
311+
312+
- `tempfile` + `indoc` fixtures for file/directory loading tests.
313+
- No external services required.
314+
- Parser benchmarks in `benches/parser.rs` (Criterion).
315+
316+
### File Map
317+
318+
```text
319+
llm-coding-tools-agents
320+
├── lib.rs crate root, re-exports
321+
├── catalog.rs AgentCatalog - in-memory name -> AgentConfig store
322+
├── extensions.rs RulesetExt - builds Ruleset from frontmatter permissions
323+
├── loader.rs AgentLoader - scans dirs/files/strings -> AgentCatalog
324+
├── parser/
325+
│ ├── mod.rs parse_agent() - YAML frontmatter + body extractor
326+
│ └── preprocessor.rs YAML preprocessor - rewrites colon-containing values
327+
├── types/
328+
│ ├── mod.rs re-exports
329+
│ ├── config.rs AgentConfig, AgentMode, PermissionRule, parse_model_parts
330+
│ └── error.rs AgentLoadError, AgentLoadResult
331+
├── runtime/
332+
│ ├── mod.rs module root, re-exports
333+
│ ├── state.rs AgentRuntime, AgentDefaults
334+
│ ├── builder.rs AgentRuntimeBuilder
335+
│ ├── model.rs resolve_model_with_catalog(), ResolvedModel, ModelResolutionError
336+
│ ├── task.rs callable_targets(), summarize_callable_targets(), allowed_tools()
337+
│ └── tool_catalog.rs ToolCatalogEntry, ToolCatalogKind, default_tools()
338+
└── benches/
339+
└── parser.rs Criterion benchmarks for frontmatter parsing
340+
```

src/llm-coding-tools-agents/README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,3 +91,5 @@ while settings with no runtime effect are accepted and ignored:
9191
([docs](https://opencode.ai/docs/permissions#what-ask-does)).
9292
- [`hidden`](https://opencode.ai/docs/agents#hidden) is accepted for
9393
compatibility, but ignored at runtime.
94+
95+
For the internal architecture, see [ARCHITECTURE.md](https://github.com/Sewer56/llm-coding-tools/blob/main/src/llm-coding-tools-agents/ARCHITECTURE.md).

0 commit comments

Comments
 (0)