Fence mobile Assistant work during suspension
This commit is contained in:
1 parent
edb315e81f
commit
ad7ecb5ec6
2 files changed
+89
No files matched your search
@@ -215,6 +215,7 @@ final class MobileAssistantModel {
|
||||
}
|
||||
|
||||
func edit(_ conversation: ConversationModel, messageID: UUID, text: String) {
|
||||
guard !isSuspending else { return }
|
||||
guard
|
||||
let prepared = execution.prepareAgentEdit(
|
||||
conversation, messageID: messageID, text: text,
|
||||
@@ -229,12 +230,14 @@ final class MobileAssistantModel {
|
||||
}
|
||||
|
||||
func compact(_ conversation: ConversationModel, instructions: String = "") async {
|
||||
guard !isSuspending else { return }
|
||||
_ = await execution.compact(conversation, instructions: instructions)
|
||||
}
|
||||
|
||||
fileprivate func implement(
|
||||
_ card: AgentPlanCard, in conversation: ConversationModel, library: AssistantLibraryModel
|
||||
) {
|
||||
guard !isSuspending else { return }
|
||||
guard
|
||||
execution.sendPlanImplementation(
|
||||
card, in: conversation,
|
||||
|
||||
@@ -485,11 +485,97 @@ struct MobileAssistantTests {
|
||||
#expect(app.assistant.isSuspending)
|
||||
assistant.send()
|
||||
#expect(assistant.conversation.composer.draft == "preserve")
|
||||
await assistant.compact(assistant.conversation)
|
||||
#expect(assistant.conversation.compactionActivity == nil)
|
||||
await app.assistant.waitForSuspension()
|
||||
#expect(!app.assistant.isSuspending)
|
||||
#expect(!app.assistant.isRunning)
|
||||
}
|
||||
|
||||
@Test func backgroundExpirationBlocksEditsUntilCleanupCompletesThenAllowsRecovery() async throws {
|
||||
let app = try MobileAppModel(inMemory: true)
|
||||
let assistant = app.assistant.makeSceneModel()
|
||||
let conversation = assistant.conversation
|
||||
conversation.composer.draft = "Original question"
|
||||
assistant.send()
|
||||
#expect(await eventually { conversation.runState == .failed })
|
||||
let original = try #require(conversation.messages.first { $0.role == .user })
|
||||
let blocker = ConversationModel()
|
||||
assistant.library.insertAndSelect(blocker)
|
||||
let blockerRepository = MobileTerminalSaveGateRepository()
|
||||
blocker.start(
|
||||
runtime: AgentRuntime(
|
||||
model: MobileImmediateAssistantModel(calls: MobileAssistantModelCalls()), tools: [],
|
||||
approval: AgentApprovalBroker(reviewer: nil, manualApproval: { _ in .allow }),
|
||||
repository: blockerRepository
|
||||
),
|
||||
request: AgentRunRequest(
|
||||
conversationID: blocker.id, prompt: "block cleanup", permissionMode: .askForApproval),
|
||||
repository: blockerRepository
|
||||
)
|
||||
await blockerRepository.waitForTerminalSave()
|
||||
|
||||
app.assistant.expireBackground()
|
||||
#expect(app.assistant.isSuspending)
|
||||
assistant.edit(conversation, messageID: original.id, text: "Replacement question")
|
||||
await Task.yield()
|
||||
#expect(conversation.messages.first(where: { $0.role == .user })?.text == "Original question")
|
||||
|
||||
await blockerRepository.releaseTerminalSaves()
|
||||
await app.assistant.waitForSuspension()
|
||||
#expect(!app.assistant.isSuspending)
|
||||
assistant.edit(conversation, messageID: original.id, text: "Replacement question")
|
||||
#expect(
|
||||
await eventually {
|
||||
conversation.messages.first(where: { $0.role == .user })?.text == "Replacement question"
|
||||
}
|
||||
)
|
||||
#expect(await eventually { conversation.runState == .failed })
|
||||
}
|
||||
|
||||
@Test func backgroundExpirationBlocksPlanImplementationUntilCleanupCompletesThenAllowsRecovery() async throws {
|
||||
let app = try MobileAppModel(inMemory: true)
|
||||
let assistant = app.assistant.makeSceneModel()
|
||||
let conversation = ConversationModel()
|
||||
_ = assistant.library.startConversation(conversation)
|
||||
let plan = AgentPlan(
|
||||
title: "Implementation plan", body: "Apply the requested change.",
|
||||
scratchPath: "/tmp/plan.md")
|
||||
conversation.isPlanning = true
|
||||
conversation.handle(.planPresented(plan))
|
||||
let card = try #require(conversation.plans.first)
|
||||
let blocker = ConversationModel()
|
||||
assistant.library.insertAndSelect(blocker)
|
||||
let blockerRepository = MobileTerminalSaveGateRepository()
|
||||
blocker.start(
|
||||
runtime: AgentRuntime(
|
||||
model: MobileImmediateAssistantModel(calls: MobileAssistantModelCalls()), tools: [],
|
||||
approval: AgentApprovalBroker(reviewer: nil, manualApproval: { _ in .allow }),
|
||||
repository: blockerRepository
|
||||
),
|
||||
request: AgentRunRequest(
|
||||
conversationID: blocker.id, prompt: "block cleanup", permissionMode: .askForApproval),
|
||||
repository: blockerRepository
|
||||
)
|
||||
await blockerRepository.waitForTerminalSave()
|
||||
|
||||
app.assistant.expireBackground()
|
||||
#expect(app.assistant.isSuspending)
|
||||
assistant.implement(card, in: conversation)
|
||||
#expect(conversation.plans.first?.outcome == .pending)
|
||||
#expect(conversation.transcript.isEmpty)
|
||||
|
||||
await blockerRepository.releaseTerminalSaves()
|
||||
await app.assistant.waitForSuspension()
|
||||
#expect(!app.assistant.isSuspending)
|
||||
assistant.implement(card, in: conversation)
|
||||
#expect(conversation.plans.first?.outcome == .implemented)
|
||||
let implementation = try #require(conversation.transcript.last)
|
||||
#expect(implementation.role == .user)
|
||||
#expect(implementation.contextSnapshot?.planContract == nil)
|
||||
#expect(await eventually { conversation.runState == .failed })
|
||||
}
|
||||
|
||||
@Test func backgroundExpirationStopsEveryConversationBeforeJoiningTheFirstDrain() async throws {
|
||||
let app = try MobileAppModel(inMemory: true)
|
||||
let assistant = app.assistant.makeSceneModel()
|
||||
|
||||
Reference in new issue
Block a user