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
//! Post-edit LSP diagnostics hooks for engine tool execution.
//!
//! The turn loop only needs to ask "did a successful edit produce diagnostics?"
//! This module owns the synthetic diagnostic message injection so the top-level
//! engine module stays focused on session orchestration. Path extraction lives
//! in `zagens-core::engine::lsp_edit_paths`.
//!
//! M3 routes the two call sites through the
//! [`LspHost`](zagens_core::engine::hosts::LspHost) trait so the future
//! core-side Engine struct can hold `Arc<dyn LspHost>` instead of
//! `Arc<LspManager>`.
use zagens_core::engine::edited_paths_for_tool;
use zagens_core::engine::hosts::LspHost;
use super::*;
impl Engine {
/// #136: post-edit hook. Inspects the tool name + input, derives the
/// edited file path, and asks the LSP manager for diagnostics. The
/// rendered block is queued in `pending_lsp_blocks` and flushed to the
/// session message stream just before the next API request. Failure is
/// silent by design — a missing/crashing LSP server must never block
/// the agent.
pub(super) async fn run_post_edit_lsp_hook(
&mut self,
tool_name: &str,
tool_input: &serde_json::Value,
) {
let host: &dyn LspHost = self.lsp.as_ref();
if !host.enabled() {
return;
}
let paths = edited_paths_for_tool(tool_name, tool_input);
for path in paths {
let absolute = if path.is_absolute() {
path.clone()
} else {
self.session.workspace.join(&path)
};
let seq = self.turn_counter;
let block = {
let host: &dyn LspHost = self.0.lsp.as_ref();
host.diagnostics_for(&absolute, seq).await
};
if let Some(block) = block {
self.0.pending_lsp_blocks.push(block);
}
}
}
/// Drain `pending_lsp_blocks` into a single synthetic user message so the
/// model sees the diagnostics on its next request. Skips when nothing is
/// pending. The message uses the standard `text` content block shape
/// (the same shape as the post-tool steer messages) so we don't need to
/// invent a new envelope.
pub(super) async fn flush_pending_lsp_diagnostics(&mut self) {
if self.effect_replay_anchor_only() {
self.pending_lsp_blocks.clear();
return;
}
if self.pending_lsp_blocks.is_empty() {
return;
}
let blocks = std::mem::take(&mut self.pending_lsp_blocks);
let rendered = crate::lsp::render_blocks(&blocks);
if rendered.is_empty() {
return;
}
self.add_session_message(Message {
role: "user".to_string(),
content: vec![ContentBlock::Text {
text: rendered,
cache_control: None,
}],
})
.await;
}
}