feat(spawn): add async delivery mode control
This commit is contained in:
parent
d37c44db90
commit
b2dc430a5e
3 changed files with 93 additions and 5 deletions
|
|
@ -61,7 +61,7 @@ func (t *SpawnTool) Name() string {
|
|||
}
|
||||
|
||||
func (t *SpawnTool) Description() string {
|
||||
return "Spawn a subagent to handle a task in the background. Use this for complex or time-consuming tasks that can run independently. The subagent will complete the task and report back when done."
|
||||
return "Spawn a subagent to handle a task in the background. Use this for complex or time-consuming tasks that can run independently. The subagent will complete the task and report back when done. Optional delivery_mode controls whether the final async result goes to the user, the parent agent, or both."
|
||||
}
|
||||
|
||||
func (t *SpawnTool) Parameters() map[string]any {
|
||||
|
|
@ -80,6 +80,15 @@ func (t *SpawnTool) Parameters() map[string]any {
|
|||
"type": "string",
|
||||
"description": "Optional target agent ID to delegate the task to",
|
||||
},
|
||||
"delivery_mode": map[string]any{
|
||||
"type": "string",
|
||||
"description": "Optional async result routing policy: user_only, parent_only, or user_and_parent. Defaults to user_only.",
|
||||
"enum": []string{
|
||||
string(AsyncDeliveryUserOnly),
|
||||
string(AsyncDeliveryParentOnly),
|
||||
string(AsyncDeliveryUserAndParent),
|
||||
},
|
||||
},
|
||||
},
|
||||
"required": []string{"task"},
|
||||
}
|
||||
|
|
@ -116,6 +125,10 @@ func (t *SpawnTool) execute(
|
|||
label, _ := args["label"].(string)
|
||||
agentID, _ := args["agent_id"].(string)
|
||||
targetAgentID := strings.TrimSpace(agentID)
|
||||
deliveryMode, err := parseSpawnDeliveryMode(args["delivery_mode"])
|
||||
if err != nil {
|
||||
return ErrorResult(err.Error()).WithError(err)
|
||||
}
|
||||
|
||||
// Check allowlist if targeting a specific agent
|
||||
if targetAgentID != "" && t.allowlistCheck != nil {
|
||||
|
|
@ -131,7 +144,7 @@ func (t *SpawnTool) execute(
|
|||
if cb != nil {
|
||||
wrappedCallback = func(cbCtx context.Context, res *ToolResult) {
|
||||
if res != nil {
|
||||
res.WithAsyncDelivery(AsyncDeliveryUserOnly)
|
||||
res.WithAsyncDelivery(deliveryMode)
|
||||
}
|
||||
cb(cbCtx, res)
|
||||
}
|
||||
|
|
@ -155,6 +168,24 @@ func (t *SpawnTool) execute(
|
|||
return ErrorResult("Subagent manager not configured")
|
||||
}
|
||||
|
||||
func parseSpawnDeliveryMode(raw any) (AsyncDeliveryMode, error) {
|
||||
if raw == nil {
|
||||
return AsyncDeliveryUserOnly, nil
|
||||
}
|
||||
value, ok := raw.(string)
|
||||
if !ok {
|
||||
return "", fmt.Errorf("delivery_mode must be a string")
|
||||
}
|
||||
switch AsyncDeliveryMode(strings.TrimSpace(value)) {
|
||||
case AsyncDeliveryUserOnly, AsyncDeliveryParentOnly, AsyncDeliveryUserAndParent:
|
||||
return AsyncDeliveryMode(strings.TrimSpace(value)), nil
|
||||
case "":
|
||||
return AsyncDeliveryUserOnly, nil
|
||||
default:
|
||||
return "", fmt.Errorf("delivery_mode must be one of: user_only, parent_only, user_and_parent")
|
||||
}
|
||||
}
|
||||
|
||||
func buildSpawnSystemPrompt(task, label string) string {
|
||||
if label != "" {
|
||||
return fmt.Sprintf(
|
||||
|
|
|
|||
|
|
@ -195,3 +195,59 @@ func TestSpawnTool_ExecuteAsync_MarksCallbackResultUserOnly(t *testing.T) {
|
|||
t.Fatal("timed out waiting for spawn callback result")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSpawnTool_ExecuteAsync_RespectsExplicitDeliveryMode(t *testing.T) {
|
||||
provider := &MockLLMProvider{}
|
||||
manager := NewSubagentManager(provider, "test-model", "/tmp/test")
|
||||
tool := NewSpawnTool(manager)
|
||||
spawner := &mockSpawner{}
|
||||
tool.SetSpawner(spawner)
|
||||
|
||||
done := make(chan *ToolResult, 1)
|
||||
result := tool.ExecuteAsync(context.Background(), map[string]any{
|
||||
"task": "Write a haiku about coding",
|
||||
"delivery_mode": string(AsyncDeliveryUserAndParent),
|
||||
}, func(_ context.Context, res *ToolResult) {
|
||||
done <- res
|
||||
})
|
||||
|
||||
if result == nil || !result.Async {
|
||||
t.Fatal("expected async acknowledgment result")
|
||||
}
|
||||
|
||||
select {
|
||||
case cbResult := <-done:
|
||||
if cbResult == nil {
|
||||
t.Fatal("expected callback result")
|
||||
}
|
||||
if cbResult.AsyncDelivery != AsyncDeliveryUserAndParent {
|
||||
t.Fatalf("AsyncDelivery = %q, want %q", cbResult.AsyncDelivery, AsyncDeliveryUserAndParent)
|
||||
}
|
||||
case <-time.After(2 * time.Second):
|
||||
t.Fatal("timed out waiting for spawn callback result")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSpawnTool_Execute_InvalidDeliveryMode(t *testing.T) {
|
||||
provider := &MockLLMProvider{}
|
||||
manager := NewSubagentManager(provider, "test-model", "/tmp/test")
|
||||
tool := NewSpawnTool(manager)
|
||||
|
||||
tests := []map[string]any{
|
||||
{"task": "test", "delivery_mode": 123},
|
||||
{"task": "test", "delivery_mode": "wrong"},
|
||||
}
|
||||
|
||||
for _, args := range tests {
|
||||
result := tool.Execute(context.Background(), args)
|
||||
if result == nil {
|
||||
t.Fatal("expected result")
|
||||
}
|
||||
if !result.IsError {
|
||||
t.Fatalf("expected error for args=%v", args)
|
||||
}
|
||||
if !strings.Contains(result.ForLLM, "delivery_mode") {
|
||||
t.Fatalf("expected delivery_mode error, got: %s", result.ForLLM)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package tools
|
|||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
|
|
@ -273,14 +274,14 @@ After completing the task, provide a clear summary of what was done.`
|
|||
if err != nil {
|
||||
task.Status = "failed"
|
||||
task.Result = fmt.Sprintf("Error: %v", err)
|
||||
// Check if it was canceled
|
||||
if ctx.Err() != nil {
|
||||
// Only report cancellation when cancellation is the actual cause.
|
||||
if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {
|
||||
task.Status = "canceled"
|
||||
task.Result = "Task canceled during execution"
|
||||
}
|
||||
result = &ToolResult{
|
||||
ForLLM: task.Result,
|
||||
ForUser: "",
|
||||
ForUser: task.Result,
|
||||
Silent: false,
|
||||
IsError: true,
|
||||
Async: false,
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue