Push live updates to the UI while agent runs execute
Engine-side writes - hires, delegated tasks, chat messages, run state - only reached the renderer after a run finished: the queue worker sent one snapshot-changed at the end, and nothing fired at claim time, so the window sat frozen for the minutes a CEO run takes unless the user reloaded by hand. The execution service now takes a notifyDataChanged callback, the agent loop fires it after every tool-bearing iteration, and the worker announces the claim itself, so the UI follows the company working in real time.
This commit is contained in:
1 parent
921c5b4456
commit
de9e6a9fa3
5 files changed
+33
-10
No files matched your search
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -23,6 +23,8 @@ export interface AgentRunLoopResult {
|
||||
export interface AgentRunLoopDeps {
|
||||
providers: Pick<ProviderService, "runToolTurn">;
|
||||
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.`);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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"]);
|
||||
|
||||
Reference in new issue
Block a user