gestura_core_tasks/
lib.rs

1//! Task management and reusable workflow primitives for Gestura.
2//!
3//! `gestura-core-tasks` owns the persistent task-list model and reusable
4//! workflow definitions used by agent sessions, orchestration layers, and user
5//! interfaces.
6//!
7//! ## Responsibilities
8//!
9//! - session-scoped task CRUD and persistence via `TaskManager`
10//! - hierarchical task lists, task state transitions, and metadata tracking
11//! - task-memory lifecycle events used to mirror memory promotions and blockers
12//! - reusable markdown workflow definitions discovered by `WorkflowManager`
13//!
14//! ## Architecture role
15//!
16//! This crate is the source of truth for task and workflow domain behavior.
17//! Higher-level orchestration—such as deciding when a supervisor creates or
18//! blocks tasks—remains in `gestura-core`, but the underlying task graph and
19//! workflow loading logic live here.
20//!
21//! ## Storage model
22//!
23//! Task state is persisted under the workspace `.gestura/` area so it can be
24//! resumed across sessions. Workflow definitions are loaded from workspace-local
25//! or user-level workflow directories, allowing reusable templates without
26//! hard-coding them into the pipeline.
27//!
28//! ## Stable import paths
29//!
30//! Most code should import through the facade:
31//!
32//! - `gestura_core::tasks::*`
33//! - `gestura_core::workflows::*`
34
35use std::collections::HashMap;
36
37use serde::{Deserialize, Serialize};
38
39pub mod tasks;
40pub mod workflows;
41
42#[cfg(feature = "advanced-primitives")]
43pub mod semantic_client;
44#[cfg(feature = "advanced-primitives")]
45pub mod verification;
46
47pub use tasks::get_global_task_manager;
48
49#[cfg(feature = "advanced-primitives")]
50pub use semantic_client::{
51    SemanticClient, SemanticClientConfig, SemanticClientError, SemanticQueryHit,
52    SemanticQueryRequest, SemanticQueryResult,
53};
54#[cfg(feature = "advanced-primitives")]
55pub use verification::{
56    PromptVerificationTargets, VerificationAttempt, VerificationCheck, VerificationLoop,
57    VerificationLoopConfig, VerificationReport,
58};
59
60/// Compile-time flag exported to downstream crates so the middleware branch can
61/// constant-fold away when advanced primitives are disabled.
62pub const ADVANCED_PRIMITIVES_ENABLED: bool = cfg!(feature = "advanced-primitives");
63
64/// Request envelope for the optional advanced-planning middleware.
65#[derive(Debug, Clone, Serialize, Deserialize)]
66pub struct AdvancedPlanRequest {
67    /// Original user intent after upstream intent parsing.
68    pub user_intent: String,
69    /// Base system prompt that should be preserved and augmented.
70    pub base_system_prompt: String,
71    /// Session identifier if the request is already session-scoped.
72    #[serde(default, skip_serializing_if = "Option::is_none")]
73    pub session_id: Option<String>,
74    /// Active task identifier if the request is already attached to a task.
75    #[serde(default, skip_serializing_if = "Option::is_none")]
76    pub task_id: Option<String>,
77    /// Human-readable request source for telemetry and semantic payloads.
78    pub source: String,
79    /// Whether the upstream runtime classified this intent as complex/multi-step.
80    #[serde(default)]
81    pub complex_intent: bool,
82    /// Whether the request explicitly asks for verification such as build/test.
83    #[serde(default)]
84    pub requires_verification: bool,
85    /// Opaque request-scoped hints passed through from the agent pipeline.
86    #[serde(default, skip_serializing_if = "HashMap::is_empty")]
87    pub metadata_hints: HashMap<String, String>,
88}
89
90/// BOS1921 waveform bridge payload derived from Gestura's existing haptic model.
91#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
92pub struct Bos1921Waveform {
93    /// Downstream transport selector.
94    pub driver: String,
95    /// Semantic preset name that a BOS1921-capable bridge can map to a waveform.
96    pub preset: String,
97    /// Suggested amplitude percentage.
98    pub amplitude_percent: u8,
99    /// Suggested waveform duration.
100    pub duration_ms: u32,
101    /// Suggested repeat count.
102    pub repeat_count: u8,
103    /// Delay between repeats.
104    pub repeat_delay_ms: u32,
105}
106
107/// Result of the optional advanced-planning middleware.
108#[derive(Debug, Clone, Serialize, Deserialize)]
109pub struct AdvancedPlanOutcome {
110    /// Whether the middleware actively augmented the request.
111    pub applied: bool,
112    /// Final system prompt that should continue through the normal pipeline.
113    pub system_prompt: String,
114    /// Additional structured hints that downstream observers or hooks may inspect.
115    #[serde(default, skip_serializing_if = "HashMap::is_empty")]
116    pub metadata_hints: HashMap<String, String>,
117    /// Optional haptic bridge payload for BOS1921-capable transports.
118    #[serde(default, skip_serializing_if = "Option::is_none")]
119    pub bos1921_waveform: Option<Bos1921Waveform>,
120}
121
122impl AdvancedPlanOutcome {
123    /// Return a no-op outcome that preserves the existing pipeline behavior.
124    pub fn passthrough(system_prompt: String) -> Self {
125        Self {
126            applied: false,
127            system_prompt,
128            metadata_hints: HashMap::new(),
129            bos1921_waveform: None,
130        }
131    }
132}
133
134/// Optional advanced task primitives for complex intent-first planning.
135pub struct AdvancedPrimitives;
136
137#[cfg(feature = "advanced-primitives")]
138impl AdvancedPrimitives {
139    /// Build an enhanced multi-step planning prompt while preserving the normal
140    /// pipeline and observer flow.
141    pub async fn run_enhanced_plan(request: AdvancedPlanRequest) -> AdvancedPlanOutcome {
142        use gestura_core_foundation::interaction::HapticFeedback;
143
144        if !request.complex_intent {
145            return AdvancedPlanOutcome::passthrough(request.base_system_prompt);
146        }
147
148        let semantic_config = semantic_client_config_from_hints(&request.metadata_hints);
149        let verification_config = verification_config_from_hints(&request.metadata_hints);
150        let semantic_query = request
151            .metadata_hints
152            .get("advanced_primitives.semantic.query")
153            .cloned()
154            .filter(|value| !value.trim().is_empty())
155            .unwrap_or_else(|| request.user_intent.clone());
156
157        let semantic_result = if semantic_config.enabled {
158            match semantic_client::SemanticClient::new(semantic_config.clone()) {
159                Ok(client) => match client
160                    .query(&semantic_client::SemanticQueryRequest {
161                        query: semantic_query,
162                        domain: semantic_config.domain.clone(),
163                        session_id: request.session_id.clone(),
164                        task_id: request.task_id.clone(),
165                        source: request.source.clone(),
166                        hints: request.metadata_hints.clone(),
167                    })
168                    .await
169                {
170                    Ok(result) => result,
171                    Err(error) => {
172                        tracing::debug!(
173                            ?error,
174                            "advanced semantic query skipped after client error"
175                        );
176                        None
177                    }
178                },
179                Err(error) => {
180                    tracing::debug!(
181                        ?error,
182                        "advanced semantic client disabled due to invalid config"
183                    );
184                    None
185                }
186            }
187        } else {
188            None
189        };
190
191        let verification_targets = build_verification_targets(&request, semantic_result.as_ref());
192        let semantic_snapshot = semantic_result.clone();
193        let verification_loop = verification::VerificationLoop::new(verification_config.clone());
194        let verification_report = verification_loop
195            .run(
196                |attempt, repair_notes| {
197                    let request = request.clone();
198                    let semantic_snapshot = semantic_snapshot.clone();
199                    let repair_notes = repair_notes.cloned();
200                    async move {
201                        compose_enhanced_system_prompt(
202                            &request,
203                            semantic_snapshot.as_ref(),
204                            repair_notes.as_ref(),
205                            attempt,
206                        )
207                    }
208                },
209                |_, candidate| {
210                    let candidate = candidate.to_string();
211                    let verification_targets = verification_targets.clone();
212                    async move { verification::verify_prompt(&candidate, &verification_targets) }
213                },
214            )
215            .await;
216
217        let final_prompt = verification_report
218            .attempts
219            .last()
220            .map(|attempt| attempt.candidate.clone())
221            .unwrap_or_else(|| {
222                compose_enhanced_system_prompt(&request, semantic_result.as_ref(), None, 0)
223            });
224
225        let haptic_feedback = if verification_report.passed {
226            Some(HapticFeedback::success())
227        } else if semantic_result.is_some() {
228            Some(HapticFeedback::notification())
229        } else {
230            None
231        };
232        let bos1921_waveform = haptic_feedback
233            .as_ref()
234            .map(|feedback| bos1921_from_feedback(feedback, semantic_result.is_some()));
235
236        let mut metadata_hints = HashMap::from([
237            (
238                "advanced_primitives.enabled".to_string(),
239                "true".to_string(),
240            ),
241            (
242                "advanced_primitives.applied".to_string(),
243                "true".to_string(),
244            ),
245            (
246                "advanced_primitives.mode".to_string(),
247                "complex_intent".to_string(),
248            ),
249            (
250                "advanced_primitives.verification".to_string(),
251                serde_json::to_string(&verification_report).unwrap_or_else(|_| "{}".to_string()),
252            ),
253        ]);
254
255        if let Some(semantic_result) = &semantic_result {
256            metadata_hints.insert(
257                "advanced_primitives.semantic".to_string(),
258                serde_json::to_string(semantic_result).unwrap_or_else(|_| "{}".to_string()),
259            );
260        }
261        if let Some(waveform) = &bos1921_waveform {
262            metadata_hints.insert(
263                "advanced_primitives.bos1921".to_string(),
264                serde_json::to_string(waveform).unwrap_or_else(|_| "{}".to_string()),
265            );
266        }
267
268        AdvancedPlanOutcome {
269            applied: true,
270            system_prompt: final_prompt,
271            metadata_hints,
272            bos1921_waveform,
273        }
274    }
275}
276
277#[cfg(not(feature = "advanced-primitives"))]
278impl AdvancedPrimitives {
279    /// Preserve the original workflow unchanged when advanced primitives are
280    /// disabled at compile time.
281    pub async fn run_enhanced_plan(request: AdvancedPlanRequest) -> AdvancedPlanOutcome {
282        AdvancedPlanOutcome::passthrough(request.base_system_prompt)
283    }
284}
285
286#[cfg(feature = "advanced-primitives")]
287fn compose_enhanced_system_prompt(
288    request: &AdvancedPlanRequest,
289    semantic_result: Option<&semantic_client::SemanticQueryResult>,
290    repair_notes: Option<&verification::VerificationCheck>,
291    attempt: u8,
292) -> String {
293    let mut prompt = request.base_system_prompt.clone();
294    prompt.push_str("\n\nAdvanced intent middleware:\n");
295    prompt.push_str("- The next user request is a complex multi-step intent. Preserve the original goal and keep the execution path modality-neutral across voice, chat, gesture, and future inputs.\n");
296    prompt.push_str("- Prefer existing MCP/A2A, NATS, hooks, memory-bank, and dual-orchestrator capabilities when they are the best fit instead of inventing a parallel workflow.\n");
297    prompt.push_str("Intent anchor:\n");
298    prompt.push_str(&format!(
299        "- Source: {}\n- Session: {}\n- Task: {}\n- User intent: {}\n",
300        request.source,
301        request.session_id.as_deref().unwrap_or("n/a"),
302        request.task_id.as_deref().unwrap_or("n/a"),
303        request.user_intent.trim()
304    ));
305    prompt.push_str("Ordered execution phases:\n");
306    prompt.push_str(
307        "1. Inspect the current state, constraints, permissions, and dependencies before acting.\n",
308    );
309    prompt.push_str("2. Prepare only the prerequisites that are truly needed, then execute the requested work in order.\n");
310    prompt.push_str("3. Keep subtasks ordered and deduplicated; do not promote later phases until prerequisite work is actually complete.\n");
311    prompt.push_str("4. Capture concrete evidence before claiming completion.\n");
312    prompt.push_str("Verification gate:\n");
313    if request.requires_verification {
314        prompt.push_str("- Before you claim success, explicitly run or request the right verification steps, build/test checks, or equivalent domain validation and summarize the observed evidence.\n");
315    } else {
316        prompt.push_str("- Before you claim success, explicitly validate the final outcome and summarize the evidence that makes the result trustworthy.\n");
317    }
318    prompt.push_str("Completion guardrails:\n");
319    prompt.push_str("- Do not skip prerequisite setup.\n");
320    prompt.push_str("- Do not mark implementation complete before verification evidence exists.\n");
321    prompt.push_str("- If a step fails, repair the plan and continue from the failed step instead of replaying completed work.\n");
322
323    if let Some(semantic_result) = semantic_result {
324        prompt.push_str("Live semantic context:\n");
325        if let Some(domain) = semantic_result.domain.as_deref() {
326            prompt.push_str(&format!("- Domain: {}\n", domain));
327        }
328        prompt.push_str(&format!("- Summary: {}\n", semantic_result.summary.trim()));
329        for hit in semantic_result.hits.iter().take(2) {
330            prompt.push_str(&format!(
331                "- Source: {} — {}\n",
332                hit.title,
333                hit.snippet.trim()
334            ));
335        }
336    }
337
338    if let Some(repair_notes) = repair_notes
339        && !repair_notes.missing_requirements.is_empty()
340    {
341        prompt.push_str("Verification repair notes:\n");
342        for note in &repair_notes.missing_requirements {
343            prompt.push_str(&format!("- {}\n", note));
344        }
345    }
346
347    if attempt > 0 {
348        prompt.push_str(&format!(
349            "Retry note:\n- This is automatic planning repair attempt {} after a verification miss.\n",
350            attempt
351        ));
352    }
353
354    prompt
355}
356
357#[cfg(feature = "advanced-primitives")]
358fn build_verification_targets(
359    request: &AdvancedPlanRequest,
360    semantic_result: Option<&semantic_client::SemanticQueryResult>,
361) -> verification::PromptVerificationTargets {
362    let mut required_headings = vec![
363        "Intent anchor:".to_string(),
364        "Ordered execution phases:".to_string(),
365        "Verification gate:".to_string(),
366        "Completion guardrails:".to_string(),
367    ];
368    if semantic_result.is_some() {
369        required_headings.push("Live semantic context:".to_string());
370    }
371
372    let mut required_phrases = vec![
373        "keep subtasks ordered and deduplicated".to_string(),
374        "do not mark implementation complete before verification evidence exists".to_string(),
375    ];
376    if request.requires_verification {
377        required_phrases.push("build/test checks".to_string());
378    }
379
380    verification::PromptVerificationTargets {
381        required_headings,
382        required_phrases,
383        require_ordered_headings: true,
384        require_verification_gate: true,
385    }
386}
387
388#[cfg(feature = "advanced-primitives")]
389fn semantic_client_config_from_hints(
390    hints: &HashMap<String, String>,
391) -> semantic_client::SemanticClientConfig {
392    let endpoint = hint_value(hints, "advanced_primitives.semantic.endpoint");
393    let api_key = hint_value(hints, "advanced_primitives.semantic.api_key");
394    let domain = hint_value(hints, "advanced_primitives.semantic.domain");
395    let enabled = hint_bool(
396        hints,
397        "advanced_primitives.semantic.enabled",
398        endpoint.is_some(),
399    );
400
401    semantic_client::SemanticClientConfig {
402        enabled,
403        endpoint,
404        api_key,
405        domain,
406        max_results: hint_usize(hints, "advanced_primitives.semantic.max_results", 3),
407        timeout_ms: hint_u64(hints, "advanced_primitives.semantic.timeout_ms", 1_500),
408    }
409}
410
411#[cfg(feature = "advanced-primitives")]
412fn verification_config_from_hints(
413    hints: &HashMap<String, String>,
414) -> verification::VerificationLoopConfig {
415    verification::VerificationLoopConfig {
416        enabled: hint_bool(hints, "advanced_primitives.verification.enabled", true),
417        max_automatic_retries: hint_u8(hints, "advanced_primitives.verification.max_retries", 2)
418            .min(2),
419    }
420}
421
422#[cfg(feature = "advanced-primitives")]
423fn bos1921_from_feedback(
424    feedback: &gestura_core_foundation::interaction::HapticFeedback,
425    semantic_hit: bool,
426) -> Bos1921Waveform {
427    Bos1921Waveform {
428        driver: "bos1921".to_string(),
429        preset: if semantic_hit {
430            "semantic-success".to_string()
431        } else {
432            "verification-success".to_string()
433        },
434        amplitude_percent: (feedback.intensity * 100.0).round().clamp(0.0, 100.0) as u8,
435        duration_ms: feedback.duration_ms,
436        repeat_count: feedback.repeat_count,
437        repeat_delay_ms: feedback.repeat_delay_ms,
438    }
439}
440
441#[cfg(feature = "advanced-primitives")]
442fn hint_value(hints: &HashMap<String, String>, key: &str) -> Option<String> {
443    hints
444        .get(key)
445        .map(|value| value.trim())
446        .filter(|value| !value.is_empty())
447        .map(ToOwned::to_owned)
448}
449
450#[cfg(feature = "advanced-primitives")]
451fn hint_bool(hints: &HashMap<String, String>, key: &str, default: bool) -> bool {
452    match hints
453        .get(key)
454        .map(|value| value.trim().to_ascii_lowercase())
455    {
456        Some(value) if matches!(value.as_str(), "1" | "true" | "yes" | "on") => true,
457        Some(value) if matches!(value.as_str(), "0" | "false" | "no" | "off") => false,
458        _ => default,
459    }
460}
461
462#[cfg(feature = "advanced-primitives")]
463fn hint_u8(hints: &HashMap<String, String>, key: &str, default: u8) -> u8 {
464    hints
465        .get(key)
466        .and_then(|value| value.trim().parse::<u8>().ok())
467        .unwrap_or(default)
468}
469
470#[cfg(feature = "advanced-primitives")]
471fn hint_u64(hints: &HashMap<String, String>, key: &str, default: u64) -> u64 {
472    hints
473        .get(key)
474        .and_then(|value| value.trim().parse::<u64>().ok())
475        .unwrap_or(default)
476}
477
478#[cfg(feature = "advanced-primitives")]
479fn hint_usize(hints: &HashMap<String, String>, key: &str, default: usize) -> usize {
480    hints
481        .get(key)
482        .and_then(|value| value.trim().parse::<usize>().ok())
483        .unwrap_or(default)
484}
485
486#[cfg(all(test, feature = "advanced-primitives"))]
487mod tests {
488    use super::*;
489
490    #[tokio::test]
491    async fn advanced_primitives_emit_waveform_and_verification_hints() {
492        let mut hints = HashMap::new();
493        hints.insert(
494            "advanced_primitives.semantic.enabled".to_string(),
495            "false".to_string(),
496        );
497
498        let outcome = AdvancedPrimitives::run_enhanced_plan(AdvancedPlanRequest {
499            user_intent: "Plan and implement the change, then verify it".to_string(),
500            base_system_prompt: "System: base".to_string(),
501            session_id: Some("session-1".to_string()),
502            task_id: Some("task-1".to_string()),
503            source: "GuiText".to_string(),
504            complex_intent: true,
505            requires_verification: true,
506            metadata_hints: hints,
507        })
508        .await;
509
510        assert!(outcome.applied);
511        assert!(outcome.system_prompt.contains("Verification gate:"));
512        assert!(
513            outcome
514                .metadata_hints
515                .contains_key("advanced_primitives.verification")
516        );
517        assert_eq!(
518            outcome
519                .bos1921_waveform
520                .as_ref()
521                .map(|waveform| waveform.driver.as_str()),
522            Some("bos1921")
523        );
524    }
525}