refactor(prompt): ♻️ Overhaul prompt injection with modular layered architecture#222
Conversation
… cache marker arbitration, surface extension traits, schema version rules, and template front-matter
…rchitecture Introduce a new prompt/ submodule architecture that replaces monolithic prompt assembly with composable, layered components: New core modules: - composer: Orchestrates prompt assembly from registered sections - registry: Manages section provider registration and resolution - layer: Supports layered prompts (base, override, compaction, etc.) - templates: Template loading, rendering, and front-matter parsing - template_sources: Bespoke per-role template source abstraction - renderer: Markdown/XML renderers for prompt surfaces - legacy_adapter: Backward-compatible bridge for existing callers New feature modules: - budget, clock, cache_marker, error_codes, exec_policy, feature_set, redactor, run_mode, runtime_message, section_id, section_source, signals, surface, surface_extensions, inheritance, emergency_fallback, active_goal_source Template files extracted to prompt/templates/ covering roles, compaction, subagents, emergency fallback, run modes, and contracts. Callers updated: agent_session, agent_run_summary, agent_run_title, subagent orchestrator, and related tests.
AI Code Review SummaryPR: #222 (refactor(prompt): ♻️ Overhaul prompt injection with modular layered architecture) Overall AssessmentDetected 10 actionable findings, prioritize CRITICAL/HIGH before merge. Major Findings by Severity
Actionable Suggestions
Potential Risks
Test Suggestions
File-Level Coverage Notes
Inline Downgraded Items (processed but not inline)
Coverage Status
Uncovered list:
No-patch covered list:
Runtime/Budget
|
|
|
||
| // EmergencyFallback: if no sections were produced, inject hard-coded fallback | ||
| let fallback_used = bodies.is_empty(); | ||
| if fallback_used { |
This comment was marked as outdated.
This comment was marked as outdated.
Sorry, something went wrong.
|
|
||
| /// Semantic ordering hint for sections within the same layer. | ||
| /// Replaces the old bare `u16` with anchor-relative or absolute positioning. | ||
| #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] |
This comment was marked as outdated.
This comment was marked as outdated.
Sorry, something went wrong.
| let renderer = cx.renderer.as_ref(); | ||
|
|
||
| for (spec, layer, body, warn, source_elapsed) in &kept { | ||
| let rendered = renderer.render_section(&spec.title, &body.markdown); |
This comment was marked as outdated.
This comment was marked as outdated.
Sorry, something went wrong.
| Ok(v) => v, | ||
| Err(e) => { | ||
| return Ok(SectionOutcome::SoftFailed { | ||
| code: "settings.approval_policy.load_failed", |
This comment was marked as outdated.
This comment was marked as outdated.
Sorry, something went wrong.
| .filter(|(_, body)| !body.trim().is_empty()) | ||
| .collect() | ||
| } | ||
| // Phase 2b: legacy string-parsing functions removed. |
This comment was marked as outdated.
This comment was marked as outdated.
Sorry, something went wrong.
…late-based SubagentBodySource Phase 7 of the prompt-injection refactor: subagent identity, persona, and shell tooling guide are no longer hardcoded as separate strings in orchestrator or SubagentProfile::system_prompt(). They are now rendered by SubagentBodySource via the Composer from template files (explore.md, review.md) or from the user-provided system_prompt for custom subagents. Key changes: - Replace LegacyCustomSubagentBodySource with SubagentBodySource - Rename SectionId::CustomSubagentBody to SectionId::SubagentBody - Remove inline helper_shell_tooling_guide() and its test coverage - Add comprehensive tests for PromptBudget scaling and registry section lists - Bump registry schema version from 1 to 2
…lementations and add anchored ordering
…section enforcement Remove the compiler-inlined `EmergencyFallback` system that served as a last-resort prompt when all sections failed. This simplifies `Composer::build` by eliminating: - Per-surface fallback text files and `emergency_fallback_text()` lookup - Critical section lists that would upgrade `SoftFailed` to `FatalError` - `SectionAudit::fallback_used` flag and `SectionWarning::EmergencyFallback` - `SurfaceExtension::critical_sections()` method and its lint tests - Associated tracing metrics (`prompt.fallback.emergency_total`) The system trade-off (hardening against total prompt failure) added complexity and was never triggered in production. Removing it reduces maintenance burden and makes the prompt pipeline easier to evolve. Soft failures are already surfaced via warnings and logs.
…ation scaffolding
… source and add key validation linter
… template version validation
…irectory Move individual SectionSource implementations from flat module files into a dedicated `sources/` module directory. This improves code organization and maintainability by grouping related source implementations together and reducing the number of top-level modules. The refactoring includes: - Creating `sources/mod.rs` to re-export all source implementations - Moving ActiveGoalSource, ActivePlanSource, CompactionContractSource, and other sources into separate files within the new directory - Removing `template_sources.rs` which contained implementations now distributed to individual source files - Updating `registry.rs` and `mod.rs` imports to use the new module structure - Adding corresponding test file `source_tests.rs` for idempotency and determinism verification
…hot tests Replace hardcoded handoff prompt strings in `agent_run_summary.rs` with template files (`templates/handoff/with_plan.tpl.md` and `without_plan.tpl.md`) that include YAML front-matter for metadata. Remove the backward-compat `providers.rs` module and update `agent_session_tests.rs` to load template bodies directly. Add comprehensive snapshot tests using `insta` to capture the full composed prompt text for all prompt surfaces (MainAgent, Subagent, Compaction, Title). These snapshots serve as an audit trail against accidental prompt drift. Add `insta` as a dev dependency for snapshot assertions.
…es and add cache marker arbiter Replace generic `TemplateSource<F>` instantiations in `registry.rs` for the Role, BehavioralGuidelines, FinalResponseStructure, and ShellToolingGuide sections with first-class `SectionSource` implementations (`RoleSource`, `BehavioralGuidelinesSource`, `FinalResponseStructureSource`, `ShellToolingGuideSource`). Each new source performs explicit front-matter version validation and template rendering, improving error handling and testability. Rename `SectionId::SubagentBody` to `SectionId::CustomSubagentBody` to clarify its purpose (applies only to custom subagents, not built-in Explore/Review). Update all usages in `inheritance.rs`, `budget.rs`, and `registry.rs` accordingly. Introduce a `cache_arbiter` field in `AgentSessionSpec` and thread a `DefaultCacheMarkerArbiter` through `build_system_prompt` to enable per-request cache marker tracking for future LLM caching support. Change `PromptBudget::for_model` to accept a `ModelTarget` reference instead of a raw `usize`, deriving the context window from the target model. Adjust all affected tests and snapshot files to match the new output (minor whitespace/byte differences from the dedicated source renderers). Add a new snapshot test `snapshot_subagent_custom` to cover the custom subagent surface. These changes unify section construction, prepare the foundation for prompt caching, and eliminate inline generic template handling in the registry.
| "- Respond in {language} unless the user explicitly asks for a different language." | ||
| )); | ||
| } | ||
| pub(crate) async fn build_compact_summary_system_prompt(response_language: Option<&str>) -> String { |
This comment was marked as outdated.
This comment was marked as outdated.
Sorry, something went wrong.
| Ok(normalize_generated_title(&message.text_content())) | ||
| } | ||
|
|
||
| /// Build the Title surface system prompt via Composer (Phase 6). |
This comment was marked as outdated.
This comment was marked as outdated.
Sorry, something went wrong.
|
|
||
| [dev-dependencies] | ||
| tempfile = "3" | ||
| insta = { version = "1", features = ["yaml"] } |
This comment was marked as outdated.
This comment was marked as outdated.
Sorry, something went wrong.
| } | ||
|
|
||
| /// Strip YAML front-matter and return the template body. | ||
| fn strip_front_matter(tpl: &str) -> &str { |
This comment was marked as outdated.
This comment was marked as outdated.
Sorry, something went wrong.
| use crate::persistence::repo::provider_repo; | ||
|
|
||
| /// Strip YAML front-matter (delimited by ---) from a template string. | ||
| fn strip_front_matter(tpl: &str) -> &str { |
This comment was marked as outdated.
This comment was marked as outdated.
Sorry, something went wrong.
| run_mode, | ||
| helper_profile: None, | ||
| custom_subagent_slug: None, | ||
| target_model: ModelTarget::AnthropicClaude { |
This comment was marked as outdated.
This comment was marked as outdated.
Sorry, something went wrong.
| supports_cache_control: true, | ||
| }, | ||
| renderer: Arc::new(MarkdownRenderer), | ||
| clock: Arc::new(FixedClock::new(chrono::Utc::now())), |
This comment was marked as outdated.
This comment was marked as outdated.
Sorry, something went wrong.
| @@ -1,555 +0,0 @@ | |||
| use std::path::Path; | |||
This comment was marked as outdated.
This comment was marked as outdated.
Sorry, something went wrong.
| use super::super::super::run_mode::RunMode; | ||
| use super::super::super::section_source::SectionSource; | ||
| use super::super::super::signals::SignalCache; | ||
| use super::super::{RunModeSource, SystemEnvironmentSource}; |
This comment was marked as outdated.
This comment was marked as outdated.
Sorry, something went wrong.
|
|
||
| let cx_plan = BuildCx { | ||
| run_mode: RunMode::Plan, | ||
| ..base_cx.clone() |
This comment was marked as outdated.
This comment was marked as outdated.
Sorry, something went wrong.
| /// The helper-specific tail (shell-tooling guide + helper-profile body) | ||
| /// is appended after the composed prompt; full migration to template-backed | ||
| /// sources is future work. | ||
| async fn build_helper_system_prompt( |
This comment was marked as outdated.
This comment was marked as outdated.
Sorry, something went wrong.
| })?; | ||
|
|
||
| if tmpl.version != self.spec_version { | ||
| return Err(FatalError::new( |
This comment was marked as outdated.
This comment was marked as outdated.
Sorry, something went wrong.
| @@ -0,0 +1,68 @@ | |||
| use async_trait::async_trait; | |||
This comment was marked as outdated.
This comment was marked as outdated.
Sorry, something went wrong.
| @@ -0,0 +1,64 @@ | |||
| use async_trait::async_trait; | |||
This comment was marked as outdated.
This comment was marked as outdated.
Sorry, something went wrong.
|
|
||
| [dev-dependencies] | ||
| tempfile = "3" | ||
| insta = { version = "1", features = ["yaml"] } |
This comment was marked as outdated.
This comment was marked as outdated.
Sorry, something went wrong.
| })?; | ||
|
|
||
| // Cow wraps the const &'static str — clone if borrowed | ||
| let _ = Cow::Borrowed(rel_path); |
This comment was marked as outdated.
This comment was marked as outdated.
Sorry, something went wrong.
| _ => ProfileResponseStyle::Balanced, | ||
| } | ||
| } | ||
|
|
This comment was marked as outdated.
This comment was marked as outdated.
Sorry, something went wrong.
| @@ -0,0 +1,76 @@ | |||
| use async_trait::async_trait; | |||
This comment was marked as outdated.
This comment was marked as outdated.
Sorry, something went wrong.
| @@ -0,0 +1,87 @@ | |||
| use async_trait::async_trait; | |||
This comment was marked as outdated.
This comment was marked as outdated.
Sorry, something went wrong.
| } | ||
|
|
||
| async fn build(&self, cx: &BuildCx<'_>) -> Result<SectionOutcome, FatalError> { | ||
| let kind = cx_compaction_kind(cx); |
This comment was marked as outdated.
This comment was marked as outdated.
Sorry, something went wrong.
| @@ -0,0 +1,132 @@ | |||
| use async_trait::async_trait; | |||
This comment was marked as outdated.
This comment was marked as outdated.
Sorry, something went wrong.
…atform-independent Bump `@tauri-apps/api` from `^2` to `^2.11.0` in package.json and package-lock.json. Fix snapshot test portability by two changes: - Replace `entry.bytes` with `[snap]` in `format_audit_snapshot` to avoid byte count differences across platforms (e.g. 7 on macOS vs 6 on Linux for the same text). - Change the test workspace path from `/tmp/tiycode-snapshot-workspace` to `/tiycode-snap-workspace` to prevent collisions with the `TMPDIR` environment variable during path normalization. Update all affected `.snap` files accordingly.
| }; | ||
|
|
||
| // Check for cycle: if already in_flight, we have a dependency loop | ||
| if slot |
This comment was marked as outdated.
This comment was marked as outdated.
Sorry, something went wrong.
| } | ||
| } | ||
|
|
||
| /// Render a handoff template that includes plan markdown. |
This comment was marked as outdated.
This comment was marked as outdated.
Sorry, something went wrong.
| /// 3. The wall-clock date enters the LLM context only via this prepend; if | ||
| /// a feature later needs server-authoritative time across the full message | ||
| /// history, a `compaction_pinned` column on `messages` would be required. | ||
| pub(crate) async fn inject_runtime_context(user_prompt: &str) -> String { |
This comment was marked as outdated.
This comment was marked as outdated.
Sorry, something went wrong.
| let mut marker_count = 0; | ||
| // Iterate layers in increasing order (StablePrefix < SessionStable < RuntimeOverlay) | ||
| for (_layer, indices) in by_layer.iter() { | ||
| if marker_count >= 2 { |
This comment was marked as outdated.
This comment was marked as outdated.
Sorry, something went wrong.
| } | ||
|
|
||
| #[cfg(test)] | ||
| mod tests { |
This comment was marked as outdated.
This comment was marked as outdated.
Sorry, something went wrong.
| } | ||
| } | ||
|
|
||
| /// Shared formatting for title-generation language rule line. |
This comment was marked as outdated.
This comment was marked as outdated.
Sorry, something went wrong.
| results.push((spec, layer, outcome, source_start.elapsed())); | ||
|
|
||
| // Hard overall budget cap; if exceeded mid-pipeline, stop building further sources | ||
| if start.elapsed() > self.exec_policy.overall_build_timeout { |
This comment was marked as outdated.
This comment was marked as outdated.
Sorry, something went wrong.
| // We construct a minimal BuildCx to satisfy the trait signature. | ||
| let injector = CurrentDateInjector::new(Arc::new(SystemClock)); | ||
| // Build a dummy BuildCx — CurrentDateInjector doesn't read it. | ||
| let placeholder_pool = match sqlx::SqlitePool::connect_lazy("sqlite::memory:") { |
This comment was marked as outdated.
This comment was marked as outdated.
Sorry, something went wrong.
| let template = include_str!("../templates/subagent/explore.md"); | ||
| let (_tmpl, body) = | ||
| super::super::templates::parse_front_matter(template).map_err(|e| { | ||
| FatalError::new("template.parse", format!("subagent/explore.md: {e}")) |
This comment was marked as outdated.
This comment was marked as outdated.
Sorry, something went wrong.
| } | ||
| } | ||
|
|
||
| // Fallback response_language |
This comment was marked as outdated.
This comment was marked as outdated.
Sorry, something went wrong.
| }; | ||
| use std::sync::Arc; | ||
|
|
||
| let placeholder_pool = |
This comment was marked as outdated.
This comment was marked as outdated.
Sorry, something went wrong.
| }; | ||
| use std::sync::Arc; | ||
|
|
||
| let placeholder_pool = |
This comment was marked as outdated.
This comment was marked as outdated.
Sorry, something went wrong.
| // We construct a minimal BuildCx to satisfy the trait signature. | ||
| let injector = CurrentDateInjector::new(Arc::new(SystemClock)); | ||
| // Build a dummy BuildCx — CurrentDateInjector doesn't read it. | ||
| let placeholder_pool = match sqlx::SqlitePool::connect_lazy("sqlite::memory:") { |
This comment was marked as outdated.
This comment was marked as outdated.
Sorry, something went wrong.
|
|
||
| /// Global cache marker arbiter for a single LLM request. | ||
| /// Enforces the ≤4 breakpoint limit across system prompt + messages. | ||
| pub trait CacheMarkerArbiter: Send + Sync + std::fmt::Debug { |
This comment was marked as outdated.
This comment was marked as outdated.
Sorry, something went wrong.
| // Find the anchor target's position within the same layer | ||
| let target_spec = all_specs.iter().find(|s| &s.id == target_id); | ||
| let target_order = match target_spec { | ||
| Some(ts) => Self::resolve_anchored_order(ts, all_specs), |
This comment was marked as outdated.
This comment was marked as outdated.
Sorry, something went wrong.
| // We construct a minimal BuildCx to satisfy the trait signature. | ||
| let injector = CurrentDateInjector::new(Arc::new(SystemClock)); | ||
| // Build a dummy BuildCx — CurrentDateInjector doesn't read it. | ||
| let placeholder_pool = match sqlx::SqlitePool::connect_lazy("sqlite::memory:") { |
This comment was marked as outdated.
This comment was marked as outdated.
Sorry, something went wrong.
| body: &SectionBody, | ||
| budget: &PromptBudget, | ||
| ) -> (SectionBody, Option<SectionWarning>) { | ||
| let limit = spec.max_chars.unwrap_or(budget.per_section_default_chars); |
This comment was marked as outdated.
This comment was marked as outdated.
Sorry, something went wrong.
| /// All registered error codes for startup lint test. | ||
| pub const ALL_ERROR_CODES: &[&str] = &[ | ||
| codes::TEMPLATE_NOT_FOUND, | ||
| codes::TEMPLATE_MISSING_KEY, |
This comment was marked as outdated.
This comment was marked as outdated.
Sorry, something went wrong.
| } | ||
|
|
||
| /// Convenience constructor for test clocks. | ||
| pub fn fixed_clock_for_test() -> Arc<FixedClock> { |
This comment was marked as outdated.
This comment was marked as outdated.
Sorry, something went wrong.
| Some(meta) | ||
| }); | ||
|
|
||
| let _plan = match active_plan { |
This comment was marked as outdated.
This comment was marked as outdated.
Sorry, something went wrong.
| } | ||
| } | ||
|
|
||
| let mut result = tpl.to_string(); |
This comment was marked as outdated.
This comment was marked as outdated.
Sorry, something went wrong.
|
|
||
| /// Resolve the order of a section within its layer, handling Anchored positions. | ||
| /// Returns (base_order, anchor_target_order) for topological comparison. | ||
| fn resolve_anchored_order(spec: &SectionSpec, all_specs: &[&SectionSpec]) -> u32 { |
This comment was marked as outdated.
This comment was marked as outdated.
Sorry, something went wrong.
|
|
||
| #[tokio::test] | ||
| async fn snapshot_compaction_compact() { | ||
| snap_surface( |
This comment was marked as outdated.
This comment was marked as outdated.
Sorry, something went wrong.
| let cx = BuildCx { | ||
| pool: &pool, | ||
| workspace_path: workspace, | ||
| thread_id: None, // No thread → Ephemeral sections will Skip |
This comment was marked as outdated.
This comment was marked as outdated.
Sorry, something went wrong.
| }; | ||
| use std::sync::Arc; | ||
|
|
||
| let placeholder_pool = |
This comment was marked as outdated.
This comment was marked as outdated.
Sorry, something went wrong.
| ) | ||
| } | ||
|
|
||
| fn apply_total_budget<'a>( |
This comment was marked as outdated.
This comment was marked as outdated.
Sorry, something went wrong.
| /// Construct a minimal BuildCx for testing sources that don't need DB access. | ||
| async fn test_cx() -> BuildCx<'static> { | ||
| let pool = sqlx::SqlitePool::connect_lazy("sqlite::memory:").unwrap(); | ||
| let pool_ref = Box::leak(Box::new(pool)); |
This comment was marked as outdated.
This comment was marked as outdated.
Sorry, something went wrong.
… remove TMPDIR from writable roots
|
|
||
| let system_prompt = build_system_prompt(pool, &raw_plan, workspace_path, run_mode).await?; | ||
| let system_prompt = inject_goal_context(pool, thread_id, system_prompt).await?; | ||
| let composed_prompt = |
This comment was marked as outdated.
This comment was marked as outdated.
Sorry, something went wrong.
| } | ||
|
|
||
| /// Render a handoff template that includes plan markdown. | ||
| fn render_handoff_template( |
This comment was marked as outdated.
This comment was marked as outdated.
Sorry, something went wrong.
| // We construct a minimal BuildCx to satisfy the trait signature. | ||
| let injector = CurrentDateInjector::new(Arc::new(SystemClock)); | ||
| // Build a dummy BuildCx — CurrentDateInjector doesn't read it. | ||
| let placeholder_pool = match sqlx::SqlitePool::connect_lazy("sqlite::memory:") { |
This comment was marked as outdated.
This comment was marked as outdated.
Sorry, something went wrong.
| ); | ||
| } | ||
|
|
||
| // ── build_helper_system_prompt(Phase 7 Composer pipeline)── |
This comment was marked as outdated.
This comment was marked as outdated.
Sorry, something went wrong.
| /// A fatal error that causes the entire prompt build to fail. | ||
| /// Rare; reserved for truly unrecoverable errors (template load failure, SQLite fatal disconnect). | ||
| #[derive(Debug)] | ||
| pub struct FatalError { |
This comment was marked as outdated.
This comment was marked as outdated.
Sorry, something went wrong.
When the Skills section is removed from the prompt surface, the leading newline of the matched "\n## " pattern was included in the remaining text, producing a double blank line between sections. This caused snapshot mismatches on CI runners where no skills are installed. Skip the leading newline so the result joins `before` (which already ends with the layer separator) directly to the next section header, yielding exactly one blank line—matching output on machines without the Skills section.
| "- Respond in {language} unless the user explicitly asks for a different language." | ||
| )); | ||
| } | ||
| pub(crate) async fn build_compact_summary_system_prompt(response_language: Option<&str>) -> String { |
This comment was marked as outdated.
This comment was marked as outdated.
Sorry, something went wrong.
| ); | ||
| } | ||
|
|
||
| #[tokio::test] |
This comment was marked as outdated.
This comment was marked as outdated.
Sorry, something went wrong.
| } | ||
| } | ||
|
|
||
| impl CacheMarkerArbiter for DefaultCacheMarkerArbiter { |
This comment was marked as outdated.
This comment was marked as outdated.
Sorry, something went wrong.
| ) | ||
| } | ||
|
|
||
| async fn build_message(&self, _cx: &BuildCx<'_>) -> Option<RuntimeMessage> { |
This comment was marked as outdated.
This comment was marked as outdated.
Sorry, something went wrong.
| use crate::persistence::init_database; | ||
|
|
||
| /// Build a snapshot for a given surface using a fresh temp DB. | ||
| async fn snap_surface(surface: PromptSurface, snapshot_name: &str) { |
This comment was marked as outdated.
This comment was marked as outdated.
Sorry, something went wrong.
| /// Construct a minimal BuildCx for testing sources that don't need DB access. | ||
| async fn test_cx() -> BuildCx<'static> { | ||
| let pool = sqlx::SqlitePool::connect_lazy("sqlite::memory:").unwrap(); | ||
| let pool_ref = Box::leak(Box::new(pool)); |
This comment was marked as outdated.
This comment was marked as outdated.
Sorry, something went wrong.
|
|
||
| impl SurfacePattern { | ||
| /// Check whether this pattern matches a given surface. | ||
| pub fn matches(&self, surface: &PromptSurface) -> bool { |
This comment was marked as outdated.
This comment was marked as outdated.
Sorry, something went wrong.
| per_section_overrides.insert(SectionId::CustomSubagentBody, total_chars / 4); | ||
|
|
||
| // Compaction / Title surfaces use tighter budgets | ||
| let total_chars = match surface { |
This comment was marked as outdated.
This comment was marked as outdated.
Sorry, something went wrong.
|
|
||
| impl CacheMarkerArbiter for DefaultCacheMarkerArbiter { | ||
| fn record_system_markers(&self, markers: &[CacheMarkerSlot]) { | ||
| let mut sys = self.system_markers.lock().unwrap(); |
This comment was marked as outdated.
This comment was marked as outdated.
Sorry, something went wrong.
| let raw = load_template(TEMPLATE_REL_PATH, TEMPLATE_EMBEDDED); | ||
| let (tmpl, body) = parse_front_matter(&raw).map_err(|e| { | ||
| FatalError::new( | ||
| super::super::error_codes::codes::TEMPLATE_NOT_FOUND, |
This comment was marked as outdated.
This comment was marked as outdated.
Sorry, something went wrong.
…nd fix unused variable warnings
…uidelines - Add operating boundaries and safety red lines to role definition - Reorganize behavioral guidelines into categorized sections - Clarify run mode guidance for default and plan modes - Bump Role, BehavioralGuidelines, and RunMode versions to 2 - Update snapshot tests and source tests for new versions
- Remove `shell` template variable from ShellToolingGuide; reference System Environment section instead of injecting runtime value - Replace Chinese example fragment with English in FinalResponseStructure template - Add honesty and coverage guidelines to explore and review subagent templates - Add example output section to merge compaction template - Use generic path placeholder in explore template examples - Bump affected prompt section versions from 1 to 2
| SUMMARY_HISTORY_MIN_CHARS, | ||
| }; | ||
| use super::agent_run_title::collapse_whitespace; | ||
| use crate::core::prompt::templates::strip_front_matter; |
This comment was marked as outdated.
This comment was marked as outdated.
Sorry, something went wrong.
| } | ||
|
|
||
| /// Build the Title surface system prompt via Composer (Phase 6). | ||
| pub(crate) async fn build_title_system_prompt() -> String { |
This comment was marked as outdated.
This comment was marked as outdated.
Sorry, something went wrong.
| } | ||
|
|
||
| fn build_helper_system_prompt( | ||
| parent_system_prompt: &str, |
This comment was marked as outdated.
This comment was marked as outdated.
Sorry, something went wrong.
| } | ||
| } | ||
|
|
||
| /// Shared formatting for title-generation language rule line. |
This comment was marked as outdated.
This comment was marked as outdated.
Sorry, something went wrong.
| /// 3. The wall-clock date enters the LLM context only via this prepend; if | ||
| /// a feature later needs server-authoritative time across the full message | ||
| /// history, a `compaction_pinned` column on `messages` would be required. | ||
| pub(crate) async fn inject_runtime_context(user_prompt: &str) -> String { |
This comment was marked as outdated.
This comment was marked as outdated.
Sorry, something went wrong.
| Some(meta) | ||
| }); | ||
|
|
||
| let _plan = match active_plan { |
This comment was marked as outdated.
This comment was marked as outdated.
Sorry, something went wrong.
| @@ -0,0 +1,67 @@ | |||
| use async_trait::async_trait; | |||
This comment was marked as outdated.
This comment was marked as outdated.
Sorry, something went wrong.
| /// Construct a minimal BuildCx for testing sources that don't need DB access. | ||
| async fn test_cx() -> BuildCx<'static> { | ||
| let pool = sqlx::SqlitePool::connect_lazy("sqlite::memory:").unwrap(); | ||
| let pool_ref = Box::leak(Box::new(pool)); |
This comment was marked as outdated.
This comment was marked as outdated.
Sorry, something went wrong.
| use crate::core::subagent::SubagentProfile; | ||
| use std::sync::Arc; | ||
|
|
||
| #[test] |
This comment was marked as outdated.
This comment was marked as outdated.
Sorry, something went wrong.
|
|
||
| [dev-dependencies] | ||
| tempfile = "3" | ||
| insta = { version = "1", features = ["yaml"] } |
This comment was marked as outdated.
This comment was marked as outdated.
Sorry, something went wrong.
| surfaces: SurfaceMatcher::Any(vec![SurfacePattern::AnyMainAgent]), | ||
| version: 1, | ||
| max_chars: None, | ||
| criticality: SectionCriticality::NonCritical, |
This comment was marked as outdated.
This comment was marked as outdated.
Sorry, something went wrong.
| @@ -0,0 +1,155 @@ | |||
| use async_trait::async_trait; | |||
This comment was marked as outdated.
This comment was marked as outdated.
Sorry, something went wrong.
| @@ -0,0 +1,95 @@ | |||
| use async_trait::async_trait; | |||
This comment was marked as outdated.
This comment was marked as outdated.
Sorry, something went wrong.
| /// Template-backed SectionSource for the Skills section. | ||
| /// Loads skills from the ExtensionsManager and renders via the skills_usage.md template. | ||
| /// Replaces LegacySkillsSource (which delegates to the old SkillsProvider). | ||
| pub struct SkillsSource; |
This comment was marked as outdated.
This comment was marked as outdated.
Sorry, something went wrong.
| use std::sync::Arc; | ||
|
|
||
| /// Construct a minimal BuildCx for testing sources that don't need DB access. | ||
| async fn test_cx() -> BuildCx<'static> { |
This comment was marked as outdated.
This comment was marked as outdated.
Sorry, something went wrong.
| .collect::<Vec<_>>() | ||
| .join("\n"); | ||
|
|
||
| let raw = load_template(TEMPLATE_REL_PATH, TEMPLATE_EMBEDDED); |
This comment was marked as outdated.
This comment was marked as outdated.
Sorry, something went wrong.
| ); | ||
| } | ||
|
|
||
| // ── build_helper_system_prompt(Phase 7 Composer pipeline)── |
This comment was marked as outdated.
This comment was marked as outdated.
Sorry, something went wrong.
|
|
||
| [dev-dependencies] | ||
| tempfile = "3" | ||
| insta = { version = "1", features = ["yaml"] } |
This comment was marked as outdated.
This comment was marked as outdated.
Sorry, something went wrong.
| .in_flight | ||
| .swap(true, std::sync::atomic::Ordering::SeqCst) | ||
| { | ||
| // Already in flight → cycle detected |
This comment was marked as outdated.
This comment was marked as outdated.
Sorry, something went wrong.
| fn pattern(&self) -> SurfacePattern { | ||
| match self { | ||
| PromptSurface::MainAgent { run_mode } => SurfacePattern::MainAgent(*run_mode), | ||
| PromptSurface::SubagentExplore { .. } => SurfacePattern::AnySubagent, |
This comment was marked as outdated.
This comment was marked as outdated.
Sorry, something went wrong.
| /// stale declarations after template edits. | ||
| #[test] | ||
| fn templates_have_no_undeclared_keys() { | ||
| let template_dir = super::template_root(); |
There was a problem hiding this comment.
[CRITICAL] Template key validation test fails to compile in non-debug test builds
The test that validates template key declarations will fail to compile in any build where debug_assertions is not enabled, making it impossible to run tests in release mode or in CI configurations that disable debug assertions.
Suggestion: Either conditionally compile the test only when debug_assertions is enabled (#[cfg(debug_assertions)]), or provide a test‑specific template_root implementation that works in all test configurations (e.g., #[cfg(any(test, debug_assertions))]).
Risk: This prevents running the test suite in some CI pipelines, concealing template key mismatch regressions. Could lead to runtime errors if keys are misdeclared.
Confidence: 0.95
| // Record marker slots via the arbiter so the message layer can coordinate | ||
| // quota (≤4 Anthropic breakpoints across system prompt + runtime messages). | ||
| if let Some(ref arbiter) = self.cache_arbiter { | ||
| let mut byte_offset = 0usize; |
There was a problem hiding this comment.
[HIGH] Cache marker byte offsets may be incorrect due to missing layer separators
The byte offsets recorded for cache markers are computed solely on block text lengths, ignoring layer separators that will be inserted between sections in the final prompt text, leading to incorrect positions.
Suggestion: Compute offsets using the same logic as the final text assembly: either insert separators between block texts when iterating, or calculate offsets after joining.
Risk: If these offsets are used for cache-control breakpoints or other markers that depend on exact byte positions in the transmitted system prompt, the offsets will be misaligned, potentially breaking cache control hints and wrecking cache performance.
Confidence: 0.85
| fn compact_summary_system_prompt_uses_response_language_when_present() { | ||
| let prompt = build_compact_summary_system_prompt(Some(" 简体中文 ")); | ||
| #[tokio::test] | ||
| async fn compact_summary_system_prompt_uses_response_language_when_present() { |
There was a problem hiding this comment.
[MEDIUM] Response language whitespace trimming removed in compaction prompts
The response language string in compaction prompts is no longer normalized, which can cause malformed language instructions when the language value contains whitespace.
Suggestion: Reintroduce normalization (trim and empty-check) before passing response_language to the Composer, or ensure the Composer's BuildCx or downstream injection normalizes the value.
Risk: If the profile response language includes padding whitespace, the resulting system prompt may contain malformed lines like Respond in 简体中文 unless..., confusing the model.
Confidence: 0.85
| "- Respond in {language} unless the user explicitly asks for a different language." | ||
| )); | ||
| } | ||
| pub(crate) async fn build_compact_summary_system_prompt(response_language: Option<&str>) -> String { |
There was a problem hiding this comment.
[MEDIUM] Language normalization edge case not covered for summary prompts
The removal of explicit language trimming may cause Composer to fail to match the language rule for strings with extranous whitespace.
Suggestion: Add a test with a language value containing leading/trailing spaces to ensure the Composer (or ProfileInstructionsSource) normalizes it correctly.
Risk: Agent might produce summaries in wrong language if the language tag is misidentified.
Confidence: 0.90
| let system_prompt = inject_goal_context(pool, thread_id, system_prompt).await?; | ||
| let composed_prompt = | ||
| build_system_prompt(pool, &raw_plan, workspace_path, run_mode, thread_id).await?; | ||
| let system_prompt = composed_prompt.text.clone(); |
There was a problem hiding this comment.
[MEDIUM] Cache arbiter path not verified in tests
The new prompt caching feature has no test coverage; no test uses a non-None cache arbiter or verifies that ephemeral markers appear in the system prompt.
Suggestion: Add an integration test that builds a session with caching enabled and asserts the system prompt contains appropriate cache markers for stable sections.
Risk: Cache marker placement errors could lead to poor cache utilization or incorrect context window injection on Anthropic models.
Confidence: 0.85
| pub per_section_overrides: BTreeMap<SectionId, usize>, | ||
|
|
||
| /// Eviction order: layers are removed in this order when total budget is exceeded. | ||
| /// Default: [Ephemeral, RuntimeOverlay, SessionStable, StablePrefix] |
There was a problem hiding this comment.
[MEDIUM] Unused eviction_order field in PromptBudget
The budget's eviction_order is never consulted during prompt composition, making it dead code and potentially misleading.
Suggestion: Remove the field and its doc comment, or use it in the composer to determine which sections to evict when total budget is exceeded, ensuring the declared eviction order is honored.
Risk: Future developers might assume the declared order is respected, but since it's not tested or used, eviction behavior may silently differ from expectations if the sorting order changes.
Confidence: 0.90
|
|
||
| impl SectionRenderer for XmlRenderer { | ||
| fn render_section(&self, title: &str, body: &str) -> String { | ||
| format!("<section name=\"{}\">\n{}\n</section>", title, body) |
There was a problem hiding this comment.
[MEDIUM] XmlRenderer does not escape XML special characters
The XmlRenderer produces raw XML without escaping special characters, which may produce malformed prompts if the title or body contain XML-sensitive characters.
Suggestion: Escape XML special characters (e.g., <, >, &, ") in render_section for the XmlRenderer, or wrap body content in CDATA sections with careful handling.
Risk: If prompt content includes XML special characters (common in code snippets, config values, or shell output), the resulting prompt will be malformed and may cause API errors or degraded section recall.
Confidence: 0.90
| /// Construct a minimal BuildCx for testing sources that don't need DB access. | ||
| async fn test_cx() -> BuildCx<'static> { | ||
| let pool = sqlx::SqlitePool::connect_lazy("sqlite::memory:").unwrap(); | ||
| let pool_ref = Box::leak(Box::new(pool)); |
There was a problem hiding this comment.
[MEDIUM] Test helper leaks SqlitePool via Box::leak
The test_cx function leaks a SqlitePool, which may cause resource exhaustion and interfere with other tests.
Suggestion: Use a lazy_static or Arc-based pool with a proper lifecycle, or adjust BuildCx to not require 'static.
Risk: Memory and connection leaks can cause test runner slowdowns and flaky tests due to resource contention.
Confidence: 0.90
| "Error: boom" | ||
| ); | ||
| } | ||
|
|
There was a problem hiding this comment.
[MEDIUM] Replaced helper system prompt tests lack inheritance verification
The Composer-based helper prompt no longer verifies the correct inheritance of required sections; regression risk if surface matchers fail to include them.
Suggestion: Add assertions on the helper prompt content for essential inherited sections (e.g., Profile Instructions, System Environment) to ensure the Composer’s section matchers are correct.
Risk: Helper subagents could miss critical context like workspace instructions, leading to bad exploration/reviews.
Confidence: 0.90
| pub model_plan: ResolvedRuntimeModelPlan, | ||
| pub initial_prompt: Option<String>, | ||
| pub initial_context_calibration: ContextTokenCalibration, | ||
| /// Global cache marker arbiter for the request lifecycle. |
There was a problem hiding this comment.
[LOW] Unused cache_arbiter field in AgentSessionSpec
A new field is added and populated but never consumed, which may confuse future readers and suggests either an incomplete feature or dead code.
Suggestion: Either implement the intended usage (resetting the arbiter after LLM calls as mentioned in the comment) or remove the field until the feature is fully integrated.
Risk: If left unused, the field could mask the need for cache marker reset logic and cause eventual prompt-cache pollution if integrated incorrectly later.
Confidence: 0.95
Summary
Refactor the monolithic prompt injection system into a modular, layered architecture with 20+ new modules, template-based content management, and a composable section registry. This establishes the foundation for future prompt customization, safer emergency fallbacks, and cleaner subagent contracts.
Changes
Core Architecture (new modules)
composer: Orchestrates prompt assembly from registered sectionsregistry: Manages section provider registration and resolution with priority orderinglayer: Supports layered prompts (base, override, compaction, emergency fallback) with section anchoring, ordering, and audittemplates+template_sources: Template loading, rendering with Handlebars-style templating, front-matter parsing, and per-role template source abstractionrenderer: Markdown/XML renderers for prompt surfaceslegacy_adapter: Backward-compatible bridge for existing callers during phased migrationFeature Modules
build_context: UnifiedBuildCxandModelTargetfor passing build-time parameterssurface+surface_extensions: Prompt surface types (main agent, subagent, compaction, title) with extension traitsruntime_message: Runtime message injection models (current date, compaction policy)section_source+section_id+section_order: Typed section specs with metadata, outcomes, and fatal error handlingsignals: Event-driven build signals with cachingcache_marker: Cache marker arbitration for LLM cachinginheritance: Layer inheritance and override resolutionexec_policy: Source execution policy configurationfeature_set: Prompt feature flag managementbudget+clock: Token budget estimation and time injectionrun_mode: Run mode enum (default, plan)redactor: Content redaction for sensitive informationemergency_fallback: Graceful degradation when prompt assembly failserror_codes: Centralized error code definitionsactive_goal_source: Active goal section providerTemplate Files
Extracted 19 template files under
prompt/templates/covering:Caller Updates
agent_session.rs: Updated to consume the new composable prompt architectureagent_run_summary.rs: Adapted summary prompt assemblyagent_run_title.rs: Adapted title prompt assemblysubagent/orchestrator.rs: Refactored to use new prompt infrastructureagent_run_manager_tests.rsandagent_session_tests.rsDocumentation
docs/prompt-injection-refactor.md, 1816 lines) covering architecture decisions, data flow, and migration strategyMotivation
The existing prompt assembly was monolithic and tightly coupled to the agent session lifecycle. This refactoring:
Testing
cargo test --locked --manifest-path src-tauri/Cargo.tomlpasses all Rust integration testsnpm run typecheckpasses TypeScript validationBreaking Changes
None — backward compatibility preserved via
legacy_adapterand retained legacy module re-exports.Related Issues
Refs: Internal prompt injection refactoring initiative
Checklist
🤖 Generated with TiyCode