Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 10 additions & 16 deletions docs/interactive-capabilities/capabilities.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"title": "OpenBitFun Playbook",
"origin": "https://playbook.openbitfun.com",
"source": "src/shared/interactive-capabilities/catalog.json",
"digest": "d2710a9488e6f021e11ece435e8d067a9285903c26f20f5bc0b586ac46d67e6d",
"digest": "63afe6c13213f11a7089fbb6208db930d563e4d3098f7c6dcf4c20a1f4e87271",
"ownerDigest": "c0e5c187cf62bc6ed06196ce8520b3eb427bf268cf24659b72d2552fb1d99c54",
"searchAcceptance": [
{
Expand Down Expand Up @@ -1353,13 +1353,11 @@
"additionalProperties": true
},
"delegateTools": [
"Task",
"AgentSpawn",
"AgentWait",
"AgentSendInput",
"AgentInterrupt",
"AgentList",
"AgentDelete"
"AgentControl",
"AgentList"
],
"presentationTarget": {
"kind": "action",
Expand Down Expand Up @@ -19711,20 +19709,18 @@
"control": {
"kind": "delegate",
"tools": [
"Task",
"AgentSpawn",
"AgentWait",
"AgentSendInput",
"AgentInterrupt",
"AgentList",
"AgentDelete"
"AgentControl",
"AgentList"
],
"workflowZh": [
"按任务形态使用 Task 或 AgentSpawn 启动有边界的子任务,并保留返回的 agent_id",
"使用 AgentSpawn 启动有边界的子任务,并保留返回的 agent_id",
"用 AgentList/AgentWait 查看进度;需要调整时用 AgentSendInput,必要时再中断或删除,最终综合子智能体结果"
],
"workflowEn": [
"Start a bounded subtask with Task or AgentSpawn and retain the returned agent_id",
"Start a bounded subtask with AgentSpawn and retain the returned agent_id",
"Use AgentList or AgentWait for progress, AgentSendInput for corrections, and interrupt or delete only when necessary before synthesizing the results"
]
}
Expand Down Expand Up @@ -19794,16 +19790,14 @@
"帮我进入智能体管理页",
"Open Agents so I can see the specialized roles",
"Take me to agent management",
"Task",
"AgentSpawn",
"AgentWait",
"AgentSendInput",
"AgentInterrupt",
"AgentControl",
"AgentList",
"AgentDelete",
"按任务形态使用 Task 或 AgentSpawn 启动有边界的子任务,并保留返回的 agent_id",
"使用 AgentSpawn 启动有边界的子任务,并保留返回的 agent_id",
"用 AgentList/AgentWait 查看进度;需要调整时用 AgentSendInput,必要时再中断或删除,最终综合子智能体结果",
"Start a bounded subtask with Task or AgentSpawn and retain the returned agent_id",
"Start a bounded subtask with AgentSpawn and retain the returned agent_id",
"Use AgentList or AgentWait for progress, AgentSendInput for corrections, and interrupt or delete only when necessary before synthesizing the results"
],
"docsUrl": "https://playbook.openbitfun.com/capabilities/feature.agents/"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ Manage built-in, custom, and subagents so specialized roles can handle research,
- Inspect each agent's tools, Skills, read-only state, and capability summary
- **智能体可定位入口,需交互完成 / Agent opens; interaction required** · 按智能体选择工具组、单个工具、Skill 组与 Skills
- Choose tool groups, individual tools, skill groups, and Skills per agent
- **由专用智能体工具控制 / Delegated Agent tool** · `Task` / `AgentSpawn` / `AgentWait` / `AgentSendInput` / `AgentInterrupt` / `AgentList` / `AgentDelete` · 让主智能体委派并行研究、编码和评审任务并协调生命周期
- **由专用智能体工具控制 / Delegated Agent tool** · `AgentSpawn` / `AgentWait` / `AgentSendInput` / `AgentControl` / `AgentList` · 让主智能体委派并行研究、编码和评审任务并协调生命周期
- Let the primary agent delegate parallel research, coding, and review work and coordinate their lifecycle

## 怎么用 / How to use it
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"schemaVersion": 1,
"generatedFrom": "src/shared/interactive-capabilities/catalog.json",
"catalogDigest": "d2710a9488e6f021e11ece435e8d067a9285903c26f20f5bc0b586ac46d67e6d",
"catalogDigest": "63afe6c13213f11a7089fbb6208db930d563e4d3098f7c6dcf4c20a1f4e87271",
"count": 212,
"reasonCounts": {
"externalAuth": 4,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"schemaVersion": 2,
"generatedFrom": "src/shared/interactive-capabilities/catalog.json",
"catalogDigest": "d2710a9488e6f021e11ece435e8d067a9285903c26f20f5bc0b586ac46d67e6d",
"catalogDigest": "63afe6c13213f11a7089fbb6208db930d563e4d3098f7c6dcf4c20a1f4e87271",
"commandCount": 668,
"coverage": {
"commandCount": 668,
Expand Down
3 changes: 0 additions & 3 deletions packages/flow-chat-presentation/src/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ export const toolPresentationRegistry = {
ControlHub: { owner: 'standard', component: 'ControlHubToolCard' },
AgentSpawn: { owner: 'standard', component: 'AgentControlToolCard' },
AgentSendInput: { owner: 'standard', component: 'SessionMessageToolCard' },
AgentInterrupt: { owner: 'standard', component: 'SessionMessageToolCard' },
AgentWait: { owner: 'standard', component: 'AgentWaitToolCard' },
TodoWrite: { owner: 'standard', component: 'TodoToolCard' },
ContextCompression: { owner: 'standard', component: 'ContextCompressionToolCard' },
Expand All @@ -38,7 +37,6 @@ export const toolPresentationRegistry = {
create_goal: { owner: 'standard', component: 'GoalToolCard' },
update_goal: { owner: 'standard', component: 'GoalToolCard' },
AgentList: { owner: 'standard', component: 'AgentRosterToolCard' },
AgentDelete: { owner: 'standard', component: 'ToolRelationRow' },
SessionHistory: { owner: 'standard', component: 'SessionHistoryToolCard' },
analyze_image: { owner: 'standard', component: 'ImageAnalysisToolCard' },
GetTime: { owner: 'standard', component: 'TimeToolCard' },
Expand All @@ -54,7 +52,6 @@ export const toolPresentationRegistry = {
PublishMiniApp: { owner: 'standard', component: 'MarketplacePublishToolCard' },
PublishAppearance: { owner: 'standard', component: 'MarketplacePublishToolCard' },
Playbook: { owner: 'standard', component: 'PlaybookToolCard' },
Task: { owner: 'product', component: 'AgentControlToolCard' },
LaunchReviewAgent: { owner: 'product', component: 'AgentControlToolCard' },
submit_code_review: { owner: 'product', component: null },
AskUserQuestion: { owner: 'product', component: 'AskUser' },
Expand Down
1 change: 0 additions & 1 deletion scripts/core-boundaries/rules/source/public-api-rules.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,6 @@ export const agentRuntimeRootPublicModules = [
'evidence_ledger',
'review_read_receipt',
'native_hooks',
'output_surface',
'permission',
'post_call_hooks',
'prompt',
Expand Down
2 changes: 1 addition & 1 deletion scripts/interactive-capabilities.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -223,7 +223,7 @@ test('every product Agent tool is mapped to a user capability or explicitly clas
productTools,
'new Agent tools must be mapped in the shared manual or receive a reviewed framework-only exclusion',
);
assert.deepEqual(excludedTools, new Set(['GetToolSpec', 'CallDeferredTool']));
assert.deepEqual(excludedTools, new Set(['Task', 'GetToolSpec', 'CallDeferredTool']));
assert.equal([...delegatedTools].filter((tool) => excludedTools.has(tool)).length, 0);

const byId = new Map(source.capabilities.map((capability) => [capability.id, capability]));
Expand Down
6 changes: 3 additions & 3 deletions src/apps/cli/src/peer_host/fanout.rs
Original file line number Diff line number Diff line change
Expand Up @@ -391,7 +391,7 @@ async fn handle_agentic_event(
{
let (tool_name, params) = effective_tool_invocation(&identity.tool_name, params);
debug_assert_eq!(identity.effective_name(), tool_name);
if tool_name == "Task"
if matches!(tool_name, "Task" | "AgentSpawn")
&& params
.get("run_in_background")
.and_then(serde_json::Value::as_bool)
Expand Down Expand Up @@ -426,7 +426,7 @@ async fn handle_agentic_event(
let terminal_task_call = match tool_event {
ToolEventData::Completed {
identity, result, ..
} if identity.effective_name() == "Task" => Some((
} if matches!(identity.effective_name(), "Task" | "AgentSpawn") => Some((
identity.tool_id.as_str(),
result
.get("background_task_id")
Expand All @@ -436,7 +436,7 @@ async fn handle_agentic_event(
.and_then(serde_json::Value::as_u64),
)),
ToolEventData::Failed { identity, .. } | ToolEventData::Cancelled { identity, .. }
if identity.effective_name() == "Task" =>
if matches!(identity.effective_name(), "Task" | "AgentSpawn") =>
{
Some((identity.tool_id.as_str(), None, None))
}
Expand Down
2 changes: 1 addition & 1 deletion src/apps/cli/src/ui/chat/tools.rs
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ impl ChatView {
}
"Edit" | "Write" | "Delete" | "search_replace" | "write_file"
| "write_file_tool" => tool_state.result.is_some(),
"Task" => {
"Task" | "AgentSpawn" => {
tool_state.result.is_some()
|| matches!(
tool_state.status,
Expand Down
54 changes: 47 additions & 7 deletions src/apps/cli/src/ui/markdown.rs
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ impl MarkdownRenderer {
let mut list_level: usize = 0;
let mut in_code_block = false;
let mut code_block_lang = String::new();
let mut image_context: Option<(String, String)> = None;
let wrap_width = if width > 0 { width } else { 80 };

// Table state
Expand Down Expand Up @@ -158,11 +159,8 @@ impl MarkdownRenderer {
Tag::Link { .. } => {
style_stack.push(StyleModifier::Link);
}
Tag::Image { .. } => {
current_line_spans.push(Span::styled(
"[Image]".to_string(),
self.theme.style(StyleKind::Info),
));
Tag::Image { dest_url, .. } => {
image_context = Some((String::new(), dest_url.to_string()));
}
Tag::Table(alignment) => {
flush_with_wrap(&mut current_line_spans, &mut lines, wrap_width, true);
Expand Down Expand Up @@ -274,14 +272,32 @@ impl MarkdownRenderer {
}
}
}
TagEnd::Image => {
if let Some((alt, destination)) = image_context.take() {
let label = if alt.is_empty() { "Image" } else { &alt };
let span = Span::styled(
format!("[{label}]({destination})"),
self.theme
.style(StyleKind::Info)
.add_modifier(Modifier::UNDERLINED),
);
if table_state.in_cell {
table_state.push_span(span);
} else {
current_line_spans.push(span);
}
}
}
_ => {}
}
}

Event::Text(text) => {
let style = self.compute_style(&style_stack, in_code_block);

if in_code_block {
if let Some((alt, _)) = image_context.as_mut() {
alt.push_str(&text);
} else if in_code_block {
for line in text.lines() {
// Code blocks preserve style but still wrap to viewport width.
wrap_spans_to_lines(
Expand All @@ -299,6 +315,10 @@ impl MarkdownRenderer {
}

Event::Code(code) => {
if let Some((alt, _)) = image_context.as_mut() {
alt.push_str(&code);
continue;
}
let span =
Span::styled(format!("`{}`", code), self.theme.style(StyleKind::Success));
if table_state.in_cell {
Expand All @@ -309,7 +329,9 @@ impl MarkdownRenderer {
}

Event::SoftBreak | Event::HardBreak => {
if !in_code_block && !table_state.in_table {
if let Some((alt, _)) = image_context.as_mut() {
alt.push(' ');
} else if !in_code_block && !table_state.in_table {
flush_with_wrap(&mut current_line_spans, &mut lines, wrap_width, true);
}
}
Expand Down Expand Up @@ -384,6 +406,24 @@ impl MarkdownRenderer {
}
}

#[cfg(test)]
mod tests {
use super::{MarkdownRenderer, Theme};

#[test]
fn renders_images_as_plain_links_in_the_terminal() {
let renderer = MarkdownRenderer::new(Theme::dark_ansi16());
let lines = renderer.render("![diagram](images/diagram.png)", 80);
let rendered = lines
.iter()
.flat_map(|line| line.spans.iter())
.map(|span| span.content.as_ref())
.collect::<String>();

assert_eq!(rendered, "[diagram](images/diagram.png)");
}
}

/// Wrap a list of styled spans into multiple Lines, breaking at `max_width`
/// display columns. Preserves the style of each span across line breaks.
/// Breaks prefer word boundaries (spaces) when possible.
Expand Down
2 changes: 1 addition & 1 deletion src/apps/cli/src/ui/theme.rs
Original file line number Diff line number Diff line change
Expand Up @@ -671,7 +671,7 @@ pub(super) fn tool_icon(tool_name: &str) -> &'static str {
"LS" | "list_dir" | "ls" => "\u{2192}", // →
"WebFetch" => "%",
"WebSearch" => "\u{25c8}", // ◈
"Task" => "#",
"Task" | "AgentSpawn" => "#",
"HmosCompilation" => "\u{2692}",
"TodoWrite" => "\u{2699}", // ⚙
"Skill" => "\u{2192}", // →
Expand Down
19 changes: 11 additions & 8 deletions src/apps/cli/src/ui/tool_cards.rs
Original file line number Diff line number Diff line change
Expand Up @@ -124,9 +124,9 @@ fn tool_display_mode(tool_name: &str, tool_state: &ToolDisplayState) -> ToolDisp
ToolDisplayMode::Inline
}
}
// Task always renders as Block — even during early detection / params streaming,
// Agent launches always render as Block — even during early detection / params streaming,
// we want to show the subagent card with real-time progress rather than inline "Delegating...".
"Task" => ToolDisplayMode::Block,
"Task" | "AgentSpawn" => ToolDisplayMode::Block,

// Always block tools
"TodoWrite" | "AskUserQuestion" | "CreatePlan" => ToolDisplayMode::Block,
Expand Down Expand Up @@ -424,7 +424,7 @@ fn inline_pending_text(canonical: &str, tool_state: &ToolDisplayState) -> String
"LS" => "Listing directory...".to_string(),
"WebSearch" => "Searching web...".to_string(),
"WebFetch" => "Fetching from the web...".to_string(),
"Task" => "Delegating...".to_string(),
"Task" | "AgentSpawn" => "Launching agent...".to_string(),
"TodoWrite" => "Updating todos...".to_string(),
"HmosCompilation" => "Compiling HarmonyOS project...".to_string(),
"Skill" => "Loading skill...".to_string(),
Expand Down Expand Up @@ -635,7 +635,9 @@ fn render_block_dispatch(
available_width,
),
"Delete" => render_delete_block(tool_state, theme, focused, spinner_frame, available_width),
"Task" => render_task_block(tool_state, theme, focused, spinner_frame, available_width),
"Task" | "AgentSpawn" => {
render_task_block(tool_state, theme, focused, spinner_frame, available_width)
}
"TodoWrite" => {
render_todo_block(tool_state, theme, focused, spinner_frame, available_width)
}
Expand Down Expand Up @@ -1416,24 +1418,25 @@ fn render_delete_block(
})
}

/// Render a Task tool as a block (sub-agent type + description + real-time progress)
/// Render an agent launch as a block (agent type + prompt + real-time progress)
fn render_task_block(
tool_state: &ToolDisplayState,
theme: &Theme,
focused: bool,
spinner_frame: &str,
available_width: u16,
) -> ToolCardRenderOutput {
let subagent_type = param_str_opt(&tool_state.parameters, &["subagent_type"])
let subagent_type = param_str_opt(&tool_state.parameters, &["subagent_type", "agent_type"])
.unwrap_or_else(|| "Unknown".to_string());
let description = param_str_opt(&tool_state.parameters, &["description"])
.unwrap_or_else(|| "Task".to_string());
.or_else(|| param_str_opt(&tool_state.parameters, &["prompt"]))
.unwrap_or_else(|| "Agent".to_string());
let is_running = matches!(
tool_state.status,
ToolDisplayStatus::Running | ToolDisplayStatus::Streaming
);

let title = format!("{} Task", capitalize_first(&subagent_type));
let title = format!("{} Agent", capitalize_first(&subagent_type));

// Build description line with tool call count (if available)
let desc_text = if let Some(ref progress) = tool_state.subagent_progress {
Expand Down
Loading
Loading