Skip to content

Commit bd54fa2

Browse files
authored
Merge pull request #1546 from trheyi/main
Enhance message handling with execute family and question types
2 parents fec35a0 + 35a6632 commit bd54fa2

7 files changed

Lines changed: 91 additions & 26 deletions

File tree

agent/assistant/handlers/stream.go

Lines changed: 28 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -294,14 +294,24 @@ func (s *streamState) handleToolCall(data []byte) int {
294294
return 0 // Continue
295295
}
296296

297+
// isExecuteFamily checks if a message type belongs to the execute family
298+
// (execute itself or any semantic sub-type: agent, todo, plan, job).
299+
// These all share the same two-phase lifecycle (running → completed/error).
300+
func isExecuteFamily(t string) bool {
301+
switch t {
302+
case message.TypeExecute, message.TypeAgent, message.TypeTodo, message.TypePlan, message.TypeJob, message.TypeQuestion:
303+
return true
304+
}
305+
return false
306+
}
307+
297308
// handleExecute handles execute observation chunks from sandbox CLI agents.
298309
// These represent tool actions observed inside the agent runtime (e.g., Bash, Read, Write).
299310
func (s *streamState) handleExecute(data []byte) int {
300311
if len(data) == 0 {
301312
return 0
302313
}
303314

304-
s.currentType = message.TypeExecute
305315
s.buffer = append(s.buffer, data...)
306316
s.chunkCount++
307317
s.messageSeq++
@@ -322,14 +332,18 @@ func (s *streamState) handleExecute(data []byte) int {
322332
s.lastExecProps[k] = v
323333
}
324334

325-
deltaAction := "merge"
335+
msgType := message.TypeExecute
336+
if st, ok := props["semantic_type"].(string); ok && st != "" {
337+
msgType = st
338+
}
339+
s.currentType = msgType
326340

327341
msg := &message.Message{
328342
ChunkID: s.ctx.IDGenerator.GenerateChunkID(),
329343
MessageID: s.currentGroupID,
330-
Type: message.TypeExecute,
344+
Type: msgType,
331345
Delta: true,
332-
DeltaAction: deltaAction,
346+
DeltaAction: "merge",
333347
Props: props,
334348
}
335349

@@ -408,13 +422,14 @@ func (s *streamState) handleMessageEnd(data []byte) int {
408422
shouldSkipHistory := s.ctx.Stack != nil && s.ctx.Stack.Options != nil &&
409423
s.ctx.Stack.Options.Skip != nil && s.ctx.Stack.Options.Skip.History
410424

411-
// Execute messages have two (or more) phases sharing the same message_id:
425+
// Execute-family messages (execute, agent, todo, plan, job) have two-phase lifecycles:
412426
// 1. running / suspended / resumed — streamed for UI display only, NOT persisted
413427
// 2. completed / error — the final state, persisted to the buffer
414428
// Only persist when we have an explicit terminal status.
415-
isExecuteFinal := msgType == message.TypeExecute &&
429+
isExecFamily := isExecuteFamily(msgType)
430+
isExecuteFinal := isExecFamily &&
416431
(s.lastExecStatus == "completed" || s.lastExecStatus == "error")
417-
skipExecute := msgType == message.TypeExecute && !isExecuteFinal
432+
skipExecute := isExecFamily && !isExecuteFinal
418433

419434
if s.ctx.Buffer != nil && len(s.buffer) > 0 && !shouldSkipHistory && !skipExecute {
420435
assistantID := ""
@@ -423,8 +438,8 @@ func (s *streamState) handleMessageEnd(data []byte) int {
423438
}
424439

425440
var props map[string]interface{}
426-
switch msgType {
427-
case message.TypeToolCall:
441+
switch {
442+
case msgType == message.TypeToolCall:
428443
var toolCallData interface{}
429444
if err := jsoniter.Unmarshal(s.buffer, &toolCallData); err == nil {
430445
props = map[string]interface{}{
@@ -435,16 +450,10 @@ func (s *streamState) handleMessageEnd(data []byte) int {
435450
"content": string(s.buffer),
436451
}
437452
}
438-
case message.TypeExecute:
439-
if s.lastExecProps != nil {
440-
props = make(map[string]interface{}, len(s.lastExecProps))
441-
for k, v := range s.lastExecProps {
442-
props[k] = v
443-
}
444-
} else {
445-
props = map[string]interface{}{
446-
"content": string(s.buffer),
447-
}
453+
case s.lastExecProps != nil:
454+
props = make(map[string]interface{}, len(s.lastExecProps))
455+
for k, v := range s.lastExecProps {
456+
props[k] = v
448457
}
449458
default:
450459
props = map[string]interface{}{

agent/output/builtin.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,7 @@ func NewVideoMessage(url string) *message.Message {
156156
// IsBuiltinType checks if a message type is a built-in type
157157
func IsBuiltinType(msgType string) bool {
158158
switch msgType {
159-
case message.TypeUserInput, message.TypeText, message.TypeThinking, message.TypeLoading, message.TypeToolCall, message.TypeExecute, message.TypeError, message.TypeImage, message.TypeAudio, message.TypeVideo, message.TypeAction, message.TypeEvent:
159+
case message.TypeUserInput, message.TypeText, message.TypeThinking, message.TypeLoading, message.TypeToolCall, message.TypeExecute, message.TypeAgent, message.TypeTodo, message.TypePlan, message.TypeJob, message.TypeQuestion, message.TypeError, message.TypeImage, message.TypeAudio, message.TypeVideo, message.TypeAction, message.TypeEvent:
160160
return true
161161
default:
162162
return false

agent/output/message/types.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,13 @@ const (
7878
// Agent execution observation types
7979
TypeExecute = "execute" // Agent tool execution observation (sandbox CLI agent actions, not LLM tool_call requests)
8080

81+
// Semantic execute sub-types (runner-annotated, rendered by dedicated frontend components)
82+
TypeAgent = "agent" // Sub-agent dispatch (Claude Agent tool, A2A calls)
83+
TypeTodo = "todo" // Structured task checklist (Claude TodoWrite tool)
84+
TypePlan = "plan" // Plan mode transitions (Claude EnterPlanMode/ExitPlanMode)
85+
TypeJob = "job" // Async background jobs (Claude TaskCreate/TaskGet/... — Unix job semantics)
86+
TypeQuestion = "question" // Interactive user questions (Claude AskUserQuestion)
87+
8188
// System types (not visible in standard chat clients)
8289
TypeAction = "action" // System action (open panel, navigate, etc.) - silent in OpenAI clients
8390
TypeEvent = "event" // Lifecycle event (stream_start, stream_end, etc.) - CUI only, silent in OpenAI clients

agent/sandbox/v2/claude/parse.go

Lines changed: 52 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -376,12 +376,14 @@ func (p *streamParser) onContentBlockStart(event map[string]any) (stopped bool)
376376
return false
377377
}
378378

379-
return p.emitExecute(map[string]any{
379+
execProps := map[string]any{
380380
"tool": toolName,
381381
"tool_id": toolID,
382382
"status": "running",
383383
"runner": "claude-cli",
384-
})
384+
}
385+
injectSemanticType(execProps, toolName)
386+
return p.emitExecute(execProps)
385387
}
386388

387389
func (p *streamParser) onContentBlockStop() (stopped bool) {
@@ -489,14 +491,16 @@ func (p *streamParser) handleAssistant(msg map[string]any) (stopped bool) {
489491
summary := extractSummary(toolName, inputStr)
490492
p.toolSummaries[toolID] = summary
491493

492-
if p.emitExecute(map[string]any{
494+
execProps := map[string]any{
493495
"tool": toolName,
494496
"tool_id": toolID,
495497
"input": json.RawMessage(inputRaw),
496498
"summary": summary,
497499
"status": "running",
498500
"runner": "claude-cli",
499-
}) {
501+
}
502+
injectSemanticType(execProps, toolName)
503+
if p.emitExecute(execProps) {
500504
p.endMessage()
501505
return true
502506
}
@@ -572,15 +576,18 @@ func (p *streamParser) handleUser(msg map[string]any) (stopped bool) {
572576
"status": status,
573577
"is_error": isError,
574578
}
579+
toolName := ""
575580
if name, ok := p.toolNames[toolUseID]; ok {
576581
execProps["tool"] = name
582+
toolName = name
577583
}
578584
if input, ok := p.toolInputs[toolUseID]; ok {
579585
execProps["input"] = json.RawMessage(input)
580586
}
581587
if summary, ok := p.toolSummaries[toolUseID]; ok {
582588
execProps["summary"] = summary
583589
}
590+
injectSemanticType(execProps, toolName)
584591

585592
if reuseMsgID, ok := p.toolMsgIDs[toolUseID]; ok {
586593
if p.beginMessageWithID(reuseMsgID, "execute") {
@@ -653,3 +660,44 @@ func (p *streamParser) handleError(msg map[string]any) error {
653660
}
654661
return nil
655662
}
663+
664+
// resolveSemanticType maps Claude Code CLI tool names to semantic types.
665+
// Returns ("", nil) for tools that should remain as "execute".
666+
func resolveSemanticType(tool string) (string, map[string]any) {
667+
switch tool {
668+
case "Agent":
669+
return message.TypeAgent, nil
670+
case "TodoWrite":
671+
return message.TypeTodo, map[string]any{"action": "write"}
672+
case "EnterPlanMode":
673+
return message.TypePlan, map[string]any{"action": "enter"}
674+
case "ExitPlanMode":
675+
return message.TypePlan, map[string]any{"action": "exit"}
676+
case "TaskCreate":
677+
return message.TypeJob, map[string]any{"action": "create"}
678+
case "TaskGet":
679+
return message.TypeJob, map[string]any{"action": "get"}
680+
case "TaskList":
681+
return message.TypeJob, map[string]any{"action": "list"}
682+
case "TaskOutput":
683+
return message.TypeJob, map[string]any{"action": "output"}
684+
case "TaskStop":
685+
return message.TypeJob, map[string]any{"action": "stop"}
686+
case "TaskUpdate":
687+
return message.TypeJob, map[string]any{"action": "update"}
688+
case "AskUserQuestion":
689+
return message.TypeQuestion, nil
690+
}
691+
return "", nil
692+
}
693+
694+
// injectSemanticType annotates execute props with semantic_type and extra metadata.
695+
func injectSemanticType(props map[string]any, toolName string) {
696+
semanticType, extraProps := resolveSemanticType(toolName)
697+
if semanticType != "" {
698+
props["semantic_type"] = semanticType
699+
for k, v := range extraProps {
700+
props[k] = v
701+
}
702+
}
703+
}

agent/sandbox/v2/claude/runner.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ func (r *Runner) Prepare(ctx context.Context, req *types.PrepareRequest) error {
6161
if err := shared.InjectSystemSkills(ws, tools.SkillsFS, ".claude/skills"); err != nil {
6262
r.logger.Warn("inject system skills: %v", err)
6363
}
64-
if err := shared.InjectAgentDefinitions(ws, tools.AgentsFS, ".claude/agents"); err != nil {
64+
if err := shared.InjectAgentDefinitions(ws, tools.AgentsFS, prefix+"/agents"); err != nil {
6565
r.logger.Warn("inject agent definitions: %v", err)
6666
}
6767
if err := shared.AppendSystemPrompt(ws, "CLAUDE.md", tools.SystemPrompt); err != nil {

tools/agent/call_schema.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
{
22
"name": "agent_call",
33
"description": "Call another AI expert agent with a message and get the response. Use when the user @mentions an expert or when you need specialized help from another agent.",
4+
"process": "tools.agent_call",
45
"inputSchema": {
56
"type": "object",
67
"properties": {

tools/prompts/system-tools.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,7 @@ The system skills (`yao-web`, `yao-process`, `yao-doc`, `yao-image`, `yao-agent`
110110

111111
User messages may contain `<Mention>` tags referencing experts, workspaces, files, or directories:
112112

113-
- `<Mention type="expert" value="assistant_id">Name</Mention>` — The user wants to involve this AI expert. Use the Agent tool with `subagent_type="a2a"` to delegate the task, passing the `assistant_id` and relevant context in the prompt.
113+
- `<Mention type="expert" value="assistant_id">Name</Mention>` — The user wants to involve this AI expert. Use `tai tool agent_call '{"assistant_id":"<the value>","message":"<relevant query>"}'` to call the expert and get their response.
114114
- `<Mention type="workspace" value="workspace_id">Name</Mention>` — References a workspace. Use `workspace_file_list` and `workspace_file_read` to access its files.
115115
- `<Mention type="file" value="workspace://wsId/path">Filename</Mention>` — References a specific file. Use `workspace_file_read` to read its content.
116116
- `<Mention type="directory" value="workspace://wsId/path">DirName</Mention>` — References a directory. Use `workspace_file_list` to browse its contents first, then `workspace_file_read` for specific files.

0 commit comments

Comments
 (0)