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:
parent
fff565b5af
commit
47d56d95a6
@ -1,11 +1,12 @@
|
|||||||
import { describe, test, expect, beforeEach } from "bun:test"
|
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
|
const TASK_TTL_MS = 30 * 60 * 1000
|
||||||
|
|
||||||
class MockBackgroundManager {
|
class MockBackgroundManager {
|
||||||
private tasks: Map<string, BackgroundTask> = new Map()
|
private tasks: Map<string, BackgroundTask> = new Map()
|
||||||
private notifications: Map<string, BackgroundTask[]> = new Map()
|
private notifications: Map<string, BackgroundTask[]> = new Map()
|
||||||
|
public resumeCalls: Array<{ sessionId: string; prompt: string }> = []
|
||||||
|
|
||||||
addTask(task: BackgroundTask): void {
|
addTask(task: BackgroundTask): void {
|
||||||
this.tasks.set(task.id, task)
|
this.tasks.set(task.id, task)
|
||||||
@ -15,6 +16,15 @@ class MockBackgroundManager {
|
|||||||
return this.tasks.get(id)
|
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[] {
|
getTasksByParentSession(sessionID: string): BackgroundTask[] {
|
||||||
const result: BackgroundTask[] = []
|
const result: BackgroundTask[] = []
|
||||||
for (const task of this.tasks.values()) {
|
for (const task of this.tasks.values()) {
|
||||||
@ -105,6 +115,29 @@ class MockBackgroundManager {
|
|||||||
}
|
}
|
||||||
return count
|
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 {
|
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()
|
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
|
error?: string
|
||||||
progress?: TaskProgress
|
progress?: TaskProgress
|
||||||
parentModel?: { providerID: string; modelID: string }
|
parentModel?: { providerID: string; modelID: string }
|
||||||
model?: string
|
model?: { providerID: string; modelID: string }
|
||||||
|
/** Agent name used for concurrency tracking */
|
||||||
|
concurrencyKey?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface LaunchInput {
|
export interface LaunchInput {
|
||||||
@ -37,4 +39,13 @@ export interface LaunchInput {
|
|||||||
parentSessionID: string
|
parentSessionID: string
|
||||||
parentMessageID: string
|
parentMessageID: string
|
||||||
parentModel?: { providerID: string; modelID: 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 }
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user