feat(background-agent): add resume capability and model field
- Add resume() method for continuing existing agent sessions - Add model field to BackgroundTask and LaunchInput types - Update launch() to pass model to session.prompt() - Comprehensive test coverage for resume functionality 🤖 GENERATED WITH ASSISTANCE OF [OhMyOpenCode](https://github.com/code-yeongyu/oh-my-opencode)
This commit is contained in:
@@ -1,11 +1,12 @@
|
||||
import { describe, test, expect, beforeEach } from "bun:test"
|
||||
import type { BackgroundTask } from "./types"
|
||||
import type { BackgroundTask, ResumeInput } from "./types"
|
||||
|
||||
const TASK_TTL_MS = 30 * 60 * 1000
|
||||
|
||||
class MockBackgroundManager {
|
||||
private tasks: Map<string, BackgroundTask> = new Map()
|
||||
private notifications: Map<string, BackgroundTask[]> = new Map()
|
||||
public resumeCalls: Array<{ sessionId: string; prompt: string }> = []
|
||||
|
||||
addTask(task: BackgroundTask): void {
|
||||
this.tasks.set(task.id, task)
|
||||
@@ -15,6 +16,15 @@ class MockBackgroundManager {
|
||||
return this.tasks.get(id)
|
||||
}
|
||||
|
||||
findBySession(sessionID: string): BackgroundTask | undefined {
|
||||
for (const task of this.tasks.values()) {
|
||||
if (task.sessionID === sessionID) {
|
||||
return task
|
||||
}
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
getTasksByParentSession(sessionID: string): BackgroundTask[] {
|
||||
const result: BackgroundTask[] = []
|
||||
for (const task of this.tasks.values()) {
|
||||
@@ -105,6 +115,29 @@ class MockBackgroundManager {
|
||||
}
|
||||
return count
|
||||
}
|
||||
|
||||
resume(input: ResumeInput): BackgroundTask {
|
||||
const existingTask = this.findBySession(input.sessionId)
|
||||
if (!existingTask) {
|
||||
throw new Error(`Task not found for session: ${input.sessionId}`)
|
||||
}
|
||||
|
||||
this.resumeCalls.push({ sessionId: input.sessionId, prompt: input.prompt })
|
||||
|
||||
existingTask.status = "running"
|
||||
existingTask.completedAt = undefined
|
||||
existingTask.error = undefined
|
||||
existingTask.parentSessionID = input.parentSessionID
|
||||
existingTask.parentMessageID = input.parentMessageID
|
||||
existingTask.parentModel = input.parentModel
|
||||
|
||||
existingTask.progress = {
|
||||
toolCalls: existingTask.progress?.toolCalls ?? 0,
|
||||
lastUpdate: new Date(),
|
||||
}
|
||||
|
||||
return existingTask
|
||||
}
|
||||
}
|
||||
|
||||
function createMockTask(overrides: Partial<BackgroundTask> & { id: string; sessionID: string; parentSessionID: string }): BackgroundTask {
|
||||
@@ -482,3 +515,131 @@ describe("BackgroundManager.pruneStaleTasksAndNotifications", () => {
|
||||
expect(manager.getTask("task-fresh")).toBeDefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe("BackgroundManager.resume", () => {
|
||||
let manager: MockBackgroundManager
|
||||
|
||||
beforeEach(() => {
|
||||
// #given
|
||||
manager = new MockBackgroundManager()
|
||||
})
|
||||
|
||||
test("should throw error when task not found", () => {
|
||||
// #given - empty manager
|
||||
|
||||
// #when / #then
|
||||
expect(() => manager.resume({
|
||||
sessionId: "non-existent",
|
||||
prompt: "continue",
|
||||
parentSessionID: "session-new",
|
||||
parentMessageID: "msg-new",
|
||||
})).toThrow("Task not found for session: non-existent")
|
||||
})
|
||||
|
||||
test("should resume existing task and reset state to running", () => {
|
||||
// #given
|
||||
const completedTask = createMockTask({
|
||||
id: "task-a",
|
||||
sessionID: "session-a",
|
||||
parentSessionID: "session-parent",
|
||||
status: "completed",
|
||||
})
|
||||
completedTask.completedAt = new Date()
|
||||
completedTask.error = "previous error"
|
||||
manager.addTask(completedTask)
|
||||
|
||||
// #when
|
||||
const result = manager.resume({
|
||||
sessionId: "session-a",
|
||||
prompt: "continue the work",
|
||||
parentSessionID: "session-new-parent",
|
||||
parentMessageID: "msg-new",
|
||||
})
|
||||
|
||||
// #then
|
||||
expect(result.status).toBe("running")
|
||||
expect(result.completedAt).toBeUndefined()
|
||||
expect(result.error).toBeUndefined()
|
||||
expect(result.parentSessionID).toBe("session-new-parent")
|
||||
expect(result.parentMessageID).toBe("msg-new")
|
||||
})
|
||||
|
||||
test("should preserve task identity while updating parent context", () => {
|
||||
// #given
|
||||
const existingTask = createMockTask({
|
||||
id: "task-a",
|
||||
sessionID: "session-a",
|
||||
parentSessionID: "old-parent",
|
||||
description: "original description",
|
||||
agent: "explore",
|
||||
})
|
||||
manager.addTask(existingTask)
|
||||
|
||||
// #when
|
||||
const result = manager.resume({
|
||||
sessionId: "session-a",
|
||||
prompt: "new prompt",
|
||||
parentSessionID: "new-parent",
|
||||
parentMessageID: "new-msg",
|
||||
parentModel: { providerID: "anthropic", modelID: "claude-opus" },
|
||||
})
|
||||
|
||||
// #then
|
||||
expect(result.id).toBe("task-a")
|
||||
expect(result.sessionID).toBe("session-a")
|
||||
expect(result.description).toBe("original description")
|
||||
expect(result.agent).toBe("explore")
|
||||
expect(result.parentModel).toEqual({ providerID: "anthropic", modelID: "claude-opus" })
|
||||
})
|
||||
|
||||
test("should track resume calls with prompt", () => {
|
||||
// #given
|
||||
const task = createMockTask({
|
||||
id: "task-a",
|
||||
sessionID: "session-a",
|
||||
parentSessionID: "session-parent",
|
||||
})
|
||||
manager.addTask(task)
|
||||
|
||||
// #when
|
||||
manager.resume({
|
||||
sessionId: "session-a",
|
||||
prompt: "continue with additional context",
|
||||
parentSessionID: "session-new",
|
||||
parentMessageID: "msg-new",
|
||||
})
|
||||
|
||||
// #then
|
||||
expect(manager.resumeCalls).toHaveLength(1)
|
||||
expect(manager.resumeCalls[0]).toEqual({
|
||||
sessionId: "session-a",
|
||||
prompt: "continue with additional context",
|
||||
})
|
||||
})
|
||||
|
||||
test("should preserve existing tool call count in progress", () => {
|
||||
// #given
|
||||
const taskWithProgress = createMockTask({
|
||||
id: "task-a",
|
||||
sessionID: "session-a",
|
||||
parentSessionID: "session-parent",
|
||||
})
|
||||
taskWithProgress.progress = {
|
||||
toolCalls: 42,
|
||||
lastTool: "read",
|
||||
lastUpdate: new Date(),
|
||||
}
|
||||
manager.addTask(taskWithProgress)
|
||||
|
||||
// #when
|
||||
const result = manager.resume({
|
||||
sessionId: "session-a",
|
||||
prompt: "continue",
|
||||
parentSessionID: "session-new",
|
||||
parentMessageID: "msg-new",
|
||||
})
|
||||
|
||||
// #then
|
||||
expect(result.progress?.toolCalls).toBe(42)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -27,7 +27,9 @@ export interface BackgroundTask {
|
||||
error?: string
|
||||
progress?: TaskProgress
|
||||
parentModel?: { providerID: string; modelID: string }
|
||||
model?: string
|
||||
model?: { providerID: string; modelID: string }
|
||||
/** Agent name used for concurrency tracking */
|
||||
concurrencyKey?: string
|
||||
}
|
||||
|
||||
export interface LaunchInput {
|
||||
@@ -37,4 +39,13 @@ export interface LaunchInput {
|
||||
parentSessionID: string
|
||||
parentMessageID: string
|
||||
parentModel?: { providerID: string; modelID: string }
|
||||
model?: { providerID: string; modelID: string }
|
||||
}
|
||||
|
||||
export interface ResumeInput {
|
||||
sessionId: string
|
||||
prompt: string
|
||||
parentSessionID: string
|
||||
parentMessageID: string
|
||||
parentModel?: { providerID: string; modelID: string }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user