mirror of
https://github.com/tinyhumansai/openhuman.git
synced 2026-07-30 23:14:37 +00:00
refactor(workflows): complete skills→workflows rename (internal + wire/FE) [#3324 follow-up] (#3412)
Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -2116,6 +2116,16 @@ pub async fn bootstrap_core_runtime(host_kind: crate::core::types::HostKind) {
|
||||
action_dir,
|
||||
);
|
||||
|
||||
// --- Triggered-workflow subscriber ---
|
||||
// Install on the always-run serve boot, not only inside `start_channels`
|
||||
// (skipped for web-chat-only cores with no messaging integrations, and when
|
||||
// `OPENHUMAN_DISABLE_CHANNEL_LISTENERS=1`). Without this, any workflow
|
||||
// declaring `triggers:` was silently ignored on web-chat-only desktop
|
||||
// installs. Idempotent — shares a process-global OnceLock with the
|
||||
// `start_channels` site so it registers exactly once regardless of which
|
||||
// path runs first. (Matching only for now; activation handoff still pending.)
|
||||
crate::openhuman::workflows::bus::ensure_triggered_workflow_subscriber(&workspace_dir);
|
||||
|
||||
// --- Approval gate (#1339) ---
|
||||
// ON by default; opt out with `OPENHUMAN_APPROVAL_GATE=0` (or `false`).
|
||||
// Prompt-class `external_effect()` tool calls route through
|
||||
|
||||
@@ -418,7 +418,7 @@ async fn render_integrations_agent(config: &Config, toolkit: &str) -> Result<Dum
|
||||
model_name: &model_name,
|
||||
agent_id: INTEGRATIONS_AGENT_ID,
|
||||
tools: &prompt_tools,
|
||||
skills: agent.skills(),
|
||||
workflows: agent.workflows(),
|
||||
dispatcher_instructions: "",
|
||||
learned: LearnedContextData::default(),
|
||||
visible_tool_names: &empty_visible,
|
||||
|
||||
@@ -70,9 +70,9 @@ pub struct ParentExecutionContext {
|
||||
/// dispatcher choice, …).
|
||||
pub agent_config: AgentConfig,
|
||||
|
||||
/// Skills loaded into the parent. Sub-agents that don't strip the
|
||||
/// skills catalog inherit this list.
|
||||
pub skills: Arc<Vec<Workflow>>,
|
||||
/// Workflows loaded into the parent. Sub-agents that don't strip the
|
||||
/// workflows catalog inherit this list.
|
||||
pub workflows: Arc<Vec<Workflow>>,
|
||||
|
||||
/// Memory context loaded for the current turn. Auto-injected into
|
||||
/// subagent prompts so they have access to conversation history and
|
||||
|
||||
@@ -531,7 +531,7 @@ fn datetime_section_output_matches_iso8601_date_and_utc_offset_pattern() {
|
||||
model_name: "test-model",
|
||||
agent_id: "",
|
||||
tools: EMPTY_TOOLS,
|
||||
skills: &[],
|
||||
workflows: &[],
|
||||
dispatcher_instructions: "",
|
||||
learned: crate::openhuman::agent::prompts::LearnedContextData::default(),
|
||||
visible_tool_names: &EMPTY_FILTER,
|
||||
|
||||
@@ -1050,7 +1050,7 @@ impl Agent {
|
||||
.temperature(effective_temperature)
|
||||
.workspace_dir(config.workspace_dir.clone())
|
||||
.action_dir(config.action_dir.clone())
|
||||
.skills(crate::openhuman::workflows::load_workflow_metadata(
|
||||
.workflows(crate::openhuman::workflows::load_workflow_metadata(
|
||||
&config.workspace_dir,
|
||||
))
|
||||
.auto_save(config.memory.auto_save)
|
||||
|
||||
@@ -32,7 +32,7 @@ impl AgentBuilder {
|
||||
temperature: None,
|
||||
workspace_dir: None,
|
||||
action_dir: None,
|
||||
skills: None,
|
||||
workflows: None,
|
||||
auto_save: None,
|
||||
post_turn_hooks: Vec::new(),
|
||||
learning_enabled: false,
|
||||
@@ -160,8 +160,8 @@ impl AgentBuilder {
|
||||
}
|
||||
|
||||
/// Sets the skills available to the agent.
|
||||
pub fn skills(mut self, skills: Vec<crate::openhuman::workflows::Workflow>) -> Self {
|
||||
self.skills = Some(skills);
|
||||
pub fn workflows(mut self, skills: Vec<crate::openhuman::workflows::Workflow>) -> Self {
|
||||
self.workflows = Some(skills);
|
||||
self
|
||||
}
|
||||
|
||||
@@ -517,7 +517,7 @@ impl AgentBuilder {
|
||||
temperature: self.temperature.unwrap_or(0.7),
|
||||
workspace_dir,
|
||||
action_dir,
|
||||
skills: self.skills.unwrap_or_default(),
|
||||
workflows: self.workflows.unwrap_or_default(),
|
||||
auto_save: self.auto_save.unwrap_or(false),
|
||||
last_memory_context: None,
|
||||
last_turn_citations: Vec::new(),
|
||||
|
||||
@@ -109,9 +109,9 @@ impl Agent {
|
||||
self.temperature
|
||||
}
|
||||
|
||||
/// The agent's loaded skills, if any.
|
||||
pub fn skills(&self) -> &[crate::openhuman::workflows::Workflow] {
|
||||
&self.skills
|
||||
/// The agent's loaded workflows, if any.
|
||||
pub fn workflows(&self) -> &[crate::openhuman::workflows::Workflow] {
|
||||
&self.workflows
|
||||
}
|
||||
|
||||
/// Active Composio integrations fetched at session start.
|
||||
|
||||
@@ -271,7 +271,7 @@ fn accessors_and_history_reset_expose_agent_runtime_state() {
|
||||
});
|
||||
let mut agent = make_agent(provider);
|
||||
agent.history = vec![ConversationMessage::Chat(ChatMessage::system("sys"))];
|
||||
agent.skills = vec![crate::openhuman::workflows::Workflow {
|
||||
agent.workflows = vec![crate::openhuman::workflows::Workflow {
|
||||
name: "demo".into(),
|
||||
..Default::default()
|
||||
}];
|
||||
@@ -283,7 +283,7 @@ fn accessors_and_history_reset_expose_agent_runtime_state() {
|
||||
assert_eq!(agent.workspace_dir(), agent.workspace_dir.as_path());
|
||||
assert_eq!(agent.model_name(), agent.model_name);
|
||||
assert_eq!(agent.temperature(), agent.temperature);
|
||||
assert_eq!(agent.skills().len(), 1);
|
||||
assert_eq!(agent.workflows().len(), 1);
|
||||
assert_eq!(
|
||||
agent.agent_config().max_tool_iterations,
|
||||
agent.config.max_tool_iterations
|
||||
|
||||
@@ -293,7 +293,7 @@ impl Agent {
|
||||
model_name: &self.model_name,
|
||||
agent_id: &self.agent_definition_name,
|
||||
tools: &prompt_tools,
|
||||
skills: &self.skills,
|
||||
workflows: &self.workflows,
|
||||
dispatcher_instructions: &instructions,
|
||||
learned,
|
||||
visible_tool_names: &prompt_visible_tool_names,
|
||||
|
||||
@@ -273,11 +273,11 @@ impl Agent {
|
||||
// heuristic and size cap rationale.
|
||||
let enriched = {
|
||||
use crate::openhuman::workflows::inject;
|
||||
let matches = inject::match_workflows(&self.skills, user_message);
|
||||
let matches = inject::match_workflows(&self.workflows, user_message);
|
||||
if matches.is_empty() {
|
||||
log::debug!(
|
||||
"[skills:inject] no skill matches for user message (skill_catalog_len={})",
|
||||
self.skills.len()
|
||||
"[workflows:inject] no skill matches for user message (skill_catalog_len={})",
|
||||
self.workflows.len()
|
||||
);
|
||||
enriched
|
||||
} else {
|
||||
@@ -288,7 +288,7 @@ impl Agent {
|
||||
);
|
||||
let matched_count = injection.decisions.iter().filter(|d| d.matched).count();
|
||||
log::info!(
|
||||
"[skills:inject] summary candidates={} matched={} injected_bytes={} truncated_any={}",
|
||||
"[workflows:inject] summary candidates={} matched={} injected_bytes={} truncated_any={}",
|
||||
injection.decisions.len(),
|
||||
matched_count,
|
||||
injection.injected_bytes,
|
||||
|
||||
@@ -124,7 +124,7 @@ impl Agent {
|
||||
workspace_dir: self.workspace_dir.clone(),
|
||||
memory: Arc::clone(&self.memory),
|
||||
agent_config: self.config.clone(),
|
||||
skills: Arc::new(self.skills.clone()),
|
||||
workflows: Arc::new(self.workflows.clone()),
|
||||
memory_context: Arc::new(self.last_memory_context.clone()),
|
||||
session_id: self.event_session_id().to_string(),
|
||||
channel: self.event_channel().to_string(),
|
||||
|
||||
@@ -447,7 +447,7 @@ fn trim_history_snaps_past_orphaned_tool_results() {
|
||||
fn build_parent_context_and_sanitize_helpers_cover_snapshot_paths() {
|
||||
let mut agent = make_agent(None);
|
||||
agent.last_memory_context = Some("remember this".into());
|
||||
agent.skills = vec![crate::openhuman::workflows::Workflow {
|
||||
agent.workflows = vec![crate::openhuman::workflows::Workflow {
|
||||
name: "demo".into(),
|
||||
..Default::default()
|
||||
}];
|
||||
@@ -458,7 +458,7 @@ fn build_parent_context_and_sanitize_helpers_cover_snapshot_paths() {
|
||||
assert_eq!(parent.memory_context.as_deref(), Some("remember this"));
|
||||
assert_eq!(parent.session_id, "turn-test-session");
|
||||
assert_eq!(parent.channel, "turn-test-channel");
|
||||
assert_eq!(parent.skills.len(), 1);
|
||||
assert_eq!(parent.workflows.len(), 1);
|
||||
|
||||
assert_eq!(sanitize_learned_entry(" "), "");
|
||||
assert_eq!(
|
||||
|
||||
@@ -56,7 +56,7 @@ pub struct Agent {
|
||||
pub(super) temperature: f64,
|
||||
pub(super) workspace_dir: std::path::PathBuf,
|
||||
pub(super) action_dir: std::path::PathBuf,
|
||||
pub(super) skills: Vec<crate::openhuman::workflows::Workflow>,
|
||||
pub(super) workflows: Vec<crate::openhuman::workflows::Workflow>,
|
||||
/// Agent workflows discovered at session start.
|
||||
pub(super) auto_save: bool,
|
||||
/// Last memory context loaded for the current turn. Stored so it can
|
||||
@@ -275,7 +275,7 @@ pub struct AgentBuilder {
|
||||
pub(super) temperature: Option<f64>,
|
||||
pub(super) workspace_dir: Option<std::path::PathBuf>,
|
||||
pub(super) action_dir: Option<std::path::PathBuf>,
|
||||
pub(super) skills: Option<Vec<crate::openhuman::workflows::Workflow>>,
|
||||
pub(super) workflows: Option<Vec<crate::openhuman::workflows::Workflow>>,
|
||||
/// Agent workflows to surface in the prompt. Populated from `load_workflows`
|
||||
/// at session start; defaults to empty when not explicitly set.
|
||||
pub(super) auto_save: Option<bool>,
|
||||
|
||||
@@ -563,7 +563,7 @@ async fn run_typed_mode(
|
||||
model_name: &model,
|
||||
agent_id: &definition.id,
|
||||
tools: &prompt_tools,
|
||||
skills: &parent.skills,
|
||||
workflows: &parent.workflows,
|
||||
dispatcher_instructions: &dispatcher_instructions,
|
||||
learned: crate::openhuman::context::prompt::LearnedContextData::default(),
|
||||
visible_tool_names: &visible_tool_names,
|
||||
|
||||
@@ -326,7 +326,7 @@ fn make_parent(provider: Arc<dyn Provider>, tools: Vec<Box<dyn Tool>>) -> Parent
|
||||
workspace_dir: std::env::temp_dir(),
|
||||
memory: noop_memory(),
|
||||
agent_config: crate::openhuman::config::AgentConfig::default(),
|
||||
skills: Arc::new(vec![]),
|
||||
workflows: Arc::new(vec![]),
|
||||
memory_context: Arc::new(None),
|
||||
session_id: "test-session".into(),
|
||||
channel: "test".into(),
|
||||
|
||||
@@ -1599,7 +1599,7 @@ async fn orchestrator_prompt_drives_composio_call_via_delegation_chain() {
|
||||
model_name: "test",
|
||||
agent_id: "orchestrator",
|
||||
tools: &[],
|
||||
skills: &[],
|
||||
workflows: &[],
|
||||
dispatcher_instructions: "",
|
||||
learned: LearnedContextData::default(),
|
||||
visible_tool_names: EMPTY.get_or_init(HashSet::new),
|
||||
|
||||
@@ -870,7 +870,7 @@ mod tests {
|
||||
model_name: "test-model",
|
||||
agent_id: "orchestrator",
|
||||
tools: &[],
|
||||
skills: &[],
|
||||
workflows: &[],
|
||||
dispatcher_instructions: "",
|
||||
learned: LearnedContextData::default(),
|
||||
visible_tool_names: &visible_tool_names,
|
||||
|
||||
@@ -58,7 +58,7 @@ fn prompt_builder_assembles_sections() {
|
||||
model_name: "test-model",
|
||||
agent_id: "",
|
||||
tools: &prompt_tools,
|
||||
skills: &[],
|
||||
workflows: &[],
|
||||
dispatcher_instructions: "instr",
|
||||
learned: LearnedContextData::default(),
|
||||
visible_tool_names: &NO_FILTER,
|
||||
@@ -92,7 +92,7 @@ fn identity_section_creates_missing_workspace_files() {
|
||||
model_name: "test-model",
|
||||
agent_id: "",
|
||||
tools: &prompt_tools,
|
||||
skills: &[],
|
||||
workflows: &[],
|
||||
dispatcher_instructions: "",
|
||||
learned: LearnedContextData::default(),
|
||||
visible_tool_names: &NO_FILTER,
|
||||
@@ -135,7 +135,7 @@ fn datetime_section_includes_timestamp_and_timezone() {
|
||||
model_name: "test-model",
|
||||
agent_id: "",
|
||||
tools: &prompt_tools,
|
||||
skills: &[],
|
||||
workflows: &[],
|
||||
dispatcher_instructions: "instr",
|
||||
learned: LearnedContextData::default(),
|
||||
visible_tool_names: &NO_FILTER,
|
||||
@@ -180,7 +180,7 @@ fn ctx_with_identity(identity: Option<UserIdentity>) -> PromptContext<'static> {
|
||||
model_name: "test-model",
|
||||
agent_id: "",
|
||||
tools: EMPTY_TOOLS,
|
||||
skills: &[],
|
||||
workflows: &[],
|
||||
dispatcher_instructions: "",
|
||||
learned: LearnedContextData::default(),
|
||||
visible_tool_names: visible,
|
||||
@@ -322,7 +322,7 @@ fn tools_section_pformat_renders_signature_not_schema() {
|
||||
model_name: "test-model",
|
||||
agent_id: "",
|
||||
tools: &prompt_tools,
|
||||
skills: &[],
|
||||
workflows: &[],
|
||||
dispatcher_instructions: "",
|
||||
learned: LearnedContextData::default(),
|
||||
visible_tool_names: &NO_FILTER,
|
||||
@@ -365,7 +365,7 @@ fn tools_section_uses_pformat_signature_for_text_dispatchers() {
|
||||
model_name: "test-model",
|
||||
agent_id: "",
|
||||
tools: &prompt_tools,
|
||||
skills: &[],
|
||||
workflows: &[],
|
||||
dispatcher_instructions: "",
|
||||
learned: LearnedContextData::default(),
|
||||
visible_tool_names: &NO_FILTER,
|
||||
@@ -415,7 +415,7 @@ fn user_memory_section_renders_namespaces_with_headings() {
|
||||
model_name: "test-model",
|
||||
agent_id: "",
|
||||
tools: &prompt_tools,
|
||||
skills: &[],
|
||||
workflows: &[],
|
||||
dispatcher_instructions: "",
|
||||
learned,
|
||||
visible_tool_names: &NO_FILTER,
|
||||
@@ -494,7 +494,7 @@ fn user_memory_section_returns_empty_when_no_summaries() {
|
||||
model_name: "test-model",
|
||||
agent_id: "",
|
||||
tools: &prompt_tools,
|
||||
skills: &[],
|
||||
workflows: &[],
|
||||
dispatcher_instructions: "",
|
||||
learned,
|
||||
visible_tool_names: &NO_FILTER,
|
||||
@@ -1142,7 +1142,7 @@ fn for_subagent_builder_injects_user_files_even_when_identity_omitted() {
|
||||
model_name: "test-model",
|
||||
agent_id: "",
|
||||
tools: &prompt_tools,
|
||||
skills: &[],
|
||||
workflows: &[],
|
||||
dispatcher_instructions: "",
|
||||
learned: LearnedContextData::default(),
|
||||
visible_tool_names: &NO_FILTER,
|
||||
@@ -1188,7 +1188,7 @@ fn for_subagent_builder_injects_user_files_even_when_identity_omitted() {
|
||||
model_name: "test-model",
|
||||
agent_id: "",
|
||||
tools: &prompt_tools,
|
||||
skills: &[],
|
||||
workflows: &[],
|
||||
dispatcher_instructions: "",
|
||||
learned: LearnedContextData::default(),
|
||||
visible_tool_names: &NO_FILTER,
|
||||
@@ -1265,7 +1265,7 @@ fn prompt_tool_constructors_and_user_memory_skip_empty_bodies() {
|
||||
model_name: "model",
|
||||
agent_id: "",
|
||||
tools: &[],
|
||||
skills: &[],
|
||||
workflows: &[],
|
||||
dispatcher_instructions: "",
|
||||
learned: LearnedContextData {
|
||||
tree_root_summaries: vec![ns_summary("user", "kept"), ns_summary("empty", " ")],
|
||||
@@ -1296,7 +1296,7 @@ fn ctx_with_learned(learned: LearnedContextData) -> PromptContext<'static> {
|
||||
model_name: "test-model",
|
||||
agent_id: "",
|
||||
tools: prompt_tools,
|
||||
skills: &[],
|
||||
workflows: &[],
|
||||
dispatcher_instructions: "",
|
||||
learned,
|
||||
visible_tool_names: &NO_FILTER,
|
||||
@@ -1435,7 +1435,7 @@ fn tools_section_empty_for_native() {
|
||||
model_name: "test-model",
|
||||
agent_id: "",
|
||||
tools: &prompt_tools,
|
||||
skills: &[],
|
||||
workflows: &[],
|
||||
dispatcher_instructions: "",
|
||||
learned: LearnedContextData::default(),
|
||||
visible_tool_names: &NO_FILTER,
|
||||
@@ -1468,7 +1468,7 @@ fn tools_section_nonempty_for_pformat() {
|
||||
model_name: "test-model",
|
||||
agent_id: "",
|
||||
tools: &prompt_tools,
|
||||
skills: &[],
|
||||
workflows: &[],
|
||||
dispatcher_instructions: "",
|
||||
learned: LearnedContextData::default(),
|
||||
visible_tool_names: &NO_FILTER,
|
||||
@@ -1503,7 +1503,7 @@ fn tools_section_native_with_dispatcher_instructions_returns_instructions() {
|
||||
model_name: "test-model",
|
||||
agent_id: "",
|
||||
tools: &prompt_tools,
|
||||
skills: &[],
|
||||
workflows: &[],
|
||||
dispatcher_instructions: "## Tool Use Protocol\n\nUse native tool calling.",
|
||||
learned: LearnedContextData::default(),
|
||||
visible_tool_names: &NO_FILTER,
|
||||
|
||||
@@ -640,7 +640,7 @@ pub fn default_workspace_file_content(filename: &str) -> &'static str {
|
||||
/// manufacture a full context when they only need the static text.
|
||||
fn empty_prompt_context_for_static_sections() -> PromptContext<'static> {
|
||||
static EMPTY_TOOLS: &[PromptTool<'static>] = &[];
|
||||
static EMPTY_SKILLS: &[crate::openhuman::workflows::Workflow] = &[];
|
||||
static EMPTY_WORKFLOWS: &[crate::openhuman::workflows::Workflow] = &[];
|
||||
static EMPTY_INTEGRATIONS: &[ConnectedIntegration] = &[];
|
||||
// SAFETY: the &HashSet reference must outlive the returned context;
|
||||
// a leaked OnceLock-style allocation gives us a permanent 'static
|
||||
@@ -652,7 +652,7 @@ fn empty_prompt_context_for_static_sections() -> PromptContext<'static> {
|
||||
model_name: "",
|
||||
agent_id: "",
|
||||
tools: EMPTY_TOOLS,
|
||||
skills: EMPTY_SKILLS,
|
||||
workflows: EMPTY_WORKFLOWS,
|
||||
dispatcher_instructions: "",
|
||||
learned: LearnedContextData::default(),
|
||||
visible_tool_names: visible,
|
||||
|
||||
@@ -339,7 +339,7 @@ pub struct PromptContext<'a> {
|
||||
/// Id of the agent this prompt is being built for.
|
||||
pub agent_id: &'a str,
|
||||
pub tools: &'a [PromptTool<'a>],
|
||||
pub skills: &'a [Workflow],
|
||||
pub workflows: &'a [Workflow],
|
||||
pub dispatcher_instructions: &'a str,
|
||||
/// Pre-fetched learned context (empty when learning is disabled).
|
||||
pub learned: LearnedContextData,
|
||||
|
||||
@@ -99,6 +99,14 @@ mod guard {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Test-only reader for the process-lifetime spawn counter. Used by the
|
||||
/// regression test that asserts a rejected spawn (e.g. unknown workflow
|
||||
/// id) doesn't consume a backstop slot.
|
||||
#[cfg(test)]
|
||||
pub fn total_spawns() -> u64 {
|
||||
TOTAL_SPAWNS.load(Ordering::SeqCst)
|
||||
}
|
||||
|
||||
/// Acquire an await slot + re-entrancy lock for `key` (a workflow-id +
|
||||
/// inputs fingerprint, or `await:<run_id>` for re-attach). `Err` if too
|
||||
/// many awaits are in flight (nesting/fan-out cap) or the same key is
|
||||
@@ -555,6 +563,41 @@ mod tests {
|
||||
super::guard::acquire_await("cap-test-9".to_string()).expect("a freed slot is reusable");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn unknown_workflow_id_does_not_burn_a_spawn_slot() {
|
||||
// Regression: a rejected spawn (unknown workflow id) must NOT consume a
|
||||
// slot against the process-lifetime backstop. `account_spawn` runs only
|
||||
// in the `Ok(started)` arm — after `spawn_workflow_run_background`
|
||||
// succeeds — so an unknown id (which fails synchronously) never accounts
|
||||
// a spawn. Without this ordering, an agent retrying a bad id would
|
||||
// exhaust the 500-spawn budget for legitimate runs. Asserts the counter
|
||||
// DELTA is zero (the counter is global + monotonic, so absolute value is
|
||||
// shared with the backstop test — hence the serial lock + delta check).
|
||||
let _s = guard_serial().lock().unwrap();
|
||||
let before = super::guard::total_spawns();
|
||||
let t = RunWorkflowTool::new();
|
||||
// wait_seconds: 0 → fire-and-forget path (no await slot taken); the
|
||||
// unknown id makes the spawn fail before accounting.
|
||||
let res = t
|
||||
.execute(json!({
|
||||
"workflow_id": "definitely-not-a-real-workflow-zzz",
|
||||
"wait_seconds": 0
|
||||
}))
|
||||
.await
|
||||
.expect("Ok(ToolResult)");
|
||||
assert!(res.is_error, "unknown workflow id must return a tool error");
|
||||
assert!(
|
||||
res.output().contains("unknown") || res.output().contains("workflow"),
|
||||
"error should reference the unknown workflow: {}",
|
||||
res.output()
|
||||
);
|
||||
let after = super::guard::total_spawns();
|
||||
assert_eq!(
|
||||
before, after,
|
||||
"a rejected spawn must not increment the spawn backstop counter"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn account_spawn_trips_the_process_backstop() {
|
||||
let _s = guard_serial().lock().unwrap();
|
||||
|
||||
@@ -280,7 +280,7 @@ async fn dispatch_target_agent(agent_id: &str, prompt: &str) -> anyhow::Result<S
|
||||
workspace_dir: agent.workspace_dir().to_path_buf(),
|
||||
memory: agent.memory_arc(),
|
||||
agent_config: agent.agent_config().clone(),
|
||||
skills: Arc::new(agent.skills().to_vec()),
|
||||
workflows: Arc::new(agent.workflows().to_vec()),
|
||||
memory_context: Arc::new(None), // Sub-agent queries memory via tools if needed
|
||||
session_id: format!("triage-{}", uuid::Uuid::new_v4()),
|
||||
channel: "triage".to_string(),
|
||||
|
||||
@@ -735,7 +735,7 @@ fn extract_inline_prompt(def: &AgentDefinition) -> Option<String> {
|
||||
model_name: "",
|
||||
agent_id: &def.id,
|
||||
tools: &empty_tools,
|
||||
skills: &[],
|
||||
workflows: &[],
|
||||
dispatcher_instructions: "",
|
||||
learned: LearnedContextData::default(),
|
||||
visible_tool_names: &empty_visible,
|
||||
|
||||
@@ -86,7 +86,7 @@ fn parent_context(provider: Arc<dyn Provider>) -> ParentExecutionContext {
|
||||
workspace_dir: std::env::temp_dir(),
|
||||
memory: Arc::new(NoopMemory),
|
||||
agent_config: AgentConfig::default(),
|
||||
skills: Arc::new(Vec::new()),
|
||||
workflows: Arc::new(Vec::new()),
|
||||
memory_context: Arc::new(None),
|
||||
session_id: "orchestrator-session".to_string(),
|
||||
channel: "test".to_string(),
|
||||
|
||||
@@ -201,7 +201,7 @@ fn parent_context_with_provider(
|
||||
workspace_dir: std::env::temp_dir(),
|
||||
memory: Arc::new(NoopMemory),
|
||||
agent_config,
|
||||
skills: Arc::new(Vec::new()),
|
||||
workflows: Arc::new(Vec::new()),
|
||||
memory_context: Arc::new(None),
|
||||
session_id: "session-test".into(),
|
||||
channel: "test".into(),
|
||||
|
||||
@@ -385,7 +385,7 @@ mod tests {
|
||||
channel: "test".into(),
|
||||
all_tools: Arc::new(vec![]),
|
||||
all_tool_specs: Arc::new(vec![]),
|
||||
skills: Arc::new(vec![]),
|
||||
workflows: Arc::new(vec![]),
|
||||
memory_context: std::sync::Arc::new(None),
|
||||
connected_integrations: vec![],
|
||||
on_progress: None,
|
||||
|
||||
@@ -193,7 +193,7 @@ fn parent_context(
|
||||
workspace_dir: workspace_dir.to_path_buf(),
|
||||
memory: Arc::new(NoopMemory),
|
||||
agent_config: Default::default(),
|
||||
skills: Arc::new(Vec::new()),
|
||||
workflows: Arc::new(Vec::new()),
|
||||
memory_context: Arc::new(None),
|
||||
session_id: "tools-e2e-session".into(),
|
||||
channel: "test".into(),
|
||||
|
||||
@@ -52,7 +52,7 @@ mod tests {
|
||||
model_name: "test",
|
||||
agent_id: "archivist",
|
||||
tools: &[],
|
||||
skills: &[],
|
||||
workflows: &[],
|
||||
dispatcher_instructions: "",
|
||||
learned: LearnedContextData::default(),
|
||||
visible_tool_names: &visible,
|
||||
|
||||
@@ -56,7 +56,7 @@ mod tests {
|
||||
model_name: "test",
|
||||
agent_id: "code_executor",
|
||||
tools: &[],
|
||||
skills: &[],
|
||||
workflows: &[],
|
||||
dispatcher_instructions: "",
|
||||
learned: LearnedContextData::default(),
|
||||
visible_tool_names: &visible,
|
||||
|
||||
@@ -51,7 +51,7 @@ mod tests {
|
||||
model_name: "test",
|
||||
agent_id: "critic",
|
||||
tools: &[],
|
||||
skills: &[],
|
||||
workflows: &[],
|
||||
dispatcher_instructions: "",
|
||||
learned: LearnedContextData::default(),
|
||||
visible_tool_names: &visible,
|
||||
|
||||
@@ -19,7 +19,7 @@ pub fn build(ctx: &PromptContext<'_>) -> Result<String> {
|
||||
agent_id = ctx.agent_id,
|
||||
model = ctx.model_name,
|
||||
tool_count = ctx.tools.len(),
|
||||
skill_count = ctx.skills.len(),
|
||||
workflow_count = ctx.workflows.len(),
|
||||
"[agent_prompt][crypto_agent] build_start"
|
||||
);
|
||||
|
||||
@@ -77,7 +77,7 @@ mod tests {
|
||||
model_name: "test",
|
||||
agent_id: "crypto_agent",
|
||||
tools: &[],
|
||||
skills: &[],
|
||||
workflows: &[],
|
||||
dispatcher_instructions: "",
|
||||
learned: LearnedContextData::default(),
|
||||
visible_tool_names: EMPTY_VISIBLE.get_or_init(HashSet::new),
|
||||
|
||||
@@ -47,7 +47,7 @@ mod tests {
|
||||
model_name: "test",
|
||||
agent_id: "desktop_control_agent",
|
||||
tools: &[],
|
||||
skills: &[],
|
||||
workflows: &[],
|
||||
dispatcher_instructions: "",
|
||||
learned: LearnedContextData::default(),
|
||||
visible_tool_names: &visible,
|
||||
|
||||
@@ -51,7 +51,7 @@ mod tests {
|
||||
model_name: "test",
|
||||
agent_id: "help",
|
||||
tools: &[],
|
||||
skills: &[],
|
||||
workflows: &[],
|
||||
dispatcher_instructions: "",
|
||||
learned: LearnedContextData::default(),
|
||||
visible_tool_names: &visible,
|
||||
|
||||
@@ -185,7 +185,7 @@ mod tests {
|
||||
model_name: "test",
|
||||
agent_id: "integrations_agent",
|
||||
tools: &[],
|
||||
skills: &[],
|
||||
workflows: &[],
|
||||
dispatcher_instructions: "",
|
||||
learned: LearnedContextData::default(),
|
||||
visible_tool_names: EMPTY_VISIBLE.get_or_init(HashSet::new),
|
||||
|
||||
@@ -467,7 +467,7 @@ mod tests {
|
||||
model_name: "test",
|
||||
agent_id: &def.id,
|
||||
tools: &empty_tools,
|
||||
skills: &[],
|
||||
workflows: &[],
|
||||
dispatcher_instructions: "",
|
||||
learned: LearnedContextData::default(),
|
||||
visible_tool_names: &empty_visible,
|
||||
|
||||
@@ -21,7 +21,7 @@ pub fn build(ctx: &PromptContext<'_>) -> Result<String> {
|
||||
agent_id = ctx.agent_id,
|
||||
model = ctx.model_name,
|
||||
tool_count = ctx.tools.len(),
|
||||
skill_count = ctx.skills.len(),
|
||||
workflow_count = ctx.workflows.len(),
|
||||
"[agent_prompt][markets_agent] build_start"
|
||||
);
|
||||
|
||||
@@ -79,7 +79,7 @@ mod tests {
|
||||
model_name: "test",
|
||||
agent_id: "markets_agent",
|
||||
tools: &[],
|
||||
skills: &[],
|
||||
workflows: &[],
|
||||
dispatcher_instructions: "",
|
||||
learned: LearnedContextData::default(),
|
||||
visible_tool_names: EMPTY_VISIBLE.get_or_init(HashSet::new),
|
||||
|
||||
@@ -52,7 +52,7 @@ mod tests {
|
||||
model_name: "test",
|
||||
agent_id: "mcp_setup",
|
||||
tools: &[],
|
||||
skills: &[],
|
||||
workflows: &[],
|
||||
dispatcher_instructions: "",
|
||||
learned: LearnedContextData::default(),
|
||||
visible_tool_names: visible,
|
||||
|
||||
@@ -67,7 +67,7 @@ mod tests {
|
||||
model_name: "test",
|
||||
agent_id: "morning_briefing",
|
||||
tools: &[],
|
||||
skills: &[],
|
||||
workflows: &[],
|
||||
dispatcher_instructions: "",
|
||||
learned: LearnedContextData::default(),
|
||||
visible_tool_names: visible,
|
||||
|
||||
@@ -200,7 +200,7 @@ mod tests {
|
||||
model_name: "test",
|
||||
agent_id: "orchestrator",
|
||||
tools: &[],
|
||||
skills: &[],
|
||||
workflows: &[],
|
||||
dispatcher_instructions: "",
|
||||
learned: LearnedContextData::default(),
|
||||
visible_tool_names: EMPTY_VISIBLE.get_or_init(HashSet::new),
|
||||
|
||||
@@ -58,7 +58,7 @@ mod tests {
|
||||
model_name: "test",
|
||||
agent_id: "planner",
|
||||
tools: &[],
|
||||
skills: &[],
|
||||
workflows: &[],
|
||||
dispatcher_instructions: "",
|
||||
learned: LearnedContextData::default(),
|
||||
visible_tool_names: &visible,
|
||||
|
||||
@@ -47,7 +47,7 @@ mod tests {
|
||||
model_name: "test",
|
||||
agent_id: "presentation_agent",
|
||||
tools: &[],
|
||||
skills: &[],
|
||||
workflows: &[],
|
||||
dispatcher_instructions: "",
|
||||
learned: LearnedContextData::default(),
|
||||
visible_tool_names: &visible,
|
||||
|
||||
@@ -52,7 +52,7 @@ mod tests {
|
||||
model_name: "test",
|
||||
agent_id: "researcher",
|
||||
tools: &[],
|
||||
skills: &[],
|
||||
workflows: &[],
|
||||
dispatcher_instructions: "",
|
||||
learned: LearnedContextData::default(),
|
||||
visible_tool_names: &visible,
|
||||
|
||||
@@ -47,7 +47,7 @@ mod tests {
|
||||
model_name: "test",
|
||||
agent_id: "scheduler_agent",
|
||||
tools: &[],
|
||||
skills: &[],
|
||||
workflows: &[],
|
||||
dispatcher_instructions: "",
|
||||
learned: LearnedContextData::default(),
|
||||
visible_tool_names: &visible,
|
||||
|
||||
@@ -101,7 +101,7 @@ mod tests {
|
||||
model_name: "test",
|
||||
agent_id: "skill_creator",
|
||||
tools: &[],
|
||||
skills: &[],
|
||||
workflows: &[],
|
||||
dispatcher_instructions: "",
|
||||
learned: LearnedContextData::default(),
|
||||
visible_tool_names: &visible,
|
||||
|
||||
@@ -52,7 +52,7 @@ mod tests {
|
||||
model_name: "test",
|
||||
agent_id: "summarizer",
|
||||
tools: &[],
|
||||
skills: &[],
|
||||
workflows: &[],
|
||||
dispatcher_instructions: "",
|
||||
learned: LearnedContextData::default(),
|
||||
visible_tool_names: &visible,
|
||||
|
||||
@@ -51,7 +51,7 @@ mod tests {
|
||||
model_name: "test",
|
||||
agent_id: "task_manager_agent",
|
||||
tools: &[],
|
||||
skills: &[],
|
||||
workflows: &[],
|
||||
dispatcher_instructions: "",
|
||||
learned: LearnedContextData::default(),
|
||||
visible_tool_names: &visible,
|
||||
|
||||
@@ -56,7 +56,7 @@ mod tests {
|
||||
model_name: "test",
|
||||
agent_id: "tool_maker",
|
||||
tools: &[],
|
||||
skills: &[],
|
||||
workflows: &[],
|
||||
dispatcher_instructions: "",
|
||||
learned: LearnedContextData::default(),
|
||||
visible_tool_names: &visible,
|
||||
|
||||
@@ -53,7 +53,7 @@ mod tests {
|
||||
model_name: "test",
|
||||
agent_id: "tools_agent",
|
||||
tools: &[],
|
||||
skills: &[],
|
||||
workflows: &[],
|
||||
dispatcher_instructions: "",
|
||||
learned: LearnedContextData::default(),
|
||||
visible_tool_names: &visible,
|
||||
|
||||
@@ -52,7 +52,7 @@ mod tests {
|
||||
model_name: "test",
|
||||
agent_id: "trigger_reactor",
|
||||
tools: &[],
|
||||
skills: &[],
|
||||
workflows: &[],
|
||||
dispatcher_instructions: "",
|
||||
learned: LearnedContextData::default(),
|
||||
visible_tool_names: &visible,
|
||||
|
||||
@@ -52,7 +52,7 @@ mod tests {
|
||||
model_name: "test",
|
||||
agent_id: "trigger_triage",
|
||||
tools: &[],
|
||||
skills: &[],
|
||||
workflows: &[],
|
||||
dispatcher_instructions: "",
|
||||
learned: LearnedContextData::default(),
|
||||
visible_tool_names: &visible,
|
||||
|
||||
@@ -322,16 +322,10 @@ pub async fn start_channels(mut config: Config) -> Result<()> {
|
||||
|
||||
// Install the triggered-workflow subscriber now that workflows are
|
||||
// discovered — otherwise any workflow declaring `triggers:` is silently
|
||||
// ignored in the channel runtime. The handle is parked in a process static
|
||||
// so the RAII SubscriptionHandle isn't dropped (which would cancel it).
|
||||
{
|
||||
use crate::core::event_bus::SubscriptionHandle;
|
||||
use std::sync::OnceLock;
|
||||
static TRIGGERED_WORKFLOW_HANDLE: OnceLock<Option<SubscriptionHandle>> = OnceLock::new();
|
||||
TRIGGERED_WORKFLOW_HANDLE.get_or_init(|| {
|
||||
crate::openhuman::workflows::bus::register_triggered_workflow_subscriber(&skills)
|
||||
});
|
||||
}
|
||||
// ignored. Idempotent + shares a process-global OnceLock with the
|
||||
// `bootstrap_core_runtime` site, so it registers exactly once regardless of
|
||||
// which startup path runs first (web-chat-only cores never reach here).
|
||||
crate::openhuman::workflows::bus::ensure_triggered_workflow_subscriber(&workspace);
|
||||
|
||||
// Collect tool descriptions for the prompt
|
||||
let mut tool_descs: Vec<(&str, &str)> = vec![
|
||||
|
||||
@@ -296,7 +296,7 @@ mod tests {
|
||||
model_name: "test-model",
|
||||
agent_id: "",
|
||||
tools: &[],
|
||||
skills: &[],
|
||||
workflows: &[],
|
||||
dispatcher_instructions: "",
|
||||
learned,
|
||||
visible_tool_names,
|
||||
|
||||
@@ -232,7 +232,7 @@ mod tests {
|
||||
model_name: "test",
|
||||
agent_id: "test",
|
||||
tools: &[],
|
||||
skills: &[],
|
||||
workflows: &[],
|
||||
dispatcher_instructions: "",
|
||||
learned: LearnedContextData::default(),
|
||||
visible_tool_names: &visible,
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
use crate::core::event_bus::{subscribe_global, DomainEvent, EventHandler, SubscriptionHandle};
|
||||
use crate::openhuman::workflows::Workflow;
|
||||
use async_trait::async_trait;
|
||||
use std::sync::Arc;
|
||||
use std::sync::{Arc, OnceLock};
|
||||
|
||||
// ── Trigger pattern ───────────────────────────────────────────────────────────
|
||||
|
||||
@@ -109,7 +109,7 @@ impl TriggeredWorkflowIndex {
|
||||
let p = TriggerPattern::parse(t);
|
||||
if p.is_none() {
|
||||
log::warn!(
|
||||
"[skills::triggered] skill '{}': malformed trigger {:?} — skipping",
|
||||
"[workflows::triggered] skill '{}': malformed trigger {:?} — skipping",
|
||||
skill.name,
|
||||
t
|
||||
);
|
||||
@@ -184,7 +184,7 @@ impl EventHandler for TriggeredSkillSubscriber {
|
||||
tracing::debug!(
|
||||
domain = event.domain(),
|
||||
skills = ?matched,
|
||||
"[skills::triggered] event matches {} skill trigger(s); \
|
||||
"[workflows::triggered] event matches {} skill trigger(s); \
|
||||
activation handoff to integration layer pending",
|
||||
matched.len()
|
||||
);
|
||||
@@ -213,7 +213,7 @@ pub fn register_triggered_workflow_subscriber(skills: &[Workflow]) -> Option<Sub
|
||||
return None;
|
||||
}
|
||||
log::info!(
|
||||
"[skills::triggered] registering subscriber for {} skill(s) with event triggers (domains: {:?})",
|
||||
"[workflows::triggered] registering subscriber for {} skill(s) with event triggers (domains: {:?})",
|
||||
index.len(),
|
||||
index.domains()
|
||||
);
|
||||
@@ -222,6 +222,39 @@ pub fn register_triggered_workflow_subscriber(skills: &[Workflow]) -> Option<Sub
|
||||
}))
|
||||
}
|
||||
|
||||
/// Process-global parking spot for the triggered-workflow subscription
|
||||
/// handle. The RAII [`SubscriptionHandle`] must outlive the process (dropping
|
||||
/// it cancels the subscription), and registration must happen exactly once no
|
||||
/// matter how many startup paths reach it.
|
||||
static TRIGGERED_WORKFLOW_HANDLE: OnceLock<Option<SubscriptionHandle>> = OnceLock::new();
|
||||
|
||||
/// Idempotently install the triggered-workflow subscriber.
|
||||
///
|
||||
/// Loads workflow metadata from `workspace` and registers the subscriber on the
|
||||
/// **first** call; subsequent calls are no-ops (the handle is parked in
|
||||
/// [`TRIGGERED_WORKFLOW_HANDLE`] so the RAII guard isn't dropped). Safe to call
|
||||
/// from every startup path.
|
||||
///
|
||||
/// Both [`crate::openhuman::channels::start_channels`] (messaging cores) and
|
||||
/// [`crate::core::jsonrpc::bootstrap_core_runtime`] (always-run serve boot)
|
||||
/// invoke this. `start_channels` is skipped for web-chat-only desktop installs
|
||||
/// (no messaging integration connected) and when
|
||||
/// `OPENHUMAN_DISABLE_CHANNEL_LISTENERS=1`; registering from
|
||||
/// `bootstrap_core_runtime` too means those cores still honour workflow
|
||||
/// `triggers:`. The shared `OnceLock` guarantees a single registration
|
||||
/// regardless of which path runs first.
|
||||
///
|
||||
/// NOTE: the subscriber currently only *matches* triggers and logs — the
|
||||
/// activation handoff to the integration layer is still pending (see
|
||||
/// [`TriggeredSkillSubscriber::handle`]). Registering on web-chat-only cores
|
||||
/// enables matching, not yet activation.
|
||||
pub fn ensure_triggered_workflow_subscriber(workspace: &std::path::Path) {
|
||||
TRIGGERED_WORKFLOW_HANDLE.get_or_init(|| {
|
||||
let workflows = crate::openhuman::workflows::load_workflow_metadata(workspace);
|
||||
register_triggered_workflow_subscriber(&workflows)
|
||||
});
|
||||
}
|
||||
|
||||
/// Legacy no-op retained while call-sites migrate to
|
||||
/// [`register_triggered_workflow_subscriber`]. Safe to call multiple times.
|
||||
pub fn register_workflow_cleanup_subscriber() {}
|
||||
@@ -242,6 +275,22 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
// ── ensure_triggered_workflow_subscriber (C1 boot-path helper) ───────────
|
||||
|
||||
#[test]
|
||||
fn ensure_triggered_workflow_subscriber_is_idempotent_and_safe() {
|
||||
// Covers the boot-path helper called from both `start_channels` and
|
||||
// `bootstrap_core_runtime`: it loads workflow metadata from the
|
||||
// workspace and registers the subscriber exactly once via the
|
||||
// process-global OnceLock. An empty temp workspace yields no triggered
|
||||
// workflows, so registration resolves to `None`; the call must not
|
||||
// panic and must be safe to repeat (the OnceLock makes the second call
|
||||
// a no-op).
|
||||
let tmp = tempfile::tempdir().expect("tempdir");
|
||||
ensure_triggered_workflow_subscriber(tmp.path());
|
||||
ensure_triggered_workflow_subscriber(tmp.path());
|
||||
}
|
||||
|
||||
// ── TriggerPattern::parse ────────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -52,7 +52,7 @@
|
||||
//!
|
||||
//! ## Logging
|
||||
//!
|
||||
//! Every candidate emits a grep-friendly `[skills:inject]` log line
|
||||
//! Every candidate emits a grep-friendly `[workflows:inject]` log line
|
||||
//! with `matched=<bool>`, reason, and injected bytes (see
|
||||
//! [`render_injection`]). A summary line lives in the caller
|
||||
//! (`Agent::turn`).
|
||||
@@ -265,7 +265,10 @@ fn contains_whole_word(haystack_lower: &str, needle_lower: &str) -> bool {
|
||||
|
||||
/// Match installed skills against a user message per the heuristic
|
||||
/// documented at the top of this module.
|
||||
pub fn match_workflows<'a>(skills: &'a [Workflow], user_message: &str) -> Vec<WorkflowMatch<'a>> {
|
||||
pub fn match_workflows<'a>(
|
||||
workflows: &'a [Workflow],
|
||||
user_message: &str,
|
||||
) -> Vec<WorkflowMatch<'a>> {
|
||||
let mentions = extract_mentions(user_message);
|
||||
let mention_set: HashSet<String> = mentions.iter().map(|(n, _)| n.clone()).collect();
|
||||
let mention_index = |skill_norm: &str| -> Option<usize> {
|
||||
@@ -278,7 +281,7 @@ pub fn match_workflows<'a>(skills: &'a [Workflow], user_message: &str) -> Vec<Wo
|
||||
let lower_msg = user_message.to_lowercase();
|
||||
|
||||
let mut matches: Vec<WorkflowMatch<'a>> = Vec::new();
|
||||
for skill in skills {
|
||||
for skill in workflows {
|
||||
let normalised_name = normalise(&skill.name);
|
||||
let user_invocable = is_user_invocable(skill);
|
||||
|
||||
@@ -385,7 +388,7 @@ where
|
||||
Some(b) => b,
|
||||
None => {
|
||||
log::warn!(
|
||||
"[skills:inject] matched={} reason={} name={} skipped=body_unavailable",
|
||||
"[workflows:inject] matched={} reason={} name={} skipped=body_unavailable",
|
||||
false,
|
||||
"body_unavailable",
|
||||
name
|
||||
@@ -415,7 +418,7 @@ where
|
||||
let min_truncated = header_len + footer_trunc_len + 1;
|
||||
if remaining < min_truncated {
|
||||
log::info!(
|
||||
"[skills:inject] matched={} reason={} name={} skipped=budget_exhausted remaining_bytes={}",
|
||||
"[workflows:inject] matched={} reason={} name={} skipped=budget_exhausted remaining_bytes={}",
|
||||
false,
|
||||
"budget_exhausted",
|
||||
name,
|
||||
@@ -439,7 +442,7 @@ where
|
||||
rendered.push_str(&footer_full);
|
||||
let injected = header_len + body.len() + footer_full_len;
|
||||
log::debug!(
|
||||
"[skills:inject] matched={} reason={} name={} injected_bytes={} truncated={}",
|
||||
"[workflows:inject] matched={} reason={} name={} injected_bytes={} truncated={}",
|
||||
true,
|
||||
m.reason.as_str(),
|
||||
name,
|
||||
@@ -469,7 +472,7 @@ where
|
||||
truncated_any = true;
|
||||
let injected = header_len + truncated_body.len() + footer_trunc_len;
|
||||
log::warn!(
|
||||
"[skills:inject] matched={} reason={} name={} injected_bytes={} truncated={} body_bytes_total={} body_bytes_kept={}",
|
||||
"[workflows:inject] matched={} reason={} name={} injected_bytes={} truncated={} body_bytes_total={} body_bytes_kept={}",
|
||||
true,
|
||||
m.reason.as_str(),
|
||||
name,
|
||||
|
||||
@@ -302,7 +302,7 @@ impl Workflow {
|
||||
pub fn read_body(&self) -> Option<String> {
|
||||
if self.legacy {
|
||||
log::debug!(
|
||||
"[skills:inject] read_body skipped for legacy skill.json skill name={}",
|
||||
"[workflows:inject] read_body skipped for legacy skill.json skill name={}",
|
||||
self.name
|
||||
);
|
||||
return None;
|
||||
@@ -312,7 +312,7 @@ impl Workflow {
|
||||
Some((_, body, _)) => Some(body),
|
||||
None => {
|
||||
log::warn!(
|
||||
"[skills:inject] read_body failed to parse {} for skill {}",
|
||||
"[workflows:inject] read_body failed to parse {} for skill {}",
|
||||
path.display(),
|
||||
self.name
|
||||
);
|
||||
|
||||
@@ -243,36 +243,36 @@ pub async fn run_github_preflight<P: PreflightProbes>(
|
||||
probes: &P,
|
||||
) -> Result<(), GithubGateError> {
|
||||
let Some(cfg) = cfg else {
|
||||
tracing::debug!("[skills:preflight] github gate skipped: no [github] block");
|
||||
tracing::debug!("[workflows:preflight] github gate skipped: no [github] block");
|
||||
return Ok(());
|
||||
};
|
||||
if !cfg.required {
|
||||
tracing::debug!("[skills:preflight] github gate skipped: required = false");
|
||||
tracing::debug!("[workflows:preflight] github gate skipped: required = false");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// (1) Composio GitHub integration must be connected.
|
||||
if !probes.composio_toolkit_active("github").await {
|
||||
tracing::warn!("[skills:preflight] github gate fail: composio_github_missing");
|
||||
tracing::warn!("[workflows:preflight] github gate fail: composio_github_missing");
|
||||
return Err(GithubGateError::ComposioGithubMissing);
|
||||
}
|
||||
|
||||
// (2) git binary present.
|
||||
if let Err(e) = probes.git_version().await {
|
||||
tracing::warn!(error = %e, "[skills:preflight] github gate fail: git_binary_missing");
|
||||
tracing::warn!(error = %e, "[workflows:preflight] github gate fail: git_binary_missing");
|
||||
return Err(GithubGateError::GitBinaryMissing(e));
|
||||
}
|
||||
|
||||
// (3a) git user.name set.
|
||||
let git_name = probes.git_user_name().await;
|
||||
if git_name.is_empty() {
|
||||
tracing::warn!("[skills:preflight] github gate fail: git_user_name_missing");
|
||||
tracing::warn!("[workflows:preflight] github gate fail: git_user_name_missing");
|
||||
return Err(GithubGateError::GitUserNameMissing);
|
||||
}
|
||||
// (3b) git user.email set.
|
||||
let git_email = probes.git_user_email().await;
|
||||
if git_email.is_empty() {
|
||||
tracing::warn!("[skills:preflight] github gate fail: git_user_email_missing");
|
||||
tracing::warn!("[workflows:preflight] github gate fail: git_user_email_missing");
|
||||
return Err(GithubGateError::GitUserEmailMissing);
|
||||
}
|
||||
|
||||
@@ -280,7 +280,7 @@ pub async fn run_github_preflight<P: PreflightProbes>(
|
||||
match cfg.identity_match {
|
||||
IdentityMatch::None => {
|
||||
tracing::debug!(
|
||||
"[skills:preflight] github gate pass (identity_match=none, reachability only)"
|
||||
"[workflows:preflight] github gate pass (identity_match=none, reachability only)"
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
@@ -289,12 +289,12 @@ pub async fn run_github_preflight<P: PreflightProbes>(
|
||||
// identity — confirms the connection is genuinely usable.
|
||||
match probes.composio_identity("github").await {
|
||||
Some(_) => {
|
||||
tracing::debug!("[skills:preflight] github gate pass (identity_match=any)");
|
||||
tracing::debug!("[workflows:preflight] github gate pass (identity_match=any)");
|
||||
Ok(())
|
||||
}
|
||||
None => {
|
||||
tracing::warn!(
|
||||
"[skills:preflight] github gate fail: composio_identity_unresolved (identity_match=any)"
|
||||
"[workflows:preflight] github gate fail: composio_identity_unresolved (identity_match=any)"
|
||||
);
|
||||
Err(GithubGateError::ComposioIdentityUnresolved)
|
||||
}
|
||||
@@ -305,7 +305,7 @@ pub async fn run_github_preflight<P: PreflightProbes>(
|
||||
Some(n) => n,
|
||||
None => {
|
||||
tracing::warn!(
|
||||
"[skills:preflight] github gate fail: composio_identity_unresolved (identity_match=strict)"
|
||||
"[workflows:preflight] github gate fail: composio_identity_unresolved (identity_match=strict)"
|
||||
);
|
||||
return Err(GithubGateError::ComposioIdentityUnresolved);
|
||||
}
|
||||
@@ -314,14 +314,14 @@ pub async fn run_github_preflight<P: PreflightProbes>(
|
||||
tracing::debug!(
|
||||
composio = %composio_name,
|
||||
git = %git_name,
|
||||
"[skills:preflight] github gate pass (identity_match=strict)"
|
||||
"[workflows:preflight] github gate pass (identity_match=strict)"
|
||||
);
|
||||
Ok(())
|
||||
} else {
|
||||
tracing::warn!(
|
||||
composio = %composio_name,
|
||||
git = %git_name,
|
||||
"[skills:preflight] github gate fail: identity_mismatch"
|
||||
"[workflows:preflight] github gate fail: identity_mismatch"
|
||||
);
|
||||
Err(GithubGateError::IdentityMismatch {
|
||||
composio_username: composio_name,
|
||||
|
||||
@@ -43,7 +43,9 @@ pub(super) fn handle_workflows_list(params: Map<String, Value>) -> ControllerFut
|
||||
);
|
||||
let summaries = skills.into_iter().map(WorkflowSummary::from).collect();
|
||||
to_json(RpcOutcome::new(
|
||||
WorkflowsListResult { skills: summaries },
|
||||
WorkflowsListResult {
|
||||
workflows: summaries,
|
||||
},
|
||||
Vec::new(),
|
||||
))
|
||||
})
|
||||
@@ -231,7 +233,7 @@ pub(super) fn handle_workflows_create(params: Map<String, Value>) -> ControllerF
|
||||
);
|
||||
to_json(RpcOutcome::new(
|
||||
WorkflowsCreateResult {
|
||||
skill: WorkflowSummary::from(skill),
|
||||
workflow: WorkflowSummary::from(skill),
|
||||
},
|
||||
Vec::new(),
|
||||
))
|
||||
@@ -261,7 +263,7 @@ pub(super) fn handle_workflows_update(params: Map<String, Value>) -> ControllerF
|
||||
match create_workflow(workspace.as_path(), create_params) {
|
||||
Ok(skill) => to_json(RpcOutcome::new(
|
||||
WorkflowsCreateResult {
|
||||
skill: WorkflowSummary::from(skill),
|
||||
workflow: WorkflowSummary::from(skill),
|
||||
},
|
||||
Vec::new(),
|
||||
)),
|
||||
@@ -296,7 +298,7 @@ pub(super) fn handle_workflows_install_from_url(params: Map<String, Value>) -> C
|
||||
url: outcome.url,
|
||||
stdout: outcome.stdout,
|
||||
stderr: outcome.stderr,
|
||||
new_skills: outcome.new_skills,
|
||||
new_workflows: outcome.new_skills,
|
||||
},
|
||||
Vec::new(),
|
||||
))
|
||||
|
||||
@@ -193,7 +193,7 @@ impl From<Workflow> for WorkflowSummary {
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub(super) struct WorkflowsListResult {
|
||||
pub(super) skills: Vec<WorkflowSummary>,
|
||||
pub(super) workflows: Vec<WorkflowSummary>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
@@ -206,7 +206,7 @@ pub(super) struct WorkflowsReadResourceResult {
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub(super) struct WorkflowsCreateResult {
|
||||
pub(super) skill: WorkflowSummary,
|
||||
pub(super) workflow: WorkflowSummary,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
@@ -214,7 +214,7 @@ pub(super) struct WorkflowsInstallFromUrlResult {
|
||||
pub(super) url: String,
|
||||
pub(super) stdout: String,
|
||||
pub(super) stderr: String,
|
||||
pub(super) new_skills: Vec<String>,
|
||||
pub(super) new_workflows: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
|
||||
Reference in New Issue
Block a user