diff --git a/AgentCompany-New/src/main/app/ipc-handlers.ts b/AgentCompany-New/src/main/app/ipc-handlers.ts index b7e82abe..3a0e5328 100644 --- a/AgentCompany-New/src/main/app/ipc-handlers.ts +++ b/AgentCompany-New/src/main/app/ipc-handlers.ts @@ -42,7 +42,12 @@ export function registerIpcHandlers(deps: IpcDeps): void { const providers = new ProviderService(deps.connection.db); const providerOAuth = new ProviderOAuthService(); const skills = new SkillService(deps.connection.db, deps.artifactRoot); - const taskExecution = new TaskExecutionService(deps.connection.db, providers, skills); + const notifyDataChanged = () => { + if (!deps.mainWindow.isDestroyed()) { + deps.mainWindow.webContents.send(IPC_CHANNELS.rendererEvent, { type: "snapshot-changed" }); + } + }; + const taskExecution = new TaskExecutionService(deps.connection.db, providers, skills, notifyDataChanged); const social = new SocialService(deps.connection.db); const files = new FileService(deps.connection.db, deps.artifactRoot); const migrations = new MigrationService(deps.connection.db); diff --git a/AgentCompany-New/src/main/app/task-queue-worker.ts b/AgentCompany-New/src/main/app/task-queue-worker.ts index 2821cf18..f15d0a6c 100644 --- a/AgentCompany-New/src/main/app/task-queue-worker.ts +++ b/AgentCompany-New/src/main/app/task-queue-worker.ts @@ -40,7 +40,12 @@ export function claimNextQueuedRun(connection: DatabaseConnection): QueuedRun | * verified by row changes) so a run is never executed twice. */ export function startTaskQueueWorker(input: TaskQueueWorkerInput): () => void { - const taskExecution = new TaskExecutionService(input.connection.db); + const notifyDataChanged = () => { + if (!input.mainWindow.isDestroyed()) { + input.mainWindow.webContents.send(IPC_CHANNELS.rendererEvent, { type: "snapshot-changed" }); + } + }; + const taskExecution = new TaskExecutionService(input.connection.db, undefined, undefined, notifyDataChanged); const intervalMs = input.intervalMs ?? DEFAULT_INTERVAL_MS; let running = false; @@ -51,19 +56,16 @@ export function startTaskQueueWorker(input: TaskQueueWorkerInput): () => void { const enqueued = input.connection.transaction(() => taskExecution.enqueueHeartbeatRuns()); const claimed = claimNextQueuedRun(input.connection); if (!claimed) { - if (enqueued.length > 0 && !input.mainWindow.isDestroyed()) { - input.mainWindow.webContents.send(IPC_CHANNELS.rendererEvent, { type: "snapshot-changed" }); - } + if (enqueued.length > 0) notifyDataChanged(); return; } + notifyDataChanged(); try { await taskExecution.executeRun(claimed.org_id, claimed.id); } catch { // executeRun records the failure on the run row; the UI reflects it below. } - if (!input.mainWindow.isDestroyed()) { - input.mainWindow.webContents.send(IPC_CHANNELS.rendererEvent, { type: "snapshot-changed" }); - } + notifyDataChanged(); } finally { running = false; } diff --git a/AgentCompany-New/src/main/services/agent-run-loop.ts b/AgentCompany-New/src/main/services/agent-run-loop.ts index acb95b1f..953ec254 100644 --- a/AgentCompany-New/src/main/services/agent-run-loop.ts +++ b/AgentCompany-New/src/main/services/agent-run-loop.ts @@ -23,6 +23,8 @@ export interface AgentRunLoopResult { export interface AgentRunLoopDeps { providers: Pick; tools: AgentToolDeps; + /** Called after each iteration's tool calls have written their effects. */ + onToolResults?: () => void; } /** @@ -63,6 +65,7 @@ export async function runAgentLoop(deps: AgentRunLoopDeps, input: AgentRunLoopIn return { toolCallId: call.id, toolName: call.name, output: dispatchAgentTool(deps.tools, input.toolContext, call) }; }); messages.push({ role: "tool", results }); + deps.onToolResults?.(); } throw new Error(`Agent run did not converge within ${MAX_ITERATIONS} iterations.`); diff --git a/AgentCompany-New/src/main/services/task-execution-service.ts b/AgentCompany-New/src/main/services/task-execution-service.ts index 02e3d5de..e6317d2b 100644 --- a/AgentCompany-New/src/main/services/task-execution-service.ts +++ b/AgentCompany-New/src/main/services/task-execution-service.ts @@ -54,6 +54,8 @@ export class TaskExecutionService { private readonly db: DatabaseSync, private readonly providers: ProviderService = new ProviderService(db), private readonly skills: SkillService = new SkillService(db, ""), + /** Invoked whenever a run writes data mid-flight, so the UI can refresh while agents work. */ + private readonly notifyDataChanged: () => void = () => undefined, ) { this.audit = new AuditService(db); this.organization = new OrganizationService(db); @@ -118,7 +120,11 @@ export class TaskExecutionService { }; const chatContext = this.loadChatContext(orgId, isolation); const result = await runAgentLoop( - { providers: this.providers, tools: { db: this.db, organization: this.organization, chat: this.chat } }, + { + providers: this.providers, + tools: { db: this.db, organization: this.organization, chat: this.chat }, + onToolResults: this.notifyDataChanged, + }, { orgId, providerId: provider.id, diff --git a/AgentCompany-New/tests/unit/agent-run-loop.test.ts b/AgentCompany-New/tests/unit/agent-run-loop.test.ts index 83318d30..44bcb361 100644 --- a/AgentCompany-New/tests/unit/agent-run-loop.test.ts +++ b/AgentCompany-New/tests/unit/agent-run-loop.test.ts @@ -80,14 +80,21 @@ describe("agent run loop (real tool dispatch)", () => { { text: "Team staffed, work delegated, plan written.", toolCalls: [] }, ]; + let toolResultNotifications = 0; const result = await runAgentLoop( - { providers: fakeProvider(script), tools: { db: connection.db, organization, chat: new ChatService(connection.db) } }, + { + providers: fakeProvider(script), + tools: { db: connection.db, organization, chat: new ChatService(connection.db) }, + onToolResults: () => { toolResultNotifications += 1; }, + }, { orgId, providerId, workspaceId, instructions: "You are the CEO.", initialUserTurn: "Start the company.", toolContext }, ); expect(result.iterations).toBe(4); expect(result.toolCallCount).toBe(5); expect(result.finalText).toContain("Team staffed"); + // The UI is told about new data after every tool-bearing iteration, not only at the end. + expect(toolResultNotifications).toBe(3); const agentNames = (connection.db.prepare("select name from agents where org_id = ? order by name").all(orgId) as Array<{ name: string }>).map((r) => r.name); expect(agentNames).toEqual(["Alex Rivera", "CEO", "Sam Lee"]);