huskies: merge 1236 story Ask "what happened with X" and get a paged, subject-scoped history
This commit is contained in:
@@ -3,8 +3,8 @@
|
||||
use serde_json::Value;
|
||||
|
||||
use super::{
|
||||
agent_tools, diagnostics, git_tools, merge_tools, qa_tools, shell_tools, status_tools,
|
||||
story_tools, timer_tools, trigger_tools, wizard_tools,
|
||||
agent_tools, diagnostics, git_tools, history_tools, merge_tools, qa_tools, shell_tools,
|
||||
status_tools, story_tools, timer_tools, trigger_tools, wizard_tools,
|
||||
};
|
||||
use crate::http::context::AppContext;
|
||||
|
||||
@@ -91,6 +91,9 @@ pub async fn dispatch_tool_call(
|
||||
"get_token_usage" => diagnostics::tool_get_token_usage(&args, ctx),
|
||||
// Chat turn telemetry (story 1209)
|
||||
"chat_telemetry" => diagnostics::tool_chat_telemetry(&args),
|
||||
// Subject-scoped history (story 1236)
|
||||
"get_history" => history_tools::tool_get_history(&args),
|
||||
"get_history_entry" => history_tools::tool_get_history_entry(&args),
|
||||
// Delete story
|
||||
"delete_story" => story_tools::tool_delete_story(&args, ctx).await,
|
||||
// Purge story (CRDT tombstone — story 521)
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
//! MCP tools for subject-scoped history queries (story 1236).
|
||||
//!
|
||||
//! `get_history` returns a time-ordered, cursor-paged list of short typed
|
||||
//! summaries for a subject (story, sled, or project); `get_history_entry`
|
||||
//! resolves the `ref` from one of those summaries into its full payload.
|
||||
|
||||
use serde_json::{Value, json};
|
||||
|
||||
/// MCP tool: return a paged, subject-scoped history listing.
|
||||
pub(crate) fn tool_get_history(args: &Value) -> Result<String, String> {
|
||||
let subject_type = args
|
||||
.get("subject_type")
|
||||
.and_then(|v| v.as_str())
|
||||
.ok_or("subject_type is required (one of: story, robot, project)")?;
|
||||
let subject_id = args
|
||||
.get("subject_id")
|
||||
.and_then(|v| v.as_str())
|
||||
.ok_or("subject_id is required")?;
|
||||
if !matches!(subject_type, "story" | "sled" | "robot" | "project") {
|
||||
return Err(format!(
|
||||
"subject_type must be one of: story, robot, project (got '{subject_type}')"
|
||||
));
|
||||
}
|
||||
let since = args.get("since").and_then(|v| v.as_i64());
|
||||
let until = args.get("until").and_then(|v| v.as_i64());
|
||||
let cursor = args.get("cursor").and_then(|v| v.as_str());
|
||||
let limit = args
|
||||
.get("limit")
|
||||
.and_then(|v| v.as_u64())
|
||||
.map(|n| n as usize)
|
||||
.unwrap_or(50);
|
||||
|
||||
let page = crate::history::get_history(subject_type, subject_id, since, until, cursor, limit);
|
||||
|
||||
let entries: Vec<Value> = page
|
||||
.entries
|
||||
.iter()
|
||||
.map(|e| {
|
||||
json!({
|
||||
"ref": e.entry_ref,
|
||||
"kind": e.kind,
|
||||
"subject_type": e.subject_type,
|
||||
"subject_id": e.subject_id,
|
||||
"at": e.at.format("%Y-%m-%dT%H:%M:%SZ").to_string(),
|
||||
"summary": e.summary,
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
|
||||
serde_json::to_string_pretty(&json!({
|
||||
"entries": entries,
|
||||
"next_cursor": page.next_cursor,
|
||||
}))
|
||||
.map_err(|e| format!("Serialization error: {e}"))
|
||||
}
|
||||
|
||||
/// MCP tool: resolve a `ref` from `get_history` into its full payload.
|
||||
pub(crate) fn tool_get_history_entry(args: &Value) -> Result<String, String> {
|
||||
let entry_ref = args
|
||||
.get("ref")
|
||||
.and_then(|v| v.as_str())
|
||||
.ok_or("ref is required (from a get_history entry)")?;
|
||||
|
||||
crate::history::get_history_entry(entry_ref)
|
||||
.ok_or_else(|| format!("No history entry found for ref '{entry_ref}'"))
|
||||
}
|
||||
@@ -12,6 +12,8 @@ pub mod diagnostics;
|
||||
pub mod dispatch;
|
||||
/// MCP tools for git operations scoped to agent worktrees.
|
||||
pub mod git_tools;
|
||||
/// MCP tools for subject-scoped history queries (`get_history`, `get_history_entry`).
|
||||
pub mod history_tools;
|
||||
/// MCP tools for merge status and merge-to-master operations.
|
||||
pub mod merge_tools;
|
||||
/// Task-local progress emitter used to deliver `notifications/progress`
|
||||
|
||||
@@ -121,7 +121,9 @@ mod tests {
|
||||
assert!(names.contains(&"gc"));
|
||||
assert!(names.contains(&"chat_telemetry"));
|
||||
assert!(names.contains(&"ask_question"));
|
||||
assert_eq!(tools.len(), 89);
|
||||
assert!(names.contains(&"get_history"));
|
||||
assert!(names.contains(&"get_history_entry"));
|
||||
assert_eq!(tools.len(), 91);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -120,6 +120,54 @@ pub(super) fn system_tools() -> Vec<Value> {
|
||||
}
|
||||
}
|
||||
}),
|
||||
json!({
|
||||
"name": "get_history",
|
||||
"description": "Return a time-ordered, cursor-paged history for a subject (story, robot, or project) over an optional time range. Each entry is a short typed summary (chat_turn, agent_run, or pipeline_transition) plus a 'ref' string; fetch the full payload for one entry with get_history_entry.",
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"subject_type": {
|
||||
"type": "string",
|
||||
"description": "One of: story, robot, project"
|
||||
},
|
||||
"subject_id": {
|
||||
"type": "string",
|
||||
"description": "Story ID, robot (sled) hex ID, or project/persona name"
|
||||
},
|
||||
"since": {
|
||||
"type": "integer",
|
||||
"description": "Optional Unix-second lower bound (inclusive)"
|
||||
},
|
||||
"until": {
|
||||
"type": "integer",
|
||||
"description": "Optional Unix-second upper bound (inclusive)"
|
||||
},
|
||||
"cursor": {
|
||||
"type": "string",
|
||||
"description": "Opaque cursor from a previous page's next_cursor to resume from"
|
||||
},
|
||||
"limit": {
|
||||
"type": "integer",
|
||||
"description": "Maximum number of entries to return (default 50, max 500)"
|
||||
}
|
||||
},
|
||||
"required": ["subject_type", "subject_id"]
|
||||
}
|
||||
}),
|
||||
json!({
|
||||
"name": "get_history_entry",
|
||||
"description": "Resolve a 'ref' string returned by get_history into its full payload.",
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"ref": {
|
||||
"type": "string",
|
||||
"description": "The 'ref' value from a get_history entry"
|
||||
}
|
||||
},
|
||||
"required": ["ref"]
|
||||
}
|
||||
}),
|
||||
json!({
|
||||
"name": "run_command",
|
||||
"description": "Execute a shell command in an agent's worktree directory. The working_dir must be inside .huskies/worktrees/. Returns stdout, stderr, exit_code, and timed_out. Supports SSE streaming (send Accept: text/event-stream) for long-running commands. Dangerous commands (rm -rf /, sudo, etc.) are blocked.",
|
||||
|
||||
Reference in New Issue
Block a user