| 493 | } |
| 494 | |
| 495 | func (r *LocalRuntime) handleTaskTransfer(ctx context.Context, sess *session.Session, toolCall tools.ToolCall, evts EventSink) (*tools.ToolCallResult, error) { |
| 496 | var params struct { |
| 497 | Agent string `json:"agent"` |
| 498 | Task string `json:"task"` |
| 499 | ExpectedOutput string `json:"expected_output"` |
| 500 | } |
| 501 | if err := json.Unmarshal([]byte(toolCall.Function.Arguments), ¶ms); err != nil { |
| 502 | return nil, fmt.Errorf("invalid arguments: %w", err) |
| 503 | } |
| 504 | |
| 505 | a := r.CurrentAgent() |
| 506 | if errResult := validateAgentInList(a.Name(), params.Agent, "transfer task to", "sub-agents list", a.SubAgents()); errResult != nil { |
| 507 | return errResult, nil |
| 508 | } |
| 509 | |
| 510 | slog.DebugContext(ctx, "Transferring task to agent", "from_agent", a.Name(), "to_agent", params.Agent, "task", params.Task) |
| 511 | |
| 512 | delegationAttrs := []attribute.KeyValue{ |
| 513 | attribute.String(genai.AttrOperationName, genai.OperationInvokeAgent), |
| 514 | // gen_ai.agent.name identifies the target agent of the invoke_agent |
| 515 | // operation per the OTel GenAI semconv (Required). cagent.agent.name |
| 516 | // is the same value but in our internal namespace; we emit both so |
| 517 | // spec-aware backends and existing cagent dashboards both see it. |
| 518 | attribute.String(genai.AttrAgentName, params.Agent), |
| 519 | attribute.String("cagent.delegation.from_agent", a.Name()), |
| 520 | attribute.String("cagent.delegation.to_agent", params.Agent), |
| 521 | attribute.String("cagent.delegation.kind", "transfer_task"), |
| 522 | attribute.String(genai.AttrConversationID, sess.ID), |
| 523 | attribute.String(genai.AttrAgentNameRuntime, params.Agent), |
| 524 | } |
| 525 | if params.Task != "" { |
| 526 | // Task length is bounded enough to be useful as a span |
| 527 | // attribute for debugging "agent X transferred which task |
| 528 | // to Y". The full task body lands on the sub-session's |
| 529 | // runtime.session span when content capture is opt-in. |
| 530 | delegationAttrs = append(delegationAttrs, attribute.Int("cagent.delegation.task_length", len(params.Task))) |
| 531 | } |
| 532 | if genai.EmitLegacyAttributes() { |
| 533 | delegationAttrs = append(delegationAttrs, |
| 534 | attribute.String("from.agent", a.Name()), |
| 535 | attribute.String("to.agent", params.Agent), |
| 536 | attribute.String("session.id", sess.ID), |
| 537 | ) |
| 538 | } |
| 539 | ctx, span := r.startSpan(ctx, "runtime.task_transfer", trace.WithAttributes(delegationAttrs...)) |
| 540 | defer span.End() |
| 541 | |
| 542 | return r.runForwarding(ctx, sess, evts, delegationRequest{ |
| 543 | SubSessionConfig: SubSessionConfig{ |
| 544 | Task: params.Task, |
| 545 | ExpectedOutput: params.ExpectedOutput, |
| 546 | AgentName: params.Agent, |
| 547 | Title: "Transferred task", |
| 548 | ToolsApproved: sess.ToolsApproved, |
| 549 | NonInteractive: sess.NonInteractive, |
| 550 | }, |
| 551 | SwitchCurrentAgent: true, |
| 552 | }) |