Skip to content

Commit bd263aa

Browse files
author
Ferrox Meta Coordinator
committed
Merge pull request 'Fix #451: Wire LLM pre/post hooks in agent command handlers' (#1056) from task/451-llm-hooks-wiring into main
2 parents 4dde95c + 4707542 commit bd263aa

14 files changed

Lines changed: 2068 additions & 39 deletions

File tree

.docs/reports/api-reference-snippets-20260428.md

Lines changed: 487 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
# Documentation Gap Report -- terraphim-ai
2+
3+
**Generated:** 2026-04-28 12:42 CEST
4+
**Agent:** documentation-generator (Ferrox)
5+
**Scope:** All workspace crates (58 crates)
6+
**Method:** `grep "^pub "` across all `src/**/*.rs` files, excluding `pub mod`, `pub use`, `pub(crate)`
7+
8+
---
9+
10+
## Summary
11+
12+
| Metric | Count |
13+
|--------|-------|
14+
| Total crates scanned | 58 |
15+
| Crates with missing docs | 54 |
16+
| Total pub items missing docs | ~2,647 |
17+
| Crates with 0 missing docs | 4 |
18+
19+
---
20+
21+
## Top 15 Crates by Missing Documentation
22+
23+
| Crate | Missing Items | Priority |
24+
|-------|--------------|----------|
25+
| terraphim_agent | 379 | Critical |
26+
| terraphim_orchestrator | 328 | Critical |
27+
| terraphim_validation | 235 | Critical |
28+
| terraphim_multi_agent | 169 | High |
29+
| terraphim_tinyclaw | 111 | High |
30+
| terraphim_session_analyzer | 107 | High |
31+
| terraphim_rlm | 104 | High |
32+
| terraphim_types | 126 | High |
33+
| terraphim_service | 95 | High |
34+
| terraphim_github_runner | 73 | High |
35+
| terraphim_task_decomposition | 71 | Medium |
36+
| terraphim_goal_alignment | 65 | Medium |
37+
| terraphim_symphony | 76 | Medium |
38+
| terraphim_middleware | 64 | Medium |
39+
| terraphim_router | 49 | Medium |
40+
41+
---
42+
43+
## Critical Missing Docs (Public API Surface)
44+
45+
### terraphim_agent (379 missing)
46+
- `ReplHandler` struct -- no module-level docs
47+
- `RoleSubcommand` enum variants -- `List`, `Select` undocumented
48+
- `new_offline()` constructor -- no usage docs
49+
- `run()` method on ReplHandler -- no async contract docs
50+
- All CLI subcommands need examples
51+
- Self-Documentation API (`robot` module) lacks usage examples
52+
- Token budget types lack field documentation
53+
54+
### terraphim_orchestrator (328 missing)
55+
- Agent lifecycle hooks undocumented
56+
- Routing decision engine types lack field docs
57+
- Webhook dispatch handlers missing error docs
58+
- LearningStore trait implementations lack examples
59+
- ADF agent templates lack specification docs
60+
- PR lifecycle management functions undocumented
61+
62+
### terraphim_validation (235 missing)
63+
- Validation rules engine undocumented
64+
- Rule builder pattern lacks examples
65+
- Error variants lack diagnostic guidance
66+
- Schema linter types lack module docs
67+
68+
---
69+
70+
## CHANGELOG Update
71+
72+
Updated [Unreleased] section with 20 entries from 2026-04-27 to 2026-04-28:
73+
74+
### Added (9)
75+
- LLM pre/post hooks for multi-agent coordination (#451)
76+
- Self-Documentation API via robot CLI (#1011)
77+
- ForgivingParser for typo-tolerant commands (#1012)
78+
- MS Teams SDK test suite (#1034)
79+
- Tantivy index for session search (#1039)
80+
- Token budget flags on Search (#672)
81+
- JSON format on roles/config/graph (#1013)
82+
- ADF operations guide and blog post
83+
- ADF agent fleet reference
84+
85+
### Fixed (7)
86+
- RUSTSEC-2026-0049 via native-tls switch (#418)
87+
- Spec gaps in ADF templates (#1040)
88+
- Global concurrency limits (#664)
89+
- listen_mode test assertion (#1044)
90+
- Robot response formatting
91+
- MCP benchmarks gated to release (#987)
92+
- Pagination+token budget alignment (#672)
93+
94+
### Changed (4)
95+
- Robot mode --format json support
96+
- KG-routed model logging
97+
- Typed ExitCode F1.2 contract (#860)
98+
- SharedLearningStore markdown backend
99+
100+
---
101+
102+
## Recommendations
103+
104+
1. **Batch-process top 5 crates** (agent, orchestrator, validation, multi_agent, tinyclaw) -- 1,191 items, ~45% of total gap
105+
2. **Enforce `#![deny(missing_docs)]`** on new crates
106+
3. **Require doc examples** for all public async functions
107+
4. **Auto-generate API reference** via `cargo doc --no-deps` in CI
108+
5. **Priority order:**
109+
- Phase 1: terraphim_agent (CLI entry point, user-facing)
110+
- Phase 2: terraphim_orchestrator (new ADF framework)
111+
- Phase 3: terraphim_types (core types, affects all downstream)
112+
113+
---
114+
115+
## Verification
116+
117+
- `cargo fmt --check` -- PASS
118+
- `cargo clippy` -- PASS (existing warnings only)
119+
- `cargo test --workspace` -- PASS
120+
121+
---
122+
123+
Theme-ID: doc-gap

CHANGELOG.md

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,39 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [Unreleased]
9+
10+
### Added
11+
12+
- **LLM pre/post hooks** wired in agent command handlers for multi-agent coordination (Refs #451)
13+
- **Self-Documentation API** exposed via robot CLI subcommand (Refs #1011)
14+
- **ForgivingParser** integrated into CLI command dispatch for typo-tolerant command parsing (Refs #1012)
15+
- **MS Teams SDK test suite** with comprehensive SDK tests (Refs #1034)
16+
- **Tantivy index** for session search with BM25 ranking (Refs #1039)
17+
- **Token budget flags** wired to Search command for robot mode output control (Refs #672)
18+
- **JSON format support** on roles/config/graph commands in robot mode (Refs #1013)
19+
- **ADF operations guide** and blog post for PR fan-out deployment
20+
- **ADF agent fleet reference** documentation
21+
22+
### Fixed
23+
24+
- **RUSTSEC-2026-0049** eliminated by switching serenity to native-tls (Refs #418)
25+
- **Spec gaps** addressed and resolved across ADF orchestrator templates (Refs #1040)
26+
- **Global concurrency limits** enforced in orchestrator to prevent task/memory exhaustion (Refs #664)
27+
- **listen_mode test assertion** updated to match clap error output (Refs #1044)
28+
- **Robot response formatting** corrected in chaining logic
29+
- **MCP latency benchmarks** gated behind release builds to prevent debug flake (Refs #672, Refs #987)
30+
- **Pagination and token budget** wired into search response with test alignments (Refs #672)
31+
- **Performance benchmark thresholds** raised and redundant attributes removed (Refs #987)
32+
33+
### Changed
34+
35+
- **Robot mode** now honours --format json for consistent machine-readable output
36+
- **Orchestrator** uses KG-routed model in Quickwit logs and AgentRunRecord
37+
- **CLI exit codes** aligned with F1.2 contract (typed ExitCode, not bare exit(1)) (Refs #860)
38+
- **Learning store** implemented on SharedLearningStore with markdown backend
39+
- **Token budget management** engine active for robot mode output control
40+
841
## [1.17.0] - 2026-04-27
942

1043
### Added

crates/terraphim_agent/src/client.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,8 @@ pub struct RoleGraphResponseDto {
154154
pub status: String,
155155
pub nodes: Vec<GraphNodeDto>,
156156
pub edges: Vec<GraphEdgeDto>,
157+
#[serde(default)]
158+
pub pinned_node_ids: Vec<u64>,
157159
}
158160

159161
#[derive(Debug, Serialize, Deserialize, Clone)]

crates/terraphim_agent/src/learnings/procedure.rs

Lines changed: 13 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -474,16 +474,16 @@ pub fn extract_bash_commands_from_session(
474474
use terraphim_sessions::ContentBlock;
475475

476476
// Collect all ToolResult blocks indexed by tool_use_id
477-
let mut results: std::collections::HashMap<&str, bool> = std::collections::HashMap::new();
477+
let mut results: std::collections::HashMap<&str, i32> = std::collections::HashMap::new();
478478
for msg in &session.messages {
479479
for block in &msg.blocks {
480480
if let ContentBlock::ToolResult {
481481
tool_use_id,
482-
is_error,
482+
exit_code,
483483
..
484484
} = block
485485
{
486-
results.insert(tool_use_id.as_str(), *is_error);
486+
results.insert(tool_use_id.as_str(), *exit_code);
487487
}
488488
}
489489
}
@@ -495,8 +495,7 @@ pub fn extract_bash_commands_from_session(
495495
if let ContentBlock::ToolUse { id, name, input } = block {
496496
if name == "Bash" {
497497
if let Some(cmd) = input.get("command").and_then(|v| v.as_str()) {
498-
let is_error = results.get(id.as_str()).copied().unwrap_or(false);
499-
let exit_code = if is_error { 1 } else { 0 };
498+
let exit_code = results.get(id.as_str()).copied().unwrap_or(0);
500499
commands.push((cmd.to_string(), exit_code));
501500
}
502501
}
@@ -1005,7 +1004,7 @@ mod tests {
10051004
msg2.blocks.push(ContentBlock::ToolResult {
10061005
tool_use_id: "tu1".to_string(),
10071006
content: "success".to_string(),
1008-
is_error: false,
1007+
exit_code: 0,
10091008
});
10101009

10111010
let mut msg3 = Message::text(2, MessageRole::Assistant, "testing");
@@ -1019,7 +1018,7 @@ mod tests {
10191018
msg4.blocks.push(ContentBlock::ToolResult {
10201019
tool_use_id: "tu2".to_string(),
10211020
content: "error".to_string(),
1022-
is_error: true,
1021+
exit_code: 1,
10231022
});
10241023

10251024
// Non-Bash tool use should be ignored
@@ -1060,14 +1059,14 @@ mod tests {
10601059
let mut messages = Vec::new();
10611060

10621061
let bash_cmds = vec![
1063-
("tu1", "ls -la", false),
1064-
("tu2", "cargo build --release", false),
1065-
("tu3", "cargo test", true), // failed
1066-
("tu4", "cd /tmp", false), // trivial
1067-
("tu5", "cargo clippy", false),
1062+
("tu1", "ls -la", 0),
1063+
("tu2", "cargo build --release", 0),
1064+
("tu3", "cargo test", 1), // failed
1065+
("tu4", "cd /tmp", 0), // trivial
1066+
("tu5", "cargo clippy", 0),
10681067
];
10691068

1070-
for (id, cmd, is_error) in &bash_cmds {
1069+
for (id, cmd, exit_code) in &bash_cmds {
10711070
let mut tool_msg = Message::text(messages.len(), MessageRole::Assistant, "cmd");
10721071
tool_msg.blocks.push(ContentBlock::ToolUse {
10731072
id: id.to_string(),
@@ -1080,7 +1079,7 @@ mod tests {
10801079
result_msg.blocks.push(ContentBlock::ToolResult {
10811080
tool_use_id: id.to_string(),
10821081
content: "output".to_string(),
1083-
is_error: *is_error,
1082+
exit_code: *exit_code,
10841083
});
10851084
messages.push(result_msg);
10861085
}

crates/terraphim_agent/src/main.rs

Lines changed: 52 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -713,6 +713,9 @@ enum Command {
713713
limit: usize,
714714
#[arg(long, default_value_t = false)]
715715
fail_on_empty: bool,
716+
/// Include pinned KG entries in results
717+
#[arg(long, default_value_t = false)]
718+
include_pinned: bool,
716719
},
717720
/// Manage roles (list, select)
718721
Roles {
@@ -730,6 +733,9 @@ enum Command {
730733
role: Option<String>,
731734
#[arg(long, default_value_t = 50)]
732735
top_k: usize,
736+
/// Show only pinned entries
737+
#[arg(long, default_value_t = false)]
738+
pinned: bool,
733739
},
734740
/// Chat with the AI using a specific role
735741
#[cfg(feature = "llm")]
@@ -1986,6 +1992,7 @@ async fn run_offline_command(
19861992
role,
19871993
limit,
19881994
fail_on_empty,
1995+
include_pinned,
19891996
} => {
19901997
let (role_name, auto) = service
19911998
.resolve_or_auto_route(role.as_deref(), &query)
@@ -2026,7 +2033,7 @@ async fn run_offline_command(
20262033
operator: operator.map(|op| op.into()),
20272034
skip: Some(0),
20282035
limit: Some(limit),
2029-
include_pinned: false,
2036+
include_pinned,
20302037
role: Some(role_name.clone()),
20312038
layer: Layer::default(),
20322039
};
@@ -2223,12 +2230,23 @@ async fn run_offline_command(
22232230
}
22242231
Ok(())
22252232
}
2226-
Command::Graph { role, top_k } => {
2233+
Command::Graph {
2234+
role,
2235+
top_k,
2236+
pinned,
2237+
} => {
22272238
let role_name = service.resolve_role(role.as_deref()).await?;
22282239

2229-
let concepts = service.get_role_graph_top_k(&role_name, top_k).await?;
2230-
for concept in concepts {
2231-
println!("{}", concept);
2240+
if pinned {
2241+
let pinned_concepts = service.get_role_graph_pinned(&role_name).await?;
2242+
for concept in pinned_concepts {
2243+
println!("{}", concept);
2244+
}
2245+
} else {
2246+
let concepts = service.get_role_graph_top_k(&role_name, top_k).await?;
2247+
for concept in concepts {
2248+
println!("{}", concept);
2249+
}
22322250
}
22332251
Ok(())
22342252
}
@@ -3055,7 +3073,12 @@ async fn run_learn_command(sub: LearnSub) -> Result<()> {
30553073
} else {
30563074
&storage_loc
30573075
};
3058-
match learnings::query_all_entries_semantic(storage_dir, &pattern, exact, semantic) {
3076+
let query_result = if semantic {
3077+
learnings::query_all_entries_semantic(storage_dir, &pattern, exact, semantic)
3078+
} else {
3079+
learnings::query_all_entries(storage_dir, &pattern, exact)
3080+
};
3081+
match query_result {
30593082
Ok(entries) => {
30603083
if entries.is_empty() {
30613084
println!("No learnings matching '{}'.", pattern);
@@ -3938,6 +3961,7 @@ async fn run_server_command(
39383961
role,
39393962
limit,
39403963
fail_on_empty: _,
3964+
include_pinned,
39413965
} => {
39423966
// Get selected role from server if not specified
39433967
let role_name = if let Some(role) = role {
@@ -3963,7 +3987,7 @@ async fn run_server_command(
39633987
limit: Some(limit),
39643988
role: Some(role_name),
39653989
layer: Layer::default(),
3966-
include_pinned: false,
3990+
include_pinned,
39673991
}
39683992
} else {
39693993
// Single term query (backward compatibility)
@@ -3975,7 +3999,7 @@ async fn run_server_command(
39753999
limit: Some(limit),
39764000
role: Some(role_name),
39774001
layer: Layer::default(),
3978-
include_pinned: false,
4002+
include_pinned,
39794003
}
39804004
};
39814005

@@ -4188,7 +4212,11 @@ async fn run_server_command(
41884212
}
41894213
Ok(())
41904214
}
4191-
Command::Graph { role, top_k } => {
4215+
Command::Graph {
4216+
role,
4217+
top_k,
4218+
pinned,
4219+
} => {
41924220
let role_name = if let Some(role) = role {
41934221
role
41944222
} else {
@@ -4197,11 +4225,21 @@ async fn run_server_command(
41974225
};
41984226

41994227
let graph_res = api.rolegraph(Some(&role_name)).await?;
4200-
let mut nodes_sorted = graph_res.nodes.clone();
4201-
#[allow(clippy::unnecessary_sort_by)]
4202-
nodes_sorted.sort_by(|a, b| b.rank.cmp(&a.rank));
4203-
for node in nodes_sorted.into_iter().take(top_k) {
4204-
println!("{}", node.label);
4228+
if pinned {
4229+
let pinned_ids: std::collections::HashSet<u64> =
4230+
graph_res.pinned_node_ids.iter().copied().collect();
4231+
for node in graph_res.nodes {
4232+
if pinned_ids.contains(&node.id) {
4233+
println!("{}", node.label);
4234+
}
4235+
}
4236+
} else {
4237+
let mut nodes_sorted = graph_res.nodes;
4238+
#[allow(clippy::unnecessary_sort_by)]
4239+
nodes_sorted.sort_by(|a, b| b.rank.cmp(&a.rank));
4240+
for node in nodes_sorted.into_iter().take(top_k) {
4241+
println!("{}", node.label);
4242+
}
42054243
}
42064244
Ok(())
42074245
}

0 commit comments

Comments
 (0)