fix(background-agent): preserve parent agent context in completion notifications

When parentAgent is undefined, omit the agent field entirely from
session.prompt body instead of passing undefined. This prevents the
OpenCode SDK from falling back to defaultAgent(), which would change
the parent session's agent context.

Changes:
- manager.ts: Build prompt body conditionally, only include agent/model
  when defined
- background-task/tools.ts: Use ctx.agent as primary source for
  parentAgent (consistent with sisyphus-task)
- registerExternalTask: Add parentAgent parameter support
- Added tests for agent context preservation scenarios
This commit is contained in:
YeonGyu-Kim 2026-01-09 15:53:36 +09:00
parent a50878df51
commit 79e9fd82c5
3 changed files with 111 additions and 11 deletions

View File

@ -674,3 +674,95 @@ describe("LaunchInput.skillContent", () => {
expect(input.skillContent).toBe("You are a playwright expert") expect(input.skillContent).toBe("You are a playwright expert")
}) })
}) })
describe("BackgroundManager.notifyParentSession - agent context preservation", () => {
test("should not pass agent field when parentAgent is undefined", async () => {
// #given
const task: BackgroundTask = {
id: "task-no-agent",
sessionID: "session-child",
parentSessionID: "session-parent",
parentMessageID: "msg-parent",
description: "task without agent context",
prompt: "test",
agent: "explore",
status: "completed",
startedAt: new Date(),
completedAt: new Date(),
parentAgent: undefined,
parentModel: { providerID: "anthropic", modelID: "claude-opus" },
}
// #when
const promptBody = buildNotificationPromptBody(task)
// #then
expect("agent" in promptBody).toBe(false)
expect(promptBody.model).toEqual({ providerID: "anthropic", modelID: "claude-opus" })
})
test("should include agent field when parentAgent is defined", async () => {
// #given
const task: BackgroundTask = {
id: "task-with-agent",
sessionID: "session-child",
parentSessionID: "session-parent",
parentMessageID: "msg-parent",
description: "task with agent context",
prompt: "test",
agent: "explore",
status: "completed",
startedAt: new Date(),
completedAt: new Date(),
parentAgent: "Sisyphus",
parentModel: { providerID: "anthropic", modelID: "claude-opus" },
}
// #when
const promptBody = buildNotificationPromptBody(task)
// #then
expect(promptBody.agent).toBe("Sisyphus")
})
test("should not pass model field when parentModel is undefined", async () => {
// #given
const task: BackgroundTask = {
id: "task-no-model",
sessionID: "session-child",
parentSessionID: "session-parent",
parentMessageID: "msg-parent",
description: "task without model context",
prompt: "test",
agent: "explore",
status: "completed",
startedAt: new Date(),
completedAt: new Date(),
parentAgent: "Sisyphus",
parentModel: undefined,
}
// #when
const promptBody = buildNotificationPromptBody(task)
// #then
expect("model" in promptBody).toBe(false)
expect(promptBody.agent).toBe("Sisyphus")
})
})
function buildNotificationPromptBody(task: BackgroundTask): Record<string, unknown> {
const body: Record<string, unknown> = {
parts: [{ type: "text", text: `[BACKGROUND TASK COMPLETED] Task "${task.description}" finished.` }],
}
if (task.parentAgent !== undefined) {
body.agent = task.parentAgent
}
if (task.parentModel?.providerID && task.parentModel?.modelID) {
body.model = { providerID: task.parentModel.providerID, modelID: task.parentModel.modelID }
}
return body
}

View File

@ -199,6 +199,7 @@ export class BackgroundManager {
parentSessionID: string parentSessionID: string
description: string description: string
agent?: string agent?: string
parentAgent?: string
}): BackgroundTask { }): BackgroundTask {
const task: BackgroundTask = { const task: BackgroundTask = {
id: input.taskId, id: input.taskId,
@ -214,6 +215,7 @@ export class BackgroundManager {
toolCalls: 0, toolCalls: 0,
lastUpdate: new Date(), lastUpdate: new Date(),
}, },
parentAgent: input.parentAgent,
} }
this.tasks.set(task.id, task) this.tasks.set(task.id, task)
@ -440,19 +442,25 @@ export class BackgroundManager {
} }
try { try {
// Use only parentModel/parentAgent - don't fallback to prevMessage const body: {
// This prevents accidentally changing parent session's model/agent agent?: string
const modelField = task.parentModel?.providerID && task.parentModel?.modelID model?: { providerID: string; modelID: string }
? { providerID: task.parentModel.providerID, modelID: task.parentModel.modelID } parts: Array<{ type: "text"; text: string }>
: undefined } = {
parts: [{ type: "text", text: message }],
}
if (task.parentAgent !== undefined) {
body.agent = task.parentAgent
}
if (task.parentModel?.providerID && task.parentModel?.modelID) {
body.model = { providerID: task.parentModel.providerID, modelID: task.parentModel.modelID }
}
await this.client.session.prompt({ await this.client.session.prompt({
path: { id: task.parentSessionID }, path: { id: task.parentSessionID },
body: { body,
agent: task.parentAgent,
model: modelField,
parts: [{ type: "text", text: message }],
},
query: { directory: this.directory }, query: { directory: this.directory },
}) })
log("[background-agent] Successfully sent prompt to parent session:", { parentSessionID: task.parentSessionID }) log("[background-agent] Successfully sent prompt to parent session:", { parentSessionID: task.parentSessionID })

View File

@ -74,7 +74,7 @@ export function createBackgroundTask(manager: BackgroundManager): ToolDefinition
parentSessionID: ctx.sessionID, parentSessionID: ctx.sessionID,
parentMessageID: ctx.messageID, parentMessageID: ctx.messageID,
parentModel, parentModel,
parentAgent: prevMessage?.agent, parentAgent: ctx.agent ?? prevMessage?.agent,
}) })
ctx.metadata?.({ ctx.metadata?.({