Let agents post coordination messages so company channels are populated
This commit is contained in:
1 parent
54ad9d1909
commit
e14a4bfce8
4 files changed
+65
-8
No files matched your search
@@ -1,6 +1,7 @@
|
|||||||
import type { DatabaseSync } from "node:sqlite";
|
import type { DatabaseSync } from "node:sqlite";
|
||||||
import { RISK_LEVEL, TASK_PRIORITY, TASK_STATUS, type RiskLevel } from "../../shared/domain";
|
import { RISK_LEVEL, TASK_PRIORITY, TASK_STATUS, type RiskLevel } from "../../shared/domain";
|
||||||
import type { OrganizationService } from "./organization-service";
|
import type { OrganizationService } from "./organization-service";
|
||||||
|
import type { ChatService } from "./chat-service";
|
||||||
import { writeDeliverable } from "./deliverable-writer";
|
import { writeDeliverable } from "./deliverable-writer";
|
||||||
import { recordWorkspaceFile } from "./files/file-object-store";
|
import { recordWorkspaceFile } from "./files/file-object-store";
|
||||||
import type { JsonRecord } from "./provider-http";
|
import type { JsonRecord } from "./provider-http";
|
||||||
@@ -21,6 +22,7 @@ export interface AgentToolContext {
|
|||||||
export interface AgentToolDeps {
|
export interface AgentToolDeps {
|
||||||
db: DatabaseSync;
|
db: DatabaseSync;
|
||||||
organization: OrganizationService;
|
organization: OrganizationService;
|
||||||
|
chat: ChatService;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** The tools the acting agent may call during a run. */
|
/** The tools the acting agent may call during a run. */
|
||||||
@@ -82,6 +84,19 @@ export const AGENT_TOOLS: ToolDefinition[] = [
|
|||||||
required: ["path", "content"],
|
required: ["path", "content"],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "send_message",
|
||||||
|
description: "Post a short coordination message to a company channel (general, product, or engineering) so the team and the operator can follow the work.",
|
||||||
|
parameters: {
|
||||||
|
type: "object",
|
||||||
|
additionalProperties: false,
|
||||||
|
properties: {
|
||||||
|
channel: { type: "string", description: "Channel name, e.g. 'general'." },
|
||||||
|
body: { type: "string", description: "The message to post." },
|
||||||
|
},
|
||||||
|
required: ["channel", "body"],
|
||||||
|
},
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
export function dispatchAgentTool(deps: AgentToolDeps, ctx: AgentToolContext, call: ToolCall): string {
|
export function dispatchAgentTool(deps: AgentToolDeps, ctx: AgentToolContext, call: ToolCall): string {
|
||||||
@@ -95,6 +110,8 @@ export function dispatchAgentTool(deps: AgentToolDeps, ctx: AgentToolContext, ca
|
|||||||
return updateTaskStatus(deps, ctx, call.arguments);
|
return updateTaskStatus(deps, ctx, call.arguments);
|
||||||
case "write_deliverable":
|
case "write_deliverable":
|
||||||
return writeDeliverableTool(deps, ctx, call.arguments);
|
return writeDeliverableTool(deps, ctx, call.arguments);
|
||||||
|
case "send_message":
|
||||||
|
return sendMessageTool(deps, ctx, call.arguments);
|
||||||
default:
|
default:
|
||||||
return `ERROR: unknown tool "${call.name}".`;
|
return `ERROR: unknown tool "${call.name}".`;
|
||||||
}
|
}
|
||||||
@@ -170,6 +187,25 @@ function writeDeliverableTool(deps: AgentToolDeps, ctx: AgentToolContext, args:
|
|||||||
return `Wrote deliverable ${written} (${content.length} chars).`;
|
return `Wrote deliverable ${written} (${content.length} chars).`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function sendMessageTool(deps: AgentToolDeps, ctx: AgentToolContext, args: JsonRecord): string {
|
||||||
|
const channel = requireString(args.channel, "channel");
|
||||||
|
const body = requireString(args.body, "body");
|
||||||
|
const space = deps.db.prepare(
|
||||||
|
"select id from chat_spaces where org_id = ? and lower(name) = lower(?) and space_type <> 'direct' limit 1",
|
||||||
|
).get(ctx.orgId, channel) as { id: string } | undefined;
|
||||||
|
if (!space) throw new Error(`No channel named "${channel}".`);
|
||||||
|
const messageId = deps.chat.sendMessage({
|
||||||
|
orgId: ctx.orgId,
|
||||||
|
spaceId: space.id,
|
||||||
|
threadId: null,
|
||||||
|
senderType: "agent",
|
||||||
|
senderId: ctx.agent.id,
|
||||||
|
bodyMd: body,
|
||||||
|
attachmentFileIds: [],
|
||||||
|
});
|
||||||
|
return `Posted to #${channel} (message_id=${messageId}).`;
|
||||||
|
}
|
||||||
|
|
||||||
function resolveAgentByName(db: DatabaseSync, orgId: string, name: string): string | null {
|
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")
|
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;
|
.get(orgId, name.trim()) as { id: string } | undefined;
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { createId } from "../../shared/ids";
|
|||||||
import { AuditService } from "./audit-service";
|
import { AuditService } from "./audit-service";
|
||||||
import { ProviderService } from "./provider-service";
|
import { ProviderService } from "./provider-service";
|
||||||
import { OrganizationService } from "./organization-service";
|
import { OrganizationService } from "./organization-service";
|
||||||
|
import { ChatService } from "./chat-service";
|
||||||
import { SkillService } from "./skill-service";
|
import { SkillService } from "./skill-service";
|
||||||
import { runAgentLoop } from "./agent-run-loop";
|
import { runAgentLoop } from "./agent-run-loop";
|
||||||
import type { AgentToolContext } from "./agent-tools";
|
import type { AgentToolContext } from "./agent-tools";
|
||||||
@@ -100,6 +101,7 @@ interface RunRow {
|
|||||||
export class TaskExecutionService {
|
export class TaskExecutionService {
|
||||||
private readonly audit: AuditService;
|
private readonly audit: AuditService;
|
||||||
private readonly organization: OrganizationService;
|
private readonly organization: OrganizationService;
|
||||||
|
private readonly chat: ChatService;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private readonly db: DatabaseSync,
|
private readonly db: DatabaseSync,
|
||||||
@@ -108,6 +110,7 @@ export class TaskExecutionService {
|
|||||||
) {
|
) {
|
||||||
this.audit = new AuditService(db);
|
this.audit = new AuditService(db);
|
||||||
this.organization = new OrganizationService(db);
|
this.organization = new OrganizationService(db);
|
||||||
|
this.chat = new ChatService(db);
|
||||||
}
|
}
|
||||||
|
|
||||||
async runTask(input: RunTaskInput): Promise<string> {
|
async runTask(input: RunTaskInput): Promise<string> {
|
||||||
@@ -167,7 +170,7 @@ export class TaskExecutionService {
|
|||||||
workspace: workspace ? { id: workspace.id, localPath: workspace.local_path } : null,
|
workspace: workspace ? { id: workspace.id, localPath: workspace.local_path } : null,
|
||||||
};
|
};
|
||||||
const result = await runAgentLoop(
|
const result = await runAgentLoop(
|
||||||
{ providers: this.providers, tools: { db: this.db, organization: this.organization } },
|
{ providers: this.providers, tools: { db: this.db, organization: this.organization, chat: this.chat } },
|
||||||
{
|
{
|
||||||
orgId,
|
orgId,
|
||||||
providerId: provider.id,
|
providerId: provider.id,
|
||||||
@@ -471,8 +474,8 @@ export class TaskExecutionService {
|
|||||||
function buildAgentInstructions(agent: TaskExecutionAgentRow): string {
|
function buildAgentInstructions(agent: TaskExecutionAgentRow): string {
|
||||||
return [
|
return [
|
||||||
`You are ${agent.name}, ${agent.title || agent.role} (${agent.department}, ${agent.role_level}) operating an autonomous AI company.`,
|
`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.",
|
"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, write_deliverable produces real files in the company workspace, and send_message posts coordination updates to a company channel (general, product, engineering).",
|
||||||
"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.",
|
"Take concrete action with the tools instead of only describing a plan. Post a brief coordination message to #general with send_message so the team and the operator can follow your decisions. When your team is staffed, the work is delegated, the first operating deliverable is written, and you have posted your update, give a brief closing summary with no further tool calls.",
|
||||||
].join("\n");
|
].join("\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -528,10 +531,10 @@ function buildTaskPrompt(input: {
|
|||||||
if (input.isManager && !input.hasReports) {
|
if (input.isManager && !input.hasReports) {
|
||||||
lines.push(
|
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.",
|
"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), write the initial operating plan into the workspace (write_deliverable), and post a kickoff update to #general (send_message). Then summarize.",
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
lines.push("", "Complete this task: do the work, write your output as real files with write_deliverable, update the task status when done, and summarize.");
|
lines.push("", "Complete this task: do the work, write your output as real files with write_deliverable, post a brief progress update to #general with send_message, update the task status when done, and summarize.");
|
||||||
}
|
}
|
||||||
return lines.join("\n");
|
return lines.join("\n");
|
||||||
}
|
}
|
||||||
@@ -33,6 +33,7 @@ function scriptedCeo(): (input: ProviderToolTurnInput) => Promise<AgentTurnResul
|
|||||||
] },
|
] },
|
||||||
{ text: "Writing the plan.", toolCalls: [
|
{ 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" }),
|
call("write_deliverable", { path: "plan/operating-plan.md", content: "# OrbitOps Operating Plan\n\n- Validate demand\n- Launch booking site\n" }),
|
||||||
|
call("send_message", { channel: "general", body: "Kickoff: team staffed, plan written, booking site delegated to Mara." }),
|
||||||
] },
|
] },
|
||||||
{ text: "Team staffed, work delegated, plan written.", toolCalls: [] },
|
{ text: "Team staffed, work delegated, plan written.", toolCalls: [] },
|
||||||
];
|
];
|
||||||
@@ -101,6 +102,12 @@ describe("autonomous bootstrap run", () => {
|
|||||||
expect(planFile).toBeTruthy();
|
expect(planFile).toBeTruthy();
|
||||||
expect(planFile).toMatchObject({ ownerType: "agent", ownerId: ceo?.id, kind: "md", scanState: "approved" });
|
expect(planFile).toMatchObject({ ownerType: "agent", ownerId: ceo?.id, kind: "md", scanState: "approved" });
|
||||||
|
|
||||||
|
// The CEO posts a coordination message, so the Messages channel is populated.
|
||||||
|
const general = state.chatSpaces.find((s) => s.orgId === orgId && s.name === "general");
|
||||||
|
const kickoff = state.chatMessages.find((m) => m.orgId === orgId && m.spaceId === general?.id);
|
||||||
|
expect(kickoff).toMatchObject({ senderType: "agent", senderId: ceo?.id });
|
||||||
|
expect(kickoff?.bodyMd).toContain("team staffed");
|
||||||
|
|
||||||
const run = state.taskRuns.find((r) => r.id === claimed!.id);
|
const run = state.taskRuns.find((r) => r.id === claimed!.id);
|
||||||
expect(run?.state).toBe("completed");
|
expect(run?.state).toBe("completed");
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { join } from "node:path";
|
|||||||
import { describe, expect, test } from "vitest";
|
import { describe, expect, test } from "vitest";
|
||||||
import { DatabaseConnection } from "../../src/main/database/connection";
|
import { DatabaseConnection } from "../../src/main/database/connection";
|
||||||
import { OrganizationService } from "../../src/main/services/organization-service";
|
import { OrganizationService } from "../../src/main/services/organization-service";
|
||||||
|
import { ChatService } from "../../src/main/services/chat-service";
|
||||||
import { runAgentLoop } from "../../src/main/services/agent-run-loop";
|
import { runAgentLoop } from "../../src/main/services/agent-run-loop";
|
||||||
import type { AgentToolContext } from "../../src/main/services/agent-tools";
|
import type { AgentToolContext } from "../../src/main/services/agent-tools";
|
||||||
import type { AgentTurnResult, ToolCall } from "../../src/main/services/provider-tool-turn";
|
import type { AgentTurnResult, ToolCall } from "../../src/main/services/provider-tool-turn";
|
||||||
@@ -74,17 +75,18 @@ describe("agent run loop (real tool dispatch)", () => {
|
|||||||
] },
|
] },
|
||||||
{ text: "Writing the plan.", toolCalls: [
|
{ 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" }),
|
call("write_deliverable", { path: "plan/operating-plan.md", content: "# Operating Plan\n\n1. Validate demand.\n2. Launch booking site.\n" }),
|
||||||
|
call("send_message", { channel: "general", body: "Team staffed and the operating plan is live. Booking site is next." }),
|
||||||
] },
|
] },
|
||||||
{ text: "Team staffed, work delegated, plan written.", toolCalls: [] },
|
{ text: "Team staffed, work delegated, plan written.", toolCalls: [] },
|
||||||
];
|
];
|
||||||
|
|
||||||
const result = await runAgentLoop(
|
const result = await runAgentLoop(
|
||||||
{ providers: fakeProvider(script), tools: { db: connection.db, organization } },
|
{ providers: fakeProvider(script), tools: { db: connection.db, organization, chat: new ChatService(connection.db) } },
|
||||||
{ orgId, providerId, workspaceId, instructions: "You are the CEO.", initialUserTurn: "Start the company.", toolContext },
|
{ orgId, providerId, workspaceId, instructions: "You are the CEO.", initialUserTurn: "Start the company.", toolContext },
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(result.iterations).toBe(4);
|
expect(result.iterations).toBe(4);
|
||||||
expect(result.toolCallCount).toBe(4);
|
expect(result.toolCallCount).toBe(5);
|
||||||
expect(result.finalText).toContain("Team staffed");
|
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);
|
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);
|
||||||
@@ -99,6 +101,15 @@ describe("agent run loop (real tool dispatch)", () => {
|
|||||||
expect(existsSync(deliverablePath)).toBe(true);
|
expect(existsSync(deliverablePath)).toBe(true);
|
||||||
expect(readFileSync(deliverablePath, "utf8")).toContain("Operating Plan");
|
expect(readFileSync(deliverablePath, "utf8")).toContain("Operating Plan");
|
||||||
|
|
||||||
|
// The CEO's coordination message lands in #general so the channel is no longer blank.
|
||||||
|
const generalSpaceId = (connection.db.prepare("select id from chat_spaces where org_id = ? and name = 'general'").get(orgId) as { id: string }).id;
|
||||||
|
const messages = connection.db.prepare(
|
||||||
|
"select sender_type, sender_id, body_md, space_id from chat_messages where org_id = ?",
|
||||||
|
).all(orgId) as Array<{ sender_type: string; sender_id: string; body_md: string; space_id: string }>;
|
||||||
|
expect(messages).toHaveLength(1);
|
||||||
|
expect(messages[0]).toMatchObject({ sender_type: "agent", sender_id: ceoId, space_id: generalSpaceId });
|
||||||
|
expect(messages[0]?.body_md).toContain("operating plan is live");
|
||||||
|
|
||||||
connection.close();
|
connection.close();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -124,7 +135,7 @@ describe("agent run loop (real tool dispatch)", () => {
|
|||||||
{ text: "done", toolCalls: [] },
|
{ text: "done", toolCalls: [] },
|
||||||
];
|
];
|
||||||
const result = await runAgentLoop(
|
const result = await runAgentLoop(
|
||||||
{ providers: fakeProvider(script), tools: { db: connection.db, organization } },
|
{ providers: fakeProvider(script), tools: { db: connection.db, organization, chat: new ChatService(connection.db) } },
|
||||||
{ orgId, providerId, workspaceId: null, instructions: "i", initialUserTurn: "u", toolContext: ctx },
|
{ 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.
|
// The tool returns an error string to the model; nothing is written outside the root.
|
||||||
|
|||||||
Reference in new issue
Block a user