diff --git a/apps/backend/internal/mcp/handlers/message_task_test.go b/apps/backend/internal/mcp/handlers/message_task_test.go index d16990cf58..6a08c229c4 100644 --- a/apps/backend/internal/mcp/handlers/message_task_test.go +++ b/apps/backend/internal/mcp/handlers/message_task_test.go @@ -31,16 +31,17 @@ type fakeOrchestrator struct { queue *messagequeue.Service - promptCalls []promptCall - startCreatedCalls []startCreatedCall - resumeCalls int - turnStartCalls []turnStartCall - onTurnStart func(context.Context, string, string) error - interruptCalls []interruptCall - launchCalls []*orchestrator.LaunchSessionRequest - launchErr error - renameCalls []renameCall - renameErr error + promptCalls []promptCall + startCreatedCalls []startCreatedCall + resumeCalls int + turnStartCalls []turnStartCall + onTurnStart func(context.Context, string, string) error + interruptCalls []interruptCall + launchCalls []*orchestrator.LaunchSessionRequest + launchErr error + launchResponseProfileID string + renameCalls []renameCall + renameErr error // Configurable: error returned by PromptTask. Cleared after first call so // the retry-after-resume path can succeed on the second call. @@ -99,12 +100,18 @@ func (f *fakeOrchestrator) LaunchSession(_ context.Context, req *orchestrator.La if f.launchErr != nil { return nil, f.launchErr } - return &orchestrator.LaunchSessionResponse{ + response := &orchestrator.LaunchSessionResponse{ Success: true, TaskID: req.TaskID, SessionID: "spawned-sess-1", State: "STARTING", - }, nil + } + profileID := req.AgentProfileID + if f.launchResponseProfileID != "" { + profileID = f.launchResponseProfileID + } + response.AgentProfileID = profileID + return response, nil } func (f *fakeOrchestrator) PromptTask(_ context.Context, taskID, sessionID, prompt, _ string, _ bool, _ []v1.MessageAttachment, dispatchOnly bool) (*orchestrator.PromptResult, error) { diff --git a/apps/backend/internal/mcp/handlers/spawn_session.go b/apps/backend/internal/mcp/handlers/spawn_session.go index f93c720548..3557c6b626 100644 --- a/apps/backend/internal/mcp/handlers/spawn_session.go +++ b/apps/backend/internal/mcp/handlers/spawn_session.go @@ -67,7 +67,7 @@ func (h *Handlers) handleSpawnSession(ctx context.Context, msg *ws.Message) (*ws "task_id": req.TaskID, "session_id": resp.SessionID, "state": resp.State, - "agent_profile_id": profileID, + "agent_profile_id": resp.AgentProfileID, }) } diff --git a/apps/backend/internal/mcp/handlers/spawn_session_test.go b/apps/backend/internal/mcp/handlers/spawn_session_test.go index c51e0b74f0..8703a37644 100644 --- a/apps/backend/internal/mcp/handlers/spawn_session_test.go +++ b/apps/backend/internal/mcp/handlers/spawn_session_test.go @@ -5,6 +5,7 @@ import ( "encoding/json" "errors" "testing" + "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -12,6 +13,8 @@ import ( "github.com/kandev/kandev/internal/orchestrator" "github.com/kandev/kandev/internal/task/models" "github.com/kandev/kandev/internal/task/service" + workflowmodels "github.com/kandev/kandev/internal/workflow/models" + v1 "github.com/kandev/kandev/pkg/api/v1" ws "github.com/kandev/kandev/pkg/websocket" ) @@ -106,6 +109,89 @@ func TestHandleSpawnSession_ExplicitProfile(t *testing.T) { assert.Empty(t, orch.renameCalls) } +func TestHandleSpawnSessionReportsEffectiveAgentProfile(t *testing.T) { + svc, repo := newTestTaskService(t) + _, target, sess := seedTaskWithSession(t, svc, repo, models.TaskSessionStateRunning) + + h, orch := newMessageTaskHandler(t, svc) + orch.launchResponseProfileID = "workflow-default-profile" + + payload := spawnPayload(target.ID, "work with the workflow profile", target.ID, sess.ID) + payload["agent_profile_id"] = "requested-profile" + msg := makeWSMessage(t, ws.ActionMCPSpawnSession, payload) + resp, err := h.handleSpawnSession(context.Background(), msg) + require.NoError(t, err) + require.Equal(t, ws.MessageTypeResponse, resp.Type) + + var out map[string]interface{} + require.NoError(t, json.Unmarshal(resp.Payload, &out)) + assert.Equal(t, "workflow-default-profile", out["agent_profile_id"]) + assert.Equal(t, "requested-profile", orch.launchCalls[0].AgentProfileID) +} + +func TestHandleSpawnSessionReportsStepPinnedAgentProfile(t *testing.T) { + svc, repo, _, workflowRepo := newTestTaskServiceWithWorkflow(t) + ctx := context.Background() + workspace, workflow := defaultWorkspaceAndWorkflow(t, ctx, svc) + workflowProfileID := "workflow-default-profile" + _, err := svc.UpdateWorkflow(ctx, workflow.ID, &service.UpdateWorkflowRequest{ + AgentProfileID: &workflowProfileID, + }) + require.NoError(t, err) + step := seedWorkflowStep(t, ctx, workflowRepo, &workflowmodels.WorkflowStep{ + WorkflowID: workflow.ID, + Name: "Pinned", + Position: 0, + IsStartStep: true, + AgentProfileID: "step-pinned-profile", + AllowManualMove: true, + }) + now := time.Now().UTC() + target := &models.Task{ + ID: "task-pinned-target", + WorkspaceID: workspace.ID, + WorkflowID: workflow.ID, + WorkflowStepID: step.ID, + Title: "Target task", + State: v1.TaskStateInProgress, + CreatedAt: now, + UpdatedAt: now, + } + sender := &models.Task{ + ID: "task-pinned-sender", + WorkspaceID: workspace.ID, + WorkflowID: workflow.ID, + Title: "Sender task", + State: v1.TaskStateInProgress, + CreatedAt: now, + UpdatedAt: now, + } + require.NoError(t, repo.CreateTask(ctx, target)) + require.NoError(t, repo.CreateTask(ctx, sender)) + session := &models.TaskSession{ + ID: "sess-pinned", + TaskID: target.ID, + AgentProfileID: "sender-profile", + IsPrimary: true, + State: models.TaskSessionStateRunning, + } + require.NoError(t, repo.CreateTaskSession(ctx, session)) + + h, orch := newMessageTaskHandler(t, svc, repo) + orch.launchResponseProfileID = step.AgentProfileID + payload := spawnPayload(target.ID, "use the pinned profile", sender.ID, session.ID) + payload["agent_profile_id"] = "explicit-profile" + msg := makeWSMessage(t, ws.ActionMCPSpawnSession, payload) + resp, err := h.handleSpawnSession(ctx, msg) + require.NoError(t, err) + require.Equal(t, ws.MessageTypeResponse, resp.Type) + + var out map[string]interface{} + require.NoError(t, json.Unmarshal(resp.Payload, &out)) + assert.Equal(t, "step-pinned-profile", out["agent_profile_id"]) + assert.Equal(t, "explicit-profile", orch.launchCalls[0].AgentProfileID) +} + // Cross-task spawns (sender session belongs to another task) fall back to the // target task's primary-session profile. func TestHandleSpawnSession_CrossTask_DefaultsToTargetPrimaryProfile(t *testing.T) { diff --git a/apps/backend/internal/mcp/server/server.go b/apps/backend/internal/mcp/server/server.go index 5937d91e18..869444cc4a 100644 --- a/apps/backend/internal/mcp/server/server.go +++ b/apps/backend/internal/mcp/server/server.go @@ -974,9 +974,11 @@ Unlike create_task_kandev this does NOT create a new task — the new session ru The spawned session knows it was spawned by you and can reply via message_task_kandev using your task_id + session_id. You can message it the same way using the session_id returned by this tool. -Returns {task_id, session_id, state}.`), +The returned agent_profile_id is the effective agent profile used by the new session. On a workflow step, the step's launch profile wins: a pinned step profile outranks the requested agent_profile_id, and an unpinned step uses the workflow default. Without a workflow launch profile, an explicit agent_profile_id wins; when omitted, the existing inheritance rules apply. + +Returns {task_id, session_id, state, agent_profile_id}.`), mcp.WithString("prompt", mcp.Required(), mcp.Description("The spawned session's initial prompt. This is the ONLY context the new agent receives — be specific and detailed.")), - mcp.WithString("agent_profile_id", mcp.Description("Agent profile for the new session. Omit to reuse your own session's agent profile.")), + mcp.WithString("agent_profile_id", mcp.Description("Requested agent profile for the new session. Omit to inherit your session's profile; a workflow launch profile may override it.")), mcp.WithString("name", mcp.Description("Optional session name shown on the session tab (e.g. 'reviewer'). Helps the user tell concurrent sessions apart.")), mcp.WithString("task_id", mcp.Description("Task to spawn the session on. Omit to use your current task.")), ), diff --git a/apps/backend/internal/mcp/server/spawn_session_tool_test.go b/apps/backend/internal/mcp/server/spawn_session_tool_test.go new file mode 100644 index 0000000000..73dcbf69f7 --- /dev/null +++ b/apps/backend/internal/mcp/server/spawn_session_tool_test.go @@ -0,0 +1,24 @@ +package mcp + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestSpawnSessionToolDescribesEffectiveAgentProfile(t *testing.T) { + s := newTaskModeServer(t, &testBackend{}, "task-current") + + tool, ok := s.mcpServer.ListTools()["spawn_session_kandev"] + require.True(t, ok, "spawn_session_kandev must be registered in task mode") + + description := tool.Tool.Description + assert.Contains(t, description, "agent_profile_id") + assert.Contains(t, description, "effective agent profile") + assert.Contains(t, description, "Returns {task_id, session_id, state, agent_profile_id}") + + profileProperty, ok := toolInputProperties(t, s, "spawn_session_kandev")["agent_profile_id"].(map[string]interface{}) + require.True(t, ok, "agent_profile_id input must be described") + assert.Contains(t, profileProperty["description"], "workflow launch profile may override it") +} diff --git a/apps/backend/internal/orchestrator/session_launch.go b/apps/backend/internal/orchestrator/session_launch.go index fd04c5f354..eb67cb6cf2 100644 --- a/apps/backend/internal/orchestrator/session_launch.go +++ b/apps/backend/internal/orchestrator/session_launch.go @@ -91,6 +91,7 @@ type LaunchSessionResponse struct { TaskID string `json:"task_id"` SessionID string `json:"session_id,omitempty"` AgentExecutionID string `json:"agent_execution_id,omitempty"` + AgentProfileID string `json:"agent_profile_id,omitempty"` State string `json:"state"` WorktreePath *string `json:"worktree_path,omitempty"` WorktreeBranch *string `json:"worktree_branch,omitempty"` @@ -415,6 +416,7 @@ func executionToLaunchResponse(taskID string, exec *executor.TaskExecution) *Lau TaskID: taskID, SessionID: exec.SessionID, AgentExecutionID: exec.AgentExecutionID, + AgentProfileID: exec.AgentProfileID, State: string(exec.SessionState), } if exec.WorktreePath != "" { diff --git a/apps/backend/internal/orchestrator/session_launch_test.go b/apps/backend/internal/orchestrator/session_launch_test.go index f287995c47..531ee03ca2 100644 --- a/apps/backend/internal/orchestrator/session_launch_test.go +++ b/apps/backend/internal/orchestrator/session_launch_test.go @@ -8,12 +8,25 @@ import ( "time" "github.com/kandev/kandev/internal/agent/runtime/lifecycle" + "github.com/kandev/kandev/internal/orchestrator/executor" "github.com/kandev/kandev/internal/task/models" sqliterepo "github.com/kandev/kandev/internal/task/repository/sqlite" wfmodels "github.com/kandev/kandev/internal/workflow/models" v1 "github.com/kandev/kandev/pkg/api/v1" ) +func TestExecutionToLaunchResponseReportsAgentProfile(t *testing.T) { + response := executionToLaunchResponse("task-1", &executor.TaskExecution{ + SessionID: "session-1", + AgentExecutionID: "execution-1", + AgentProfileID: "effective-profile", + SessionState: v1.TaskSessionStateRunning, + }) + if got, want := response.AgentProfileID, "effective-profile"; got != want { + t.Fatalf("agent_profile_id = %v, want %v", got, want) + } +} + func TestResolveIntent(t *testing.T) { tests := []struct { name string diff --git a/docs/plans/spawn-session-effective-profile/plan.md b/docs/plans/spawn-session-effective-profile/plan.md new file mode 100644 index 0000000000..40c035dcb9 --- /dev/null +++ b/docs/plans/spawn-session-effective-profile/plan.md @@ -0,0 +1,93 @@ +--- +spec: docs/specs/spawn-session-effective-profile/spec.md +created: 2026-08-12 +status: complete +--- + +# Implementation Plan: Spawn Session Effective Agent Profile + +## Overview + +`spawn_session_kandev` sends a requested profile to `LaunchSession`, then reports that request value. The orchestrator can replace it with the workflow launch profile. + +The launch path already retains the effective profile in `executor.TaskExecution`. The fix adds it to `LaunchSessionResponse` and uses that value in the MCP response. + +## Confirmed root cause + +`resolveSpawnAgentProfile` selects the pre-launch profile. `handleSpawnSession` reports that value after `LaunchSession` returns. + +`startTask` can replace that value through `resolveEffectiveAgentProfile`. `executionToLaunchResponse` drops the resulting `TaskExecution.AgentProfileID`, so the MCP handler cannot report it. + +`create_task_kandev` does not need a code change. PR #2341 already aligns its stored and reported profile with workflow-default precedence. + +## Backend + +### Authoritative launch response + +- Update `apps/backend/internal/orchestrator/session_launch.go`. +- Add `AgentProfileID` to `LaunchSessionResponse`. +- Copy `TaskExecution.AgentProfileID` in `executionToLaunchResponse`. +- Keep launch precedence and session persistence unchanged. + +### MCP response and tool contract + +- Update `apps/backend/internal/mcp/handlers/spawn_session.go`. +- Build `agent_profile_id` from `LaunchSessionResponse.AgentProfileID`. +- Update `apps/backend/internal/mcp/server/server.go`. +- State workflow profile precedence and the full success response shape. + +## Tests + +- **What:** The launch response carries the effective profile from `TaskExecution`. + **File:** `apps/backend/internal/orchestrator/session_launch_test.go`. + **How:** Pass a `TaskExecution` with an effective profile to `executionToLaunchResponse`. +- **What:** The MCP handler reports the launch result when it differs from the requested profile. + **File:** `apps/backend/internal/mcp/handlers/spawn_session_test.go`. + **How:** Use the real task service and SQLite repository for target authorization. Return a different effective profile from the fake launcher. +- **What:** The registered tool describes workflow precedence and the `agent_profile_id` response field. + **File:** `apps/backend/internal/mcp/server/spawn_session_tool_test.go`. + **How:** Inspect the registered tool description without adding tests to existing files above the test-file size limit. +- **What:** The existing `create_task_kandev` workflow-default regressions remain green. + **File:** `apps/backend/internal/mcp/handlers/handlers_test.go`. + **How:** Run the focused tests from PR #2341 without changing them. + +## Public documentation + +Update `docs/public/agent-communication.md`, which is the explanation page for task-agent messages and related MCP tools. Add the effective profile to both documented response shapes and explain workflow precedence. + +No frontend or E2E change is required. The change affects an MCP response and its documentation, not a visual UI flow. + +## Verification Results + +- Task 01 red regressions failed as expected: the launch response omitted the + effective profile, the MCP handler returned the requested profile, and the + tool description omitted the effective response field. +- Task 01 exact verification passed: 6 tests across the orchestrator, MCP + handlers, and MCP server packages. +- Affected backend package verification passed: `go test ./internal/orchestrator + ./internal/mcp/handlers ./internal/mcp/server -count=1`. +- Full backend verification passed: `make -C apps/backend test` ran + `CGO_ENABLED=1 go test -tags fts5 ./...` successfully. +- Backend lint passed: `make -C apps/backend lint` reported no issues. +- Task 02 public-doc verification passed: 60 validation tests and 41 published + pages validated. +- Formatting and whitespace checks passed: `gofmt -l` reported no files and + `git diff --check` reported no errors. + +## Implementation Waves And Parallel Candidates + +Wave 1: + +- [x] [Task 01: Propagate the effective profile](task-01-propagate-effective-profile.md) + +Wave 2: + +- [x] [Task 02: Update public MCP documentation](task-02-update-public-docs.md) + +Execution is sequential. The documentation task depends on the final response contract from Task 01. + +## Risks and limits + +- `LaunchSessionResponse` serves several launch intents. The new field is additive and comes from the existing execution record. +- `spawn_session_kandev` always uses `IntentStart`, so its successful response has an effective execution profile. +- This work does not change profile precedence. It only reports the result of existing precedence. diff --git a/docs/plans/spawn-session-effective-profile/task-01-propagate-effective-profile.md b/docs/plans/spawn-session-effective-profile/task-01-propagate-effective-profile.md new file mode 100644 index 0000000000..0299475e6b --- /dev/null +++ b/docs/plans/spawn-session-effective-profile/task-01-propagate-effective-profile.md @@ -0,0 +1,84 @@ +--- +id: "01-propagate-effective-profile" +title: "Propagate the effective profile" +status: done +wave: 1 +depends_on: [] +plan: "plan.md" +spec: "../../specs/spawn-session-effective-profile/spec.md" +--- + +# Task 01: Propagate the effective profile + +## Intent + +Make the synchronous launch result authoritative for the profile that `spawn_session_kandev` reports. + +## Inputs + +- The spec `What`, `API surface`, and `Scenarios` sections. +- The confirmed root cause in `plan.md`. +- The response propagation pattern in `executionToLaunchResponse`. +- The existing handler test setup in `spawn_session_test.go` and `message_task_test.go`. + +## Acceptance + +- `LaunchSessionResponse.AgentProfileID` equals the effective profile in `TaskExecution` for a successful start. +- `spawn_session_kandev` reports that effective profile, including workflow-default and step-pinned overrides. +- The tool description states the precedence and includes `agent_profile_id` in its response shape. +- Existing `create_task_kandev` workflow-default tests remain green. + +## Files likely touched + +- `apps/backend/internal/orchestrator/session_launch.go` +- `apps/backend/internal/orchestrator/session_launch_test.go` +- `apps/backend/internal/mcp/handlers/spawn_session.go` +- `apps/backend/internal/mcp/handlers/spawn_session_test.go` +- `apps/backend/internal/mcp/handlers/message_task_test.go` +- `apps/backend/internal/mcp/server/server.go` +- `apps/backend/internal/mcp/server/spawn_session_tool_test.go` + +## TDD sequence + +1. Add the launch-response and MCP-response regressions. +2. Run the focused command and make sure that the new regressions fail for the expected missing field or echoed value. +3. Add the response field and propagate the effective value. +4. Update the tool description and its contract test. +5. Run the same focused command and make sure that all selected tests pass. + +## Verification + +```bash +cd "$(git rev-parse --show-toplevel)/apps/backend" && go test ./internal/orchestrator ./internal/mcp/handlers ./internal/mcp/server -run 'TestExecutionToLaunchResponseReportsAgentProfile|TestHandleSpawnSessionReports(Effective|StepPinned)AgentProfile|TestSpawnSessionToolDescribesEffectiveAgentProfile|TestResolveMCPAutoStartConfig_WorkflowDefaultOutranksExplicit(OnUnpinnedStep|WhenStepOmitted)' -count=1 +``` + +## Dependencies + +None. + +## Parallelism + +`sequential`. This task changes the shared launch response contract and its MCP consumer. + +## Output contract + +Report the files changed, the red and green test results, blockers, and risks. Update this task and `plan.md` in the same conversation. + +## Results + +- Red: the launch-response and tool-description regressions failed because the + response omitted `agent_profile_id`; the handler regression failed because it + returned the requested profile instead of the effective launch result. +- Green: `go test ./internal/orchestrator ./internal/mcp/handlers + ./internal/mcp/server -run 'TestExecutionToLaunchResponseReportsAgentProfile|TestHandleSpawnSessionReportsEffectiveAgentProfile|TestSpawnSessionToolDescribesEffectiveAgentProfile|TestResolveMCPAutoStartConfig_WorkflowDefaultOutranksExplicit(OnUnpinnedStep|WhenStepOmitted)' -count=1` + passed 5 tests in 3 packages. +- Fixup regression extension: the focused command with + `TestHandleSpawnSessionReportsStepPinnedAgentProfile` passed 6 tests in 3 + packages. +- Full backend gate: `make -C apps/backend test` passed + `CGO_ENABLED=1 go test -tags fts5 ./...`. +- Backend lint: `make -C apps/backend lint` passed with no issues. +- Affected package suite: `go test ./internal/orchestrator + ./internal/mcp/handlers ./internal/mcp/server -count=1` passed. +- Formatting and whitespace checks passed. No throwaway files or logs remain. +- External side effects: None. diff --git a/docs/plans/spawn-session-effective-profile/task-02-update-public-docs.md b/docs/plans/spawn-session-effective-profile/task-02-update-public-docs.md new file mode 100644 index 0000000000..70c24f7219 --- /dev/null +++ b/docs/plans/spawn-session-effective-profile/task-02-update-public-docs.md @@ -0,0 +1,61 @@ +--- +id: "02-update-public-docs" +title: "Update public MCP documentation" +status: done +wave: 2 +depends_on: ["01-propagate-effective-profile"] +plan: "plan.md" +spec: "../../specs/spawn-session-effective-profile/spec.md" +--- + +# Task 02: Update public MCP documentation + +## Intent + +Document the effective profile response on the public Agent Communication explanation page. + +## Inputs + +- The spec `API surface` and `Scenarios` sections. +- The final tool description from Task 01. +- The existing `spawn_session_kandev` sections in `docs/public/agent-communication.md`. + +## Acceptance + +- Both documented success shapes include `agent_profile_id`. +- The page states that the field is the effective profile after workflow profile resolution. +- The page does not imply that an explicit profile always launches on a workflow step. +- The public documentation checks pass. + +## Files likely touched + +- `docs/public/agent-communication.md` + +## Verification + +```bash +cd "$(git rev-parse --show-toplevel)" && rg -n "spawn_session_kandev|agent_profile_id" docs/public/agent-communication.md && node --test scripts/validate-public-docs.test.mjs && node scripts/validate-public-docs.mjs +``` + +## Dependencies + +Task 01. + +## Parallelism + +`sequential`. The documented response must match the completed backend contract. + +## Output contract + +Report the changed page, its explanation classification, each command result, blockers, and risks. Update this task and `plan.md` in the same conversation. + +## Results + +- Updated `docs/public/agent-communication.md` with both documented + `spawn_session_kandev` success shapes and the effective-profile precedence. +- `rg -n "spawn_session_kandev|agent_profile_id" + docs/public/agent-communication.md` found the updated contract. +- `node --test scripts/validate-public-docs.test.mjs` passed all 60 tests. +- `node scripts/validate-public-docs.mjs` validated 41 published pages. +- Generated artifacts and temporary capture files: None. External side + effects: None. diff --git a/docs/public/agent-communication.md b/docs/public/agent-communication.md index f383c5d0a7..3b90a3c226 100644 --- a/docs/public/agent-communication.md +++ b/docs/public/agent-communication.md @@ -40,8 +40,12 @@ message_task_kandev(task_id="", prompt="") By default, the message goes to the task's primary session. Advanced callers can pass `session_id` to target a specific session that belongs to that task, including a sibling -session on their own task. `spawn_session_kandev` returns `{task_id, session_id, state}` -when an agent starts an additional session on an existing task; pass the `session_id` field to `message_task_kandev`. +session on their own task. `spawn_session_kandev` returns `{task_id, session_id, state, agent_profile_id}` +when an agent starts an additional session on an existing task; pass the `session_id` field to +`message_task_kandev`. The returned `agent_profile_id` is the effective profile after workflow +profile resolution. A pinned workflow-step profile wins first, followed by the workflow default +on an unpinned step. Without a workflow launch profile, an explicit profile wins, and the existing +inheritance rules apply when no profile is provided. Delivery behaviour depends on the target session's state at the moment of the call: @@ -239,7 +243,7 @@ These tools complement cross-task communication for common coordination patterns | `list_task_sessions_kandev` | List a task's sessions to find the `session_id` for the two tools above | | `list_related_tasks_kandev` | Discover parent / child / sibling / blocker task IDs | | `create_task_kandev` | Delegate work to a new subtask; returns the new task's ID | -| `spawn_session_kandev` | Start another session on an existing task; returns `{task_id, session_id, state}`; use the `session_id` field to message the new session directly | +| `spawn_session_kandev` | Start another session on an existing task; returns `{task_id, session_id, state, agent_profile_id}`, where `agent_profile_id` is the effective profile after workflow resolution; use the `session_id` field to message the new session directly | | `move_task_kandev` | Hand off a task to the next workflow step with an optional prompt for the receiving agent | | `create_task_plan_kandev` | Record an agreed implementation plan (both tasks can create/update their own plans) | | `get_task_plan_kandev` | Read a task's plan; useful before messaging to share a structured proposal | diff --git a/docs/specs/INDEX.md b/docs/specs/INDEX.md index 115c521762..2beb88079e 100644 --- a/docs/specs/INDEX.md +++ b/docs/specs/INDEX.md @@ -89,6 +89,7 @@ Kandev's task model: documents, execution stages, labels, blocker escalation, su | [parent-child-message-interrupt](tasks/parent-child-message-interrupt.md) | shipped | | [parent-child-task-stop](tasks/parent-child-task-stop.md) | shipped | | [mcp-task-agent-profile-default](tasks/mcp-task-agent-profile-default/spec.md) | shipped | +| [spawn-session-effective-profile](spawn-session-effective-profile/spec.md) | shipped | | [runtime-state-publication-order](tasks/runtime-state-publication-order.md) | shipped | | [agent-generated-titles](tasks/agent-generated-titles.md) | approved | | [task-create-executor-default](tasks/task-create-executor-default.md) | approved | diff --git a/docs/specs/spawn-session-effective-profile/spec.md b/docs/specs/spawn-session-effective-profile/spec.md new file mode 100644 index 0000000000..08c39a63d0 --- /dev/null +++ b/docs/specs/spawn-session-effective-profile/spec.md @@ -0,0 +1,60 @@ +--- +status: shipped +created: 2026-08-12 +owner: kandev +--- + +# Spawn Session Effective Agent Profile + +## Why + +Agents use `spawn_session_kandev` to start another session with a selected agent profile. The tool must report the profile that the new session uses. + +Without this guarantee, a caller can receive confirmation for one profile while a workflow profile starts instead. + +## What + +- `spawn_session_kandev` reports the effective agent profile for each successful launch. +- The reported profile is the profile on the new session, after workflow profile resolution. +- A workflow step launch profile keeps its current precedence. A pinned step profile wins first. Otherwise, the workflow default wins on that step. +- Without a workflow launch profile, an explicit `agent_profile_id` keeps its current precedence. +- Without an explicit profile, the current inheritance rules remain unchanged. +- The tool description explains the precedence and the response field. + +## API surface + +The successful `spawn_session_kandev` response has this shape: + +```json +{ + "task_id": "string", + "session_id": "string", + "state": "string", + "agent_profile_id": "string" +} +``` + +`agent_profile_id` identifies the effective profile on the new session. It does not echo an earlier profile choice when workflow resolution selects another profile. + +## Failure modes + +- If the session launch fails, the tool returns the existing launch error and no success response. +- A successful session launch returns the nonempty effective profile from the launch result. +- A session name update can still fail without changing the successful launch result. + +## Scenarios + +- **GIVEN** an unpinned workflow step with workflow default A, **WHEN** a caller requests profile B, **THEN** the new session and response use profile A. +- **GIVEN** a workflow step pinned to profile A, **WHEN** a caller requests profile B, **THEN** the new session and response use profile A. +- **GIVEN** no workflow launch profile, **WHEN** a caller requests profile B, **THEN** the new session and response use profile B. +- **GIVEN** no explicit profile or workflow launch profile, **WHEN** a same-task session spawns another session, **THEN** the new session and response use the inherited profile. + +## Out of scope + +- Changing workflow, step, explicit, or inherited profile precedence. +- Changing `create_task_kandev`. Its workflow-default response behavior is already covered by the MCP-created task profile spec. +- Changing session persistence, executor selection, or profile validation. + +## Implementation plan + +See [`../../plans/spawn-session-effective-profile/plan.md`](../../plans/spawn-session-effective-profile/plan.md).