Run the CEO agent autonomously with a real tool-use loop

The new app reached a real LLM but never ran an agent: the kickoff task_run
sat queued forever and runTask was a single text completion with no tools.

This adds the missing runtime:
- provider-tool-turn: OpenAI Responses + Anthropic Messages tool-use turns
- agent-tools + deliverable-writer: hire_agent, create_task,
  update_task_status, write_deliverable (workspace-scoped, traversal-safe)
- agent-run-loop: real tool-use loop, capped iterations
- task-queue-worker: claims queued runs in a transaction and executes them

task-execution now drives the loop; a background worker auto-runs the
bootstrap CEO run so it hires its team, delegates tasks, and writes the first
deliverable into the bound workspace. Autonomy is gated by AGENTCOMPANY_NEW_AUTORUN
(tests run with it off to stay hermetic).
This commit is contained in:
ZacharyZhang-NY committed 2026-05-29 21:08:04 -04:00
1 parent 1c7b81830e
commit 0575712e02
15 files changed
+1008 -74

No files matched your search

@@ -0,0 +1,71 @@
import type { BrowserWindow } from "electron";
import type { DatabaseConnection } from "../database/connection";
import { IPC_CHANNELS } from "../../shared/ipc";
import { TaskExecutionService } from "../services/task-execution-service";
const DEFAULT_INTERVAL_MS = 5_000;
export interface TaskQueueWorkerInput {
connection: DatabaseConnection;
mainWindow: BrowserWindow;
intervalMs?: number;
}
export interface QueuedRun {
id: string;
org_id: string;
}
/**
* Atomically claims the oldest queued task_run by transitioning it
* `queued` -> `running` in one transaction; returns the claimed run or null.
*/
export function claimNextQueuedRun(connection: DatabaseConnection): QueuedRun | null {
return connection.transaction(() => {
const row = connection.db
.prepare("select id, org_id from task_runs where state = 'queued' order by created_at asc limit 1")
.get() as QueuedRun | undefined;
if (!row) return null;
const result = connection.db
.prepare("update task_runs set state = 'running', started_at = coalesce(started_at, ?) where id = ? and state = 'queued'")
.run(new Date().toISOString(), row.id);
return result.changes === 1 ? row : null;
});
}
/**
* Autonomous executor: every tick it claims the oldest `queued` task_run (the
* bootstrap CEO run, plus any future heartbeat-created runs) and executes its
* real agent loop. The claim is a single transaction (`queued` -> `running`,
* 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 intervalMs = input.intervalMs ?? DEFAULT_INTERVAL_MS;
let running = false;
const tick = async (): Promise<void> => {
if (running) return;
running = true;
try {
const claimed = claimNextQueuedRun(input.connection);
if (!claimed) return;
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" });
}
} finally {
running = false;
}
};
const interval = setInterval(() => {
void tick();
}, intervalMs);
void tick();
return () => clearInterval(interval);
}
+6
View File
@@ -5,6 +5,7 @@ import { DatabaseConnection } from "./database/connection";
import { createMainWindow } from "./app/create-window";
import { registerIpcHandlers } from "./app/ipc-handlers";
import { startProviderBackgroundWorker } from "./app/provider-background-worker";
import { startTaskQueueWorker } from "./app/task-queue-worker";
const __dirname = dirname(fileURLToPath(import.meta.url));
const hasSingleInstanceLock = app.requestSingleInstanceLock();
@@ -31,6 +32,10 @@ app.whenReady().then(() => {
db: connection.db,
mainWindow: win,
});
const autonomousExecution = process.env.AGENTCOMPANY_NEW_AUTORUN !== "0";
const stopTaskQueueWorker = autonomousExecution
? startTaskQueueWorker({ connection, mainWindow: win })
: () => {};
app.on("second-instance", () => {
if (win.isMinimized()) win.restore();
@@ -39,6 +44,7 @@ app.whenReady().then(() => {
app.on("before-quit", () => {
stopProviderBackgroundWorker();
stopTaskQueueWorker();
connection.close();
});
});
@@ -0,0 +1,63 @@
import type { ProviderService } from "./provider-service";
import { AGENT_TOOLS, dispatchAgentTool, type AgentToolContext, type AgentToolDeps } from "./agent-tools";
import type { AgentTurnMessage } from "./provider-tool-turn";
const MAX_ITERATIONS = 12;
export interface AgentRunLoopInput {
orgId: string;
providerId: string;
workspaceId: string | null;
instructions: string;
initialUserTurn: string;
toolContext: AgentToolContext;
}
export interface AgentRunLoopResult {
finalText: string;
iterations: number;
toolCallCount: number;
lastRequestId: string;
}
export interface AgentRunLoopDeps {
providers: Pick<ProviderService, "runToolTurn">;
tools: AgentToolDeps;
}
/**
* Drives a real tool-use loop against the live provider: prompt the model with
* the agent tools, execute every tool call it makes, feed results back, and
* repeat until the model returns a turn with no tool calls (it is done) or the
* iteration cap is hit (which throws rather than silently truncating).
*/
export async function runAgentLoop(deps: AgentRunLoopDeps, input: AgentRunLoopInput): Promise<AgentRunLoopResult> {
const messages: AgentTurnMessage[] = [{ role: "user", text: input.initialUserTurn }];
let toolCallCount = 0;
let lastRequestId = "";
for (let iteration = 1; iteration <= MAX_ITERATIONS; iteration += 1) {
const turn = await deps.providers.runToolTurn({
orgId: input.orgId,
providerId: input.providerId,
workspaceId: input.workspaceId,
instructions: input.instructions,
messages,
tools: AGENT_TOOLS,
});
lastRequestId = turn.requestId;
if (turn.toolCalls.length === 0) {
return { finalText: turn.text, iterations: iteration, toolCallCount, lastRequestId };
}
messages.push({ role: "assistant", text: turn.text, toolCalls: turn.toolCalls });
const results = turn.toolCalls.map((call) => {
toolCallCount += 1;
return { toolCallId: call.id, toolName: call.name, output: dispatchAgentTool(deps.tools, input.toolContext, call) };
});
messages.push({ role: "tool", results });
}
throw new Error(`Agent run did not converge within ${MAX_ITERATIONS} iterations.`);
}
@@ -0,0 +1,190 @@
import type { DatabaseSync } from "node:sqlite";
import { RISK_LEVEL, TASK_PRIORITY, TASK_STATUS, type RiskLevel } from "../../shared/domain";
import type { OrganizationService } from "./organization-service";
import { writeDeliverable } from "./deliverable-writer";
import type { JsonRecord } from "./provider-http";
import type { ToolCall, ToolDefinition } from "./provider-tool-turn";
const HIRE_BUDGET_CENTS = 25000;
const HEARTBEAT_INTERVAL_SEC = 120;
export interface AgentToolContext {
orgId: string;
/** The acting agent (the CEO during team-building). */
agent: { id: string; modelProviderId: string | null; modelId: string | null };
taskProjectId: string | null;
taskGoalId: string | null;
workspace: { id: string; localPath: string } | null;
}
export interface AgentToolDeps {
db: DatabaseSync;
organization: OrganizationService;
}
/** The tools the acting agent may call during a run. */
export const AGENT_TOOLS: ToolDefinition[] = [
{
name: "hire_agent",
description:
"Add one direct report to your team. During team building, call this 3-5 times to staff the roles you need before delegating work.",
parameters: {
type: "object",
additionalProperties: false,
properties: {
name: { type: "string", description: "Full name of the new agent." },
role: { type: "string", description: "Their role/title, e.g. 'Growth Marketer'." },
department: { type: "string", description: "Department, e.g. 'Marketing' or 'Operations'." },
model: { type: "string", description: "Optional model id; defaults to your own model." },
},
required: ["name", "role", "department"],
},
},
{
name: "create_task",
description: "Create a task for the team and optionally assign it to one of your reports by name.",
parameters: {
type: "object",
additionalProperties: false,
properties: {
title: { type: "string" },
description: { type: "string" },
assignee_agent_name: { type: "string", description: "Name of the agent to assign, if any." },
risk: { type: "string", enum: ["low", "medium", "high"] },
},
required: ["title", "description"],
},
},
{
name: "update_task_status",
description: "Move a task to a new status.",
parameters: {
type: "object",
additionalProperties: false,
properties: {
task_id: { type: "string" },
status: { type: "string", enum: ["backlog", "in_progress", "review", "done"] },
},
required: ["task_id", "status"],
},
},
{
name: "write_deliverable",
description: "Write a deliverable file into the company workspace folder (relative path). Use this to produce real output.",
parameters: {
type: "object",
additionalProperties: false,
properties: {
path: { type: "string", description: "Relative path inside the workspace, e.g. 'plan/business-plan.md'." },
content: { type: "string" },
},
required: ["path", "content"],
},
},
];
export function dispatchAgentTool(deps: AgentToolDeps, ctx: AgentToolContext, call: ToolCall): string {
try {
switch (call.name) {
case "hire_agent":
return hireAgent(deps, ctx, call.arguments);
case "create_task":
return createTask(deps, ctx, call.arguments);
case "update_task_status":
return updateTaskStatus(deps, ctx, call.arguments);
case "write_deliverable":
return writeDeliverableTool(ctx, call.arguments);
default:
return `ERROR: unknown tool "${call.name}".`;
}
} catch (error) {
return `ERROR: ${error instanceof Error ? error.message : String(error)}`;
}
}
function hireAgent(deps: AgentToolDeps, ctx: AgentToolContext, args: JsonRecord): string {
if (!ctx.agent.modelProviderId) throw new Error("Acting agent has no model provider to assign hires.");
const name = requireString(args.name, "name");
const role = requireString(args.role, "role");
const department = requireString(args.department, "department");
const model = optionalString(args.model) ?? ctx.agent.modelId;
const agentId = deps.organization.createAgent({
orgId: ctx.orgId,
name,
role,
department,
modelProviderId: ctx.agent.modelProviderId,
modelId: model,
budgetCents: HIRE_BUDGET_CENTS,
managerAgentId: ctx.agent.id,
roleLevel: "staff",
workspaceId: ctx.workspace?.id ?? null,
heartbeatEnabled: true,
heartbeatIntervalSec: HEARTBEAT_INTERVAL_SEC,
});
return `Hired ${name} as ${role} in ${department} (agent_id=${agentId}).`;
}
function createTask(deps: AgentToolDeps, ctx: AgentToolContext, args: JsonRecord): string {
const title = requireString(args.title, "title");
const description = requireString(args.description, "description");
const assigneeName = optionalString(args.assignee_agent_name);
const assigneeId = assigneeName ? resolveAgentByName(deps.db, ctx.orgId, assigneeName) : null;
if (assigneeName && !assigneeId) throw new Error(`No agent named "${assigneeName}" — hire them first.`);
const taskId = deps.organization.createTask({
orgId: ctx.orgId,
title,
description,
status: TASK_STATUS.BACKLOG,
assigneeAgentId: assigneeId,
risk: normalizeRisk(args.risk),
projectId: ctx.taskProjectId,
goalId: ctx.taskGoalId,
workspaceId: ctx.workspace?.id ?? null,
priority: TASK_PRIORITY.NORMAL,
requiresFilesystem: false,
});
return `Created task "${title}" (task_id=${taskId})${assigneeId ? ` assigned to ${assigneeName}` : " (unassigned)"}.`;
}
function updateTaskStatus(deps: AgentToolDeps, ctx: AgentToolContext, args: JsonRecord): string {
const taskId = requireString(args.task_id, "task_id");
const status = normalizeStatus(args.status);
deps.organization.updateTaskStatus(ctx.orgId, taskId, status);
return `Task ${taskId} moved to ${status}.`;
}
function writeDeliverableTool(ctx: AgentToolContext, args: JsonRecord): string {
if (!ctx.workspace) throw new Error("No workspace is bound to this task; cannot write deliverables.");
const path = requireString(args.path, "path");
const content = typeof args.content === "string" ? args.content : "";
const written = writeDeliverable({ workspaceRoot: ctx.workspace.localPath, relativePath: path, content });
return `Wrote deliverable ${written} (${content.length} chars).`;
}
function resolveAgentByName(db: DatabaseSync, orgId: string, name: string): string | null {
const row = db.prepare("select id from agents where org_id = ? and lower(name) = lower(?) and lifecycle = 'active' limit 1")
.get(orgId, name.trim()) as { id: string } | undefined;
return row?.id ?? null;
}
function requireString(value: unknown, field: string): string {
if (typeof value !== "string" || !value.trim()) throw new Error(`"${field}" is required.`);
return value.trim();
}
function optionalString(value: unknown): string | null {
return typeof value === "string" && value.trim() ? value.trim() : null;
}
function normalizeRisk(value: unknown): RiskLevel {
if (value === RISK_LEVEL.MEDIUM) return RISK_LEVEL.MEDIUM;
if (value === RISK_LEVEL.HIGH) return RISK_LEVEL.HIGH;
return RISK_LEVEL.LOW;
}
function normalizeStatus(value: unknown): string {
const allowed = [TASK_STATUS.BACKLOG, TASK_STATUS.IN_PROGRESS, TASK_STATUS.REVIEW, TASK_STATUS.DONE] as string[];
if (typeof value === "string" && allowed.includes(value)) return value;
throw new Error(`Invalid status. Use one of: ${allowed.join(", ")}.`);
}
@@ -0,0 +1,24 @@
import { mkdirSync, writeFileSync } from "node:fs";
import { dirname, isAbsolute, relative, resolve, sep } from "node:path";
/**
* Writes an agent deliverable into the run's bound workspace folder.
*
* The target path is resolved against the workspace root and rejected if it
* escapes that root (path traversal / absolute paths), so an agent can only
* write inside the folder the operator bound to the company.
*/
export function writeDeliverable(input: { workspaceRoot: string; relativePath: string; content: string }): string {
const root = resolve(input.workspaceRoot);
const requested = input.relativePath.trim().replace(/^[\\/]+/, "");
if (!requested) throw new Error("Deliverable path is required.");
if (isAbsolute(requested)) throw new Error("Deliverable path must be relative to the workspace.");
const target = resolve(root, requested);
const rel = relative(root, target);
if (rel === "" || rel.startsWith("..") || isAbsolute(rel)) {
throw new Error("Deliverable path escapes the workspace root.");
}
mkdirSync(dirname(target), { recursive: true });
writeFileSync(target, input.content, "utf8");
return rel.split(sep).join("/");
}
@@ -12,6 +12,8 @@ import type { ProviderRateLimitRegistry } from "./provider-rate-limit-registry";
import type { ProviderRequestStore } from "./provider-request-store";
import type { ProviderResponseObserver } from "./provider-response-observer";
import { hashPrompt, readProviderResponseId, requireStreamText } from "./provider-response-utils";
import { requireApiKeyProfile } from "./provider-auth-profiles";
import { selectToolTurn, type AgentTurnMessage, type AgentTurnResult, type ToolDefinition, type ToolTurnFn } from "./provider-tool-turn";
import {
HEALTH_STATE_HEALTHY,
WORKSPACE_SCOPES,
@@ -37,6 +39,8 @@ export interface ProviderResponseRunnerDeps {
responseObserver: ProviderResponseObserver;
requestStore: ProviderRequestStore;
background: ProviderBackgroundService;
/** Test-injectable override for the provider's tool-use HTTP turn. */
toolTurn?: ToolTurnFn;
}
export class ProviderResponseRunner {
@@ -91,6 +95,72 @@ export class ProviderResponseRunner {
}
}
async runToolTurn(input: {
orgId: string;
providerId: string;
workspaceId: string | null;
instructions: string | null;
messages: AgentTurnMessage[];
tools: ToolDefinition[];
}): Promise<AgentTurnResult & { requestId: string }> {
const provider = loadConfiguredProvider(this.deps.db, input.orgId, input.providerId);
const workspaceId = this.normalizeWorkspaceId(input.orgId, input.workspaceId ?? null);
this.enforcePolicy(input.orgId, provider, PROVIDER_EXECUTION_MODE.FOREGROUND, workspaceId);
if (provider.auth_type !== PROVIDER_AUTH_TYPE.API_KEY) throw new Error("Agent tool execution requires an API-key provider.");
const model = provider.model;
if (!model) throw new Error("Provider model is required.");
const owner: ProviderRow & { model: string } = { ...provider, model };
const adapter = requireAdapter(this.deps.adapters, provider.provider_key, provider.auth_type);
const turn = this.deps.toolTurn ?? selectToolTurn(provider.provider_key);
const credential = loadCredential(this.deps.authBroker, input.orgId, provider);
const apiKey = requireApiKeyProfile(adapter.parseAuthProfile(credential.secret), provider.provider_key).apiKey;
const promptChars = JSON.stringify(input.messages).length;
const requestId = this.deps.requestStore.create({
orgId: input.orgId,
provider: owner,
model,
workspaceId,
promptSha256: hashPrompt(JSON.stringify(input.messages)),
inputCharCount: promptChars,
executionMode: PROVIDER_EXECUTION_MODE.FOREGROUND,
createdAt: new Date().toISOString(),
});
const startedAt = Date.now();
try {
const result = await turn({ apiKey, model, instructions: input.instructions, messages: input.messages, tools: input.tools });
this.deps.requestStore.markSucceeded({
orgId: input.orgId,
requestId,
responseText: result.text,
rawResponseJson: result.rawResponseJson,
outputCharCount: result.text.length,
externalResponseId: readProviderResponseId(result.rawResponseJson),
completedAt: new Date().toISOString(),
});
this.deps.responseObserver.recordSuccess({
orgId: input.orgId,
provider: owner,
requestId,
executionMode: PROVIDER_EXECUTION_MODE.FOREGROUND,
startedAt,
action: "response_created",
});
this.deps.requestStore.recordUsage(input.orgId, provider.id, promptChars, result.text.length);
return { ...result, requestId };
} catch (error) {
this.deps.responseObserver.recordFailure({
orgId: input.orgId,
provider: owner,
requestId,
executionMode: PROVIDER_EXECUTION_MODE.FOREGROUND,
startedAt,
inputCharCount: promptChars,
error,
});
throw error;
}
}
async streamResponse(input: StreamProviderResponseInput, onEvent: (event: ProviderStreamRendererEvent) => void): Promise<string> {
const execution = this.prepareExecution(input);
const requestId = this.createRequestRecord(execution);
@@ -14,6 +14,7 @@ import { ProviderResponseObserver } from "./provider-response-observer";
import { ProviderRequestStore } from "./provider-request-store";
import { ProviderResponseRunner, type ProviderResponseCreateInput } from "./provider-response-runner";
import { launchProviderSignInWindow, type LaunchProviderSignInWindowInput } from "./provider-sign-in-window";
import type { ToolTurnFn } from "./provider-tool-turn";
import {
AUTH_STATE_CONFIGURED,
AUTH_STATE_REVOKED,
@@ -48,6 +49,7 @@ export class ProviderService {
private readonly adapters: ProviderAdapterRegistry = providerAdapters,
vault: ProviderSecretVault = new SecretVault(),
private readonly launchSignIn: (input: LaunchProviderSignInWindowInput) => void = launchProviderSignInWindow,
toolTurn?: ToolTurnFn,
) {
this.audit = new AuditService(db);
this.authBroker = new AuthBrokerService(db, vault);
@@ -66,6 +68,7 @@ export class ProviderService {
responseObserver: this.responseObserver,
requestStore: this.requestStore,
background: this.background,
toolTurn,
});
}
@@ -81,6 +84,10 @@ export class ProviderService {
return this.runner.cancelResponse(input);
}
runToolTurn(input: Parameters<ProviderResponseRunner["runToolTurn"]>[0]): ReturnType<ProviderResponseRunner["runToolTurn"]> {
return this.runner.runToolTurn(input);
}
saveCredential(input: SaveProviderCredentialInput): void {
const provider = this.db.prepare("select * from providers where id = ? and org_id = ?")
.get(input.providerId, input.orgId) as { provider_key: string; auth_type: ProviderAuthType } | undefined;
@@ -0,0 +1,164 @@
/**
* Provider-agnostic single turn of a tool-use ("function calling") conversation.
*
* The agent loop maintains a normalized conversation ({@link AgentTurnMessage}[])
* and the requested {@link ToolDefinition}s; these helpers translate that into the
* provider's wire format (OpenAI Responses API or Anthropic Messages API), call
* the live endpoint, and parse the assistant's reply plus any tool calls back into
* the normalized shape.
*/
import { isRecord, postJson, type JsonRecord } from "./provider-http";
export interface ToolDefinition {
name: string;
description: string;
/** JSON Schema for the tool's arguments. */
parameters: JsonRecord;
}
export interface ToolCall {
id: string;
name: string;
arguments: JsonRecord;
}
export type AgentTurnMessage =
| { role: "user"; text: string }
| { role: "assistant"; text: string; toolCalls: ToolCall[] }
| { role: "tool"; results: Array<{ toolCallId: string; toolName: string; output: string }> };
export interface AgentTurnResult {
text: string;
toolCalls: ToolCall[];
rawResponseJson: string;
}
export interface ProviderToolTurnInput {
apiKey: string;
model: string;
instructions: string | null;
messages: AgentTurnMessage[];
tools: ToolDefinition[];
}
function parseArguments(value: unknown): JsonRecord {
if (isRecord(value)) return value;
if (typeof value === "string" && value.trim()) {
try {
const parsed = JSON.parse(value) as unknown;
if (isRecord(parsed)) return parsed;
} catch {
return {};
}
}
return {};
}
// ---- OpenAI Responses API ----
function toOpenAiInput(messages: AgentTurnMessage[]): unknown[] {
const items: unknown[] = [];
for (const message of messages) {
if (message.role === "user") {
items.push({ role: "user", content: message.text });
} else if (message.role === "assistant") {
for (const call of message.toolCalls) {
items.push({ type: "function_call", call_id: call.id, name: call.name, arguments: JSON.stringify(call.arguments) });
}
} else {
for (const result of message.results) {
items.push({ type: "function_call_output", call_id: result.toolCallId, output: result.output });
}
}
}
return items;
}
function parseOpenAiResult(data: JsonRecord): AgentTurnResult {
const output = Array.isArray(data.output) ? data.output : [];
const textParts: string[] = [];
const toolCalls: ToolCall[] = [];
for (const item of output) {
if (!isRecord(item)) continue;
if (item.type === "function_call") {
const callId = typeof item.call_id === "string" ? item.call_id : typeof item.id === "string" ? item.id : "";
const name = typeof item.name === "string" ? item.name : "";
if (callId && name) toolCalls.push({ id: callId, name, arguments: parseArguments(item.arguments) });
} else if (item.type === "message" && Array.isArray(item.content)) {
for (const block of item.content) {
if (isRecord(block) && block.type === "output_text" && typeof block.text === "string") textParts.push(block.text);
}
}
}
return { text: textParts.join("\n").trim(), toolCalls, rawResponseJson: JSON.stringify(data) };
}
export async function runOpenAiToolTurn(input: ProviderToolTurnInput): Promise<AgentTurnResult> {
const body: JsonRecord = {
model: input.model,
input: toOpenAiInput(input.messages),
tools: input.tools.map((tool) => ({ type: "function", name: tool.name, description: tool.description, parameters: tool.parameters })),
tool_choice: "auto",
...(input.instructions ? { instructions: input.instructions } : {}),
};
const response = await postJson("https://api.openai.com/v1/responses", { authorization: `Bearer ${input.apiKey}` }, body);
return parseOpenAiResult(response.data);
}
// ---- Anthropic Messages API ----
function toAnthropicMessages(messages: AgentTurnMessage[]): unknown[] {
const out: unknown[] = [];
for (const message of messages) {
if (message.role === "user") {
out.push({ role: "user", content: [{ type: "text", text: message.text }] });
} else if (message.role === "assistant") {
const content: unknown[] = [];
if (message.text) content.push({ type: "text", text: message.text });
for (const call of message.toolCalls) content.push({ type: "tool_use", id: call.id, name: call.name, input: call.arguments });
out.push({ role: "assistant", content });
} else {
out.push({ role: "user", content: message.results.map((result) => ({ type: "tool_result", tool_use_id: result.toolCallId, content: result.output })) });
}
}
return out;
}
function parseAnthropicResult(data: JsonRecord): AgentTurnResult {
const content = Array.isArray(data.content) ? data.content : [];
const textParts: string[] = [];
const toolCalls: ToolCall[] = [];
for (const block of content) {
if (!isRecord(block)) continue;
if (block.type === "text" && typeof block.text === "string") textParts.push(block.text);
else if (block.type === "tool_use" && typeof block.id === "string" && typeof block.name === "string") {
toolCalls.push({ id: block.id, name: block.name, arguments: isRecord(block.input) ? block.input : {} });
}
}
return { text: textParts.join("\n").trim(), toolCalls, rawResponseJson: JSON.stringify(data) };
}
export async function runAnthropicToolTurn(input: ProviderToolTurnInput): Promise<AgentTurnResult> {
const body: JsonRecord = {
model: input.model,
max_tokens: 4096,
tools: input.tools.map((tool) => ({ name: tool.name, description: tool.description, input_schema: tool.parameters })),
messages: toAnthropicMessages(input.messages),
...(input.instructions ? { system: input.instructions } : {}),
};
const response = await postJson("https://api.anthropic.com/v1/messages", { "x-api-key": input.apiKey, "anthropic-version": "2023-06-01" }, body);
return parseAnthropicResult(response.data);
}
export type ToolTurnFn = (input: ProviderToolTurnInput) => Promise<AgentTurnResult>;
/** Provider keys that can drive a native tool-use loop, by wire protocol. */
export function selectToolTurn(providerKey: string): ToolTurnFn {
if (providerKey === "openai") return runOpenAiToolTurn;
if (providerKey === "anthropic") return runAnthropicToolTurn;
throw new Error(`${providerKey} does not support agent tool execution; assign the agent an OpenAI or Anthropic provider.`);
}
export function supportsToolTurn(providerKey: string): boolean {
return providerKey === "openai" || providerKey === "anthropic";
}
@@ -1,10 +1,13 @@
import type { DatabaseSync } from "node:sqlite";
import type { RunTaskInput } from "../../shared/commands";
import { APPROVAL_STATE, PROVIDER_EXECUTION_MODE, RISK_LEVEL } from "../../shared/domain";
import { APPROVAL_STATE, RISK_LEVEL } from "../../shared/domain";
import { createId } from "../../shared/ids";
import { AuditService } from "./audit-service";
import { ProviderService } from "./provider-service";
import { OrganizationService } from "./organization-service";
import { SkillService } from "./skill-service";
import { runAgentLoop } from "./agent-run-loop";
import type { AgentToolContext } from "./agent-tools";
const TASK_RUN_STATE = {
RUNNING: "running",
@@ -82,8 +85,19 @@ interface TaskRunIsolation {
secretScopeJson: string;
}
interface RunRow {
task_id: string;
agent_id: string;
provider_id: string;
execution_context_id: string;
service_identity_id: string;
workspace_mount_json: string;
secret_scope_json: string;
}
export class TaskExecutionService {
private readonly audit: AuditService;
private readonly organization: OrganizationService;
constructor(
private readonly db: DatabaseSync,
@@ -91,6 +105,7 @@ export class TaskExecutionService {
private readonly skills: SkillService = new SkillService(db, ""),
) {
this.audit = new AuditService(db);
this.organization = new OrganizationService(db);
}
async runTask(input: RunTaskInput): Promise<string> {
@@ -101,35 +116,79 @@ export class TaskExecutionService {
const workspace = this.workspace(task.workspace_id);
const isolation = this.createIsolationContext(input.orgId, task, agent, provider, workspace);
const runId = this.createRun(input.orgId, task.id, agent.id, provider.id, isolation);
return this.executeAgentRun({ orgId: input.orgId, runId, task, agent, provider, workspace, isolation });
}
/**
* Executes an existing `running` task_run (claimed by the autonomous queue
* worker). System-initiated, so it does not re-gate on the manual execution
* approval — the operator configured the company to run autonomously.
*/
async executeRun(orgId: string, runId: string): Promise<string> {
const run = this.db.prepare(`
select task_id, agent_id, provider_id, execution_context_id, service_identity_id, workspace_mount_json, secret_scope_json
from task_runs where id = ? and org_id = ?
`).get(runId, orgId) as RunRow | undefined;
if (!run) throw new Error("Task run was not found.");
const task = this.requireTask(orgId, run.task_id);
const agent = this.requireAssignedAgent(orgId, task);
const provider = this.requireAgentProvider(orgId, agent);
const workspace = this.workspace(task.workspace_id);
const isolation: TaskRunIsolation = {
executionContextId: run.execution_context_id,
serviceIdentityId: run.service_identity_id,
workspaceMountJson: run.workspace_mount_json,
secretScopeJson: run.secret_scope_json,
};
return this.executeAgentRun({ orgId, runId, task, agent, provider, workspace, isolation });
}
private async executeAgentRun(args: {
orgId: string;
runId: string;
task: TaskExecutionTaskRow;
agent: TaskExecutionAgentRow;
provider: TaskExecutionProviderRow;
workspace: WorkspaceRow | null;
isolation: TaskRunIsolation;
}): Promise<string> {
const { orgId, runId, task, agent, provider, workspace, isolation } = args;
let providerRequestId: string | null = null;
let skillRuntimeSnapshot: SkillRuntimeSnapshotRow | null = null;
try {
skillRuntimeSnapshot = this.resolveSkillRuntimeSnapshot(input.orgId, agent.id, runId);
const requestId = await this.providers.createResponse({
orgId: input.orgId,
providerId: provider.id,
workspaceId: task.workspace_id,
prompt: buildTaskPrompt({
task,
agent,
provider,
project: this.project(task.project_id),
goal: this.goal(task.goal_id),
workspace,
isolation,
skillRuntimeSnapshot,
}),
instructions: buildAgentInstructions(agent),
executionMode: PROVIDER_EXECUTION_MODE.FOREGROUND,
onRequestCreated: (requestId) => {
providerRequestId = requestId;
this.linkProviderRequest(input.orgId, runId, requestId);
skillRuntimeSnapshot = this.resolveSkillRuntimeSnapshot(orgId, agent.id, runId);
const toolContext: AgentToolContext = {
orgId,
agent: { id: agent.id, modelProviderId: agent.model_provider_id, modelId: agent.model_id },
taskProjectId: task.project_id,
taskGoalId: task.goal_id,
workspace: workspace ? { id: workspace.id, localPath: workspace.local_path } : null,
};
const result = await runAgentLoop(
{ providers: this.providers, tools: { db: this.db, organization: this.organization } },
{
orgId,
providerId: provider.id,
workspaceId: task.workspace_id,
instructions: buildAgentInstructions(agent),
initialUserTurn: buildTaskPrompt({
task,
agent,
provider,
project: this.project(task.project_id),
goal: this.goal(task.goal_id),
workspace,
isolation,
skillRuntimeSnapshot,
hasReports: this.agentHasReports(orgId, agent.id),
}),
toolContext,
},
});
this.completeRun(input.orgId, runId, requestId);
);
providerRequestId = result.lastRequestId;
this.completeRun(orgId, runId, result.lastRequestId);
this.audit.record({
orgId: input.orgId,
orgId,
entityType: "task_run",
entityId: runId,
actorType: "system",
@@ -139,7 +198,9 @@ export class TaskExecutionService {
taskId: task.id,
agentId: agent.id,
providerId: provider.id,
providerRequestId: requestId,
providerRequestId: result.lastRequestId,
iterations: result.iterations,
toolCalls: result.toolCallCount,
executionContextId: isolation.executionContextId,
serviceIdentityId: isolation.serviceIdentityId,
skillRuntimeSnapshotId: skillRuntimeSnapshot?.id ?? null,
@@ -148,9 +209,9 @@ export class TaskExecutionService {
return runId;
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
this.failRun(input.orgId, runId, providerRequestId, message);
this.failRun(orgId, runId, providerRequestId, message);
this.audit.record({
orgId: input.orgId,
orgId,
entityType: "task_run",
entityId: runId,
actorType: "system",
@@ -252,9 +313,10 @@ export class TaskExecutionService {
return runId;
}
private linkProviderRequest(orgId: string, runId: string, providerRequestId: string): void {
this.db.prepare("update task_runs set provider_request_id = ? where id = ? and org_id = ?")
.run(providerRequestId, runId, orgId);
private agentHasReports(orgId: string, agentId: string): boolean {
const row = this.db.prepare("select 1 from agents where org_id = ? and manager_agent_id = ? and lifecycle = 'active' limit 1")
.get(orgId, agentId);
return Boolean(row);
}
private completeRun(orgId: string, runId: string, providerRequestId: string): void {
@@ -345,10 +407,9 @@ export class TaskExecutionService {
function buildAgentInstructions(agent: TaskExecutionAgentRow): string {
return [
`You are ${agent.name}, ${agent.title || agent.role}.`,
`Department: ${agent.department}.`,
`Role level: ${agent.role_level}.`,
"Return an auditable execution result with concrete output and next action.",
`You are ${agent.name}, ${agent.title || agent.role} (${agent.department}, ${agent.role_level}) operating an autonomous AI company.`,
"You act by calling tools: hire_agent staffs your team, create_task delegates work and assigns it by agent name, update_task_status tracks progress, and write_deliverable produces real files in the company workspace.",
"Take concrete action with the tools instead of only describing a plan. When your team is staffed, the work is delegated, and the first operating deliverable is written, give a brief closing summary with no further tool calls.",
].join("\n");
}
@@ -361,6 +422,7 @@ function buildTaskPrompt(input: {
workspace: WorkspaceRow | null;
isolation: TaskRunIsolation;
skillRuntimeSnapshot: SkillRuntimeSnapshotRow | null;
hasReports: boolean;
}): string {
const lines = [
"Execute this AgentCompany task.",
@@ -399,6 +461,13 @@ function buildTaskPrompt(input: {
`Skill revisions: ${input.skillRuntimeSnapshot.source_revision_ids}`,
);
}
lines.push("", "Deliverable: completed work, decisions made, evidence needed, and next action.");
if (!input.hasReports) {
lines.push(
"",
"Your team is currently empty. Hire 3-5 direct reports for the roles this goal needs (hire_agent), create and assign their first tasks (create_task), and write the initial operating plan into the workspace (write_deliverable). Then summarize.",
);
} else {
lines.push("", "Deliver completed work: make decisions, delegate or update tasks, and write real files with write_deliverable where appropriate.");
}
return lines.join("\n");
}
@@ -13,6 +13,7 @@ test("onboards, hires, and creates work in the Electron shell", async () => {
env: {
...process.env,
AGENTCOMPANY_NEW_PROFILE_DIR: profileDir,
AGENTCOMPANY_NEW_AUTORUN: "0",
},
});
@@ -9,7 +9,7 @@ test("captures every page for visual review", async () => {
mkdirSync(profileDir, { recursive: true });
const app = await electron.launch({
args: [resolve("out/main/index.js")],
env: { ...process.env, AGENTCOMPANY_NEW_PROFILE_DIR: profileDir },
env: { ...process.env, AGENTCOMPANY_NEW_PROFILE_DIR: profileDir, AGENTCOMPANY_NEW_AUTORUN: "0" },
});
const outDir = resolve("test-results/screenshots");
mkdirSync(outDir, { recursive: true });
@@ -0,0 +1,101 @@
import { existsSync, mkdirSync, readFileSync } from "node:fs";
import { randomUUID } from "node:crypto";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { describe, expect, test } from "vitest";
import { DatabaseConnection } from "../../src/main/database/connection";
import { OnboardingService } from "../../src/main/services/onboarding-service";
import { ProviderService } from "../../src/main/services/provider-service";
import { providerAdapters } from "../../src/main/services/provider-adapters";
import { TaskExecutionService } from "../../src/main/services/task-execution-service";
import { SnapshotService } from "../../src/main/services/snapshot-service";
import { claimNextQueuedRun } from "../../src/main/app/task-queue-worker";
import type { AgentTurnResult, ProviderToolTurnInput, ToolCall } from "../../src/main/services/provider-tool-turn";
const testVault = {
encryptSecret: (secret: string) => Buffer.from(secret, "utf8"),
decryptSecret: (ciphertext: Buffer) => ciphertext.toString("utf8"),
};
function call(name: string, args: Record<string, unknown>): ToolCall {
return { id: `call_${randomUUID()}`, name, arguments: args };
}
/** Scripts the CEO: staff two reports, delegate a task, write the plan, then stop. */
function scriptedCeo(): (input: ProviderToolTurnInput) => Promise<AgentTurnResult> {
const script: Array<{ text: string; toolCalls: ToolCall[] }> = [
{ text: "Staffing.", toolCalls: [
call("hire_agent", { name: "Mara Quinn", role: "Growth Marketer", department: "Marketing" }),
call("hire_agent", { name: "Owen Diaz", role: "Operations Lead", department: "Operations" }),
] },
{ text: "Delegating.", toolCalls: [
call("create_task", { title: "Launch the booking site", description: "Ship landing + booking flow.", assignee_agent_name: "Mara Quinn" }),
] },
{ text: "Writing the plan.", toolCalls: [
call("write_deliverable", { path: "plan/operating-plan.md", content: "# OrbitOps Operating Plan\n\n- Validate demand\n- Launch booking site\n" }),
] },
{ text: "Team staffed, work delegated, plan written.", toolCalls: [] },
];
let index = 0;
return async () => {
const turn = script[index] ?? { text: "done", toolCalls: [] };
index += 1;
return { text: turn.text, toolCalls: turn.toolCalls, rawResponseJson: JSON.stringify({ id: `resp_${index}` }) };
};
}
describe("autonomous bootstrap run", () => {
test("the queued kickoff run autonomously hires, delegates, and writes a deliverable", async () => {
const connection = new DatabaseConnection(join(tmpdir(), `ac-auto-${randomUUID()}.db`));
const workspacePath = join(tmpdir(), `ac-auto-ws-${randomUUID()}`);
mkdirSync(workspacePath, { recursive: true });
const providers = new ProviderService(connection.db, providerAdapters, testVault, undefined, scriptedCeo());
const onboarding = new OnboardingService({ db: connection.db, saveProviderCredential: (input) => providers.saveCredential(input) });
const snapshot = new SnapshotService(connection.db);
let orgId = "";
connection.transaction(() => {
const summary = onboarding.bootstrap({
companyName: "OrbitOps AI",
companyDescription: "AI-run operations company.",
employeeName: "Yang",
workspacePath,
executionMode: "ai_ceo",
autoApproveHires: true,
reviewDeliverables: false,
goalTitle: "Stand up a local service business in 30 days.",
goalDescription: "Validate the market and win the first paying customers.",
providerKey: "openai",
providerSecret: "sk-test-key",
providerModel: "gpt-5.4",
});
orgId = summary.orgId;
});
// The bootstrap created exactly one queued run; the worker claims it.
const claimed = claimNextQueuedRun(connection);
expect(claimed?.org_id).toBe(orgId);
expect(claimNextQueuedRun(connection)).toBeNull();
const taskExecution = new TaskExecutionService(connection.db, providers);
await taskExecution.executeRun(claimed!.org_id, claimed!.id);
const state = snapshot.load();
const agentNames = state.agents.filter((a) => a.orgId === orgId).map((a) => a.name).sort();
expect(agentNames).toEqual(["CEO", "Mara Quinn", "Owen Diaz"]);
const delegated = state.tasks.find((t) => t.orgId === orgId && t.title === "Launch the booking site");
expect(delegated).toBeTruthy();
const mara = state.agents.find((a) => a.orgId === orgId && a.name === "Mara Quinn");
expect(delegated?.assigneeAgentId).toBe(mara?.id);
const deliverable = join(workspacePath, "plan", "operating-plan.md");
expect(existsSync(deliverable)).toBe(true);
expect(readFileSync(deliverable, "utf8")).toContain("OrbitOps Operating Plan");
const run = state.taskRuns.find((r) => r.id === claimed!.id);
expect(run?.state).toBe("completed");
connection.close();
});
});
@@ -0,0 +1,135 @@
import { existsSync, mkdirSync, readFileSync } from "node:fs";
import { randomUUID } from "node:crypto";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { describe, expect, test } from "vitest";
import { DatabaseConnection } from "../../src/main/database/connection";
import { OrganizationService } from "../../src/main/services/organization-service";
import { runAgentLoop } from "../../src/main/services/agent-run-loop";
import type { AgentToolContext } from "../../src/main/services/agent-tools";
import type { AgentTurnResult, ToolCall } from "../../src/main/services/provider-tool-turn";
interface ScriptedTurn {
text: string;
toolCalls: ToolCall[];
}
function fakeProvider(script: ScriptedTurn[]) {
let index = 0;
return {
runToolTurn: async (): Promise<AgentTurnResult & { requestId: string }> => {
const turn = script[index] ?? { text: "done", toolCalls: [] };
index += 1;
return { text: turn.text, toolCalls: turn.toolCalls, rawResponseJson: "{}", requestId: `req_${index}` };
},
};
}
function call(name: string, args: Record<string, unknown>): ToolCall {
return { id: `call_${randomUUID()}`, name, arguments: args };
}
describe("agent run loop (real tool dispatch)", () => {
test("the CEO hires, creates a task, writes a deliverable, then converges", async () => {
const connection = new DatabaseConnection(join(tmpdir(), `ac-loop-${randomUUID()}.db`));
const organization = new OrganizationService(connection.db);
const workspaceRoot = join(tmpdir(), `ac-ws-${randomUUID()}`);
mkdirSync(workspaceRoot, { recursive: true });
let orgId = "";
let ceoId = "";
let providerId = "";
const workspaceId = "wsp_test";
connection.transaction(() => {
orgId = organization.createOrganization({
organizationName: "OrbitOps", employeeName: "Yang", mission: "Run an AI company.", goal: "Win customers.",
});
providerId = (connection.db.prepare("select id from providers where org_id = ? and provider_key = 'openai'").get(orgId) as { id: string }).id;
connection.db.prepare(`
insert into workspaces(id, org_id, name, local_path, repo_url, repo_ref, project_id, is_primary, created_at, updated_at)
values (?, ?, 'Default Workspace', ?, '', '', null, 1, datetime('now'), datetime('now'))
`).run(workspaceId, orgId, workspaceRoot);
ceoId = organization.createAgent({
orgId, name: "CEO", role: "CEO", title: "Chief Executive Officer", department: "Executive",
modelProviderId: providerId, modelId: "gpt-5.4", budgetCents: 25000, managerAgentId: null,
roleLevel: "c_suite", workspaceId,
});
});
const toolContext: AgentToolContext = {
orgId,
agent: { id: ceoId, modelProviderId: providerId, modelId: "gpt-5.4" },
taskProjectId: null,
taskGoalId: null,
workspace: { id: workspaceId, localPath: workspaceRoot },
};
const script: ScriptedTurn[] = [
{ text: "Staffing the team.", toolCalls: [
call("hire_agent", { name: "Alex Rivera", role: "Growth Marketer", department: "Marketing" }),
call("hire_agent", { name: "Sam Lee", role: "Operations Lead", department: "Operations" }),
] },
{ text: "Delegating.", toolCalls: [
call("create_task", { title: "Build the booking site", description: "Ship a landing + booking flow.", assignee_agent_name: "Alex Rivera" }),
] },
{ text: "Writing the plan.", toolCalls: [
call("write_deliverable", { path: "plan/operating-plan.md", content: "# Operating Plan\n\n1. Validate demand.\n2. Launch booking site.\n" }),
] },
{ text: "Team staffed, work delegated, plan written.", toolCalls: [] },
];
const result = await runAgentLoop(
{ providers: fakeProvider(script), tools: { db: connection.db, organization } },
{ orgId, providerId, workspaceId, instructions: "You are the CEO.", initialUserTurn: "Start the company.", toolContext },
);
expect(result.iterations).toBe(4);
expect(result.toolCallCount).toBe(4);
expect(result.finalText).toContain("Team staffed");
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"]);
const task = connection.db.prepare("select title, assignee_agent_id from tasks where org_id = ?").get(orgId) as { title: string; assignee_agent_id: string } | undefined;
expect(task?.title).toBe("Build the booking site");
const alexId = (connection.db.prepare("select id from agents where org_id = ? and name = 'Alex Rivera'").get(orgId) as { id: string }).id;
expect(task?.assignee_agent_id).toBe(alexId);
const deliverablePath = join(workspaceRoot, "plan", "operating-plan.md");
expect(existsSync(deliverablePath)).toBe(true);
expect(readFileSync(deliverablePath, "utf8")).toContain("Operating Plan");
connection.close();
});
test("rejects deliverable paths that escape the workspace", async () => {
const connection = new DatabaseConnection(join(tmpdir(), `ac-loop2-${randomUUID()}.db`));
const organization = new OrganizationService(connection.db);
const workspaceRoot = join(tmpdir(), `ac-ws2-${randomUUID()}`);
mkdirSync(workspaceRoot, { recursive: true });
let orgId = "";
let ceoId = "";
let providerId = "";
connection.transaction(() => {
orgId = organization.createOrganization({ organizationName: "OrbitOps", employeeName: "Yang", mission: "m", goal: "g" });
providerId = (connection.db.prepare("select id from providers where org_id = ? and provider_key = 'openai'").get(orgId) as { id: string }).id;
ceoId = organization.createAgent({ orgId, name: "CEO", role: "CEO", department: "Executive", modelProviderId: providerId, modelId: "gpt-5.4", budgetCents: 25000, managerAgentId: null });
});
const ctx: AgentToolContext = {
orgId, agent: { id: ceoId, modelProviderId: providerId, modelId: "gpt-5.4" },
taskProjectId: null, taskGoalId: null, workspace: { id: "wsp", localPath: workspaceRoot },
};
const script: ScriptedTurn[] = [
{ text: "escape", toolCalls: [call("write_deliverable", { path: "../escape.txt", content: "x" })] },
{ text: "done", toolCalls: [] },
];
const result = await runAgentLoop(
{ providers: fakeProvider(script), tools: { db: connection.db, organization } },
{ orgId, providerId, workspaceId: null, instructions: "i", initialUserTurn: "u", toolContext: ctx },
);
// The tool returns an error string to the model; nothing is written outside the root.
expect(existsSync(join(workspaceRoot, "..", "escape.txt"))).toBe(false);
expect(result.toolCallCount).toBe(1);
connection.close();
});
});
@@ -7,7 +7,8 @@ import { DatabaseConnection } from "../../src/main/database/connection";
import { ApprovalService } from "../../src/main/services/approval-service";
import { OrganizationService } from "../../src/main/services/organization-service";
import { ProviderService } from "../../src/main/services/provider-service";
import { providerAdapters, type ProviderAdapterRegistry } from "../../src/main/services/provider-adapters";
import { providerAdapters } from "../../src/main/services/provider-adapters";
import type { AgentTurnResult, ProviderToolTurnInput } from "../../src/main/services/provider-tool-turn";
import { SkillService } from "../../src/main/services/skill-service";
import { SnapshotService } from "../../src/main/services/snapshot-service";
import { TaskExecutionService } from "../../src/main/services/task-execution-service";
@@ -33,21 +34,12 @@ describe("task execution service", () => {
const organization = new OrganizationService(connection.db);
const snapshot = new SnapshotService(connection.db);
let capturedPrompt = "";
const adapters: ProviderAdapterRegistry = {
...providerAdapters,
openai: {
...providerAdapters.openai,
createResponse: async ({ prompt, model }) => {
capturedPrompt = prompt;
return {
text: `executed:${model}`,
rawResponseJson: JSON.stringify({ id: "resp-task-1" }),
rateLimitState: null,
};
},
},
const fakeToolTurn = async (input: ProviderToolTurnInput): Promise<AgentTurnResult> => {
const first = input.messages[0];
if (first?.role === "user") capturedPrompt = first.text;
return { text: "executed:gpt-5.4", toolCalls: [], rawResponseJson: JSON.stringify({ id: "resp-task-1" }) };
};
const providers = new ProviderService(connection.db, adapters, testVault);
const providers = new ProviderService(connection.db, providerAdapters, testVault, undefined, fakeToolTurn);
const skills = new SkillService(connection.db, join(tmpdir(), `agentcompany-task-skills-${randomUUID()}`));
const execution = new TaskExecutionService(connection.db, providers);
const skillDir = join(tmpdir(), `agentcompany-task-skill-${randomUUID()}`);
@@ -149,18 +141,12 @@ describe("task execution service", () => {
const organization = new OrganizationService(connection.db);
const approvals = new ApprovalService(connection.db);
const snapshot = new SnapshotService(connection.db);
const adapters: ProviderAdapterRegistry = {
...providerAdapters,
openai: {
...providerAdapters.openai,
createResponse: async ({ model }) => ({
text: `executed:${model}`,
rawResponseJson: JSON.stringify({ id: "resp-task-approved" }),
rateLimitState: null,
}),
},
};
const providers = new ProviderService(connection.db, adapters, testVault);
const fakeToolTurn = async (): Promise<AgentTurnResult> => ({
text: "executed:gpt-5.4",
toolCalls: [],
rawResponseJson: JSON.stringify({ id: "resp-task-approved" }),
});
const providers = new ProviderService(connection.db, providerAdapters, testVault, undefined, fakeToolTurn);
const execution = new TaskExecutionService(connection.db, providers);
let orgId = "";
let employeeId = "";
@@ -231,16 +217,10 @@ describe("task execution service", () => {
const connection = createConnection();
const organization = new OrganizationService(connection.db);
const snapshot = new SnapshotService(connection.db);
const adapters: ProviderAdapterRegistry = {
...providerAdapters,
openai: {
...providerAdapters.openai,
createResponse: async () => {
throw new Error("provider rejected task");
},
},
const fakeToolTurn = async (): Promise<AgentTurnResult> => {
throw new Error("provider rejected task");
};
const providers = new ProviderService(connection.db, adapters, testVault);
const providers = new ProviderService(connection.db, providerAdapters, testVault, undefined, fakeToolTurn);
const execution = new TaskExecutionService(connection.db, providers);
let orgId = "";
let taskId = "";
@@ -282,7 +262,7 @@ describe("task execution service", () => {
await expect(execution.runTask({ orgId, taskId })).rejects.toThrow("provider rejected task");
const state = snapshot.load();
const run = state.taskRuns[0];
const request = state.providerRequests.find((item) => item.id === run?.providerRequestId);
const request = state.providerRequests.find((item) => item.state === "failed");
expect(run).toMatchObject({ taskId, state: "failed", errorMessage: "provider rejected task" });
expect(request).toMatchObject({ state: "failed", errorMessage: "provider rejected task" });
@@ -0,0 +1,53 @@
import { randomUUID } from "node:crypto";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { describe, expect, test } from "vitest";
import { RISK_LEVEL, TASK_PRIORITY, TASK_STATUS } from "../../src/shared/domain";
import { DatabaseConnection } from "../../src/main/database/connection";
import { OrganizationService } from "../../src/main/services/organization-service";
import { claimNextQueuedRun } from "../../src/main/app/task-queue-worker";
function seedQueuedRun(connection: DatabaseConnection, orgId: string, taskId: string, agentId: string, providerId: string): string {
const runId = `run_${randomUUID()}`;
connection.db.prepare(`
insert into task_runs(
id, org_id, task_id, agent_id, provider_id, provider_request_id,
execution_context_id, service_identity_id, workspace_mount_json, secret_scope_json,
trigger, state, created_at, started_at, completed_at, error_message
) values (?, ?, ?, ?, ?, null, ?, null, '[]', '{}', 'bootstrap', 'queued', datetime('now'), null, null, null)
`).run(runId, orgId, taskId, agentId, providerId, `ctx_${randomUUID()}`);
return runId;
}
describe("task queue worker claim", () => {
test("claims queued runs once each, in order, then returns null", () => {
const connection = new DatabaseConnection(join(tmpdir(), `ac-queue-${randomUUID()}.db`));
const organization = new OrganizationService(connection.db);
let orgId = "";
let agentId = "";
let providerId = "";
let taskId = "";
connection.transaction(() => {
orgId = organization.createOrganization({ organizationName: "Acme", employeeName: "Dana", mission: "m", goal: "g" });
providerId = (connection.db.prepare("select id from providers where org_id = ? and provider_key = 'openai'").get(orgId) as { id: string }).id;
agentId = organization.createAgent({ orgId, name: "CEO", role: "CEO", department: "Executive", modelProviderId: providerId, modelId: "gpt-5.4", budgetCents: 25000, managerAgentId: null });
taskId = organization.createTask({
orgId, title: "Kickoff", description: "Start.", status: TASK_STATUS.BACKLOG, assigneeAgentId: agentId,
risk: RISK_LEVEL.LOW, projectId: null, goalId: null, workspaceId: null, priority: TASK_PRIORITY.NORMAL, requiresFilesystem: false,
});
});
const first = seedQueuedRun(connection, orgId, taskId, agentId, providerId);
const second = seedQueuedRun(connection, orgId, taskId, agentId, providerId);
const claimedA = claimNextQueuedRun(connection);
const claimedB = claimNextQueuedRun(connection);
const claimedC = claimNextQueuedRun(connection);
expect([claimedA?.id, claimedB?.id].sort()).toEqual([first, second].sort());
expect(claimedC).toBeNull();
const states = (connection.db.prepare("select state from task_runs where org_id = ? order by created_at").all(orgId) as Array<{ state: string }>).map((r) => r.state);
expect(states).toEqual(["running", "running"]);
connection.close();
});
});