@@ -940,3 +940,95 @@ func (c *coordinator) refreshApiKeyTemplate(ctx context.Context, providerCfg con
940940 }
941941 return nil
942942}
943+
944+// runSubAgent runs a sub-agent and handles session management and cost accumulation.
945+// It creates a sub-session, runs the agent with the given prompt, and propagates
946+// the cost to the parent session.
947+func (c *coordinator) runSubAgent(
948+ ctx context.Context,
949+ agent SessionAgent,
950+ sessionID, agentMessageID, toolCallID string,
951+ prompt string,
952+ sessionTitle string,
953+) (fantasy.ToolResponse, error) {
954+ return c.runSubAgentWithOptions(ctx, agent, sessionID, agentMessageID, toolCallID, prompt, sessionTitle, nil)
955+}
956+
957+// runSubAgentWithOptions runs a sub-agent with additional session configuration options.
958+// The sessionSetup function is called after session creation but before agent execution.
959+func (c *coordinator) runSubAgentWithOptions(
960+ ctx context.Context,
961+ agent SessionAgent,
962+ sessionID, agentMessageID, toolCallID string,
963+ prompt string,
964+ sessionTitle string,
965+ sessionSetup func(sessionID string),
966+) (fantasy.ToolResponse, error) {
967+ // Create sub-session
968+ agentToolSessionID := c.sessions.CreateAgentToolSessionID(agentMessageID, toolCallID)
969+ session, err := c.sessions.CreateTaskSession(ctx, agentToolSessionID, sessionID, sessionTitle)
970+ if err != nil {
971+ return fantasy.ToolResponse{}, fmt.Errorf("create session: %w", err)
972+ }
973+
974+ // Call session setup function if provided
975+ if sessionSetup != nil {
976+ sessionSetup(session.ID)
977+ }
978+
979+ // Get model configuration
980+ model := agent.Model()
981+ maxTokens := model.CatwalkCfg.DefaultMaxTokens
982+ if model.ModelCfg.MaxTokens != 0 {
983+ maxTokens = model.ModelCfg.MaxTokens
984+ }
985+
986+ providerCfg, ok := c.cfg.Providers.Get(model.ModelCfg.Provider)
987+ if !ok {
988+ return fantasy.ToolResponse{}, errors.New("model provider not configured")
989+ }
990+
991+ // Run the agent
992+ result, err := agent.Run(ctx, SessionAgentCall{
993+ SessionID: session.ID,
994+ Prompt: prompt,
995+ MaxOutputTokens: maxTokens,
996+ ProviderOptions: getProviderOptions(model, providerCfg),
997+ Temperature: model.ModelCfg.Temperature,
998+ TopP: model.ModelCfg.TopP,
999+ TopK: model.ModelCfg.TopK,
1000+ FrequencyPenalty: model.ModelCfg.FrequencyPenalty,
1001+ PresencePenalty: model.ModelCfg.PresencePenalty,
1002+ })
1003+ if err != nil {
1004+ return fantasy.NewTextErrorResponse("error generating response"), nil
1005+ }
1006+
1007+ // Update parent session cost
1008+ if err := c.updateParentSessionCost(ctx, session.ID, sessionID); err != nil {
1009+ return fantasy.ToolResponse{}, err
1010+ }
1011+
1012+ return fantasy.NewTextResponse(result.Response.Content.Text()), nil
1013+}
1014+
1015+// updateParentSessionCost accumulates the cost from a child session to its parent session.
1016+func (c *coordinator) updateParentSessionCost(ctx context.Context, childSessionID, parentSessionID string) error {
1017+ childSession, err := c.sessions.Get(ctx, childSessionID)
1018+ if err != nil {
1019+ return fmt.Errorf("get child session: %w", err)
1020+ }
1021+
1022+ parentSession, err := c.sessions.Get(ctx, parentSessionID)
1023+ if err != nil {
1024+ return fmt.Errorf("get parent session: %w", err)
1025+ }
1026+
1027+ parentSession.Cost += childSession.Cost
1028+
1029+ if _, err := c.sessions.Save(ctx, parentSession); err != nil {
1030+ return fmt.Errorf("save parent session: %w", err)
1031+ }
1032+
1033+ return nil
1034+}