Split oversized modules along domain seams
Every source file is now under 500 lines. Task prompt construction moves from task-execution-service into task-prompts; goal assignee resolution moves from organization-service into goal-kickoff; the shared command types become a domain-structured shared/commands directory with unchanged import paths; the provider adapter contract types move to provider-adapter-contract, re-exported through provider-adapters so callers keep a single public surface.
This commit is contained in:
1 parent
f964d052e2
commit
cfbe5e1c8e
17 files changed
+899
-873
No files matched your search
@@ -2,6 +2,53 @@ import type { DatabaseSync } from "node:sqlite";
|
||||
import { RISK_LEVEL, TASK_STATUS } from "../../shared/domain";
|
||||
import { createId } from "../../shared/ids";
|
||||
|
||||
export interface CreatedGoalKickoffInput {
|
||||
orgId: string;
|
||||
goalId: string;
|
||||
title: string;
|
||||
description: string;
|
||||
ownerAgentId: string | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Routes a newly created goal to an accountable agent — the chosen owner,
|
||||
* or the top-level agent (the CEO) — as a kickoff task bound to the org's
|
||||
* primary workspace. Returns null when the org has no agent yet; the goal
|
||||
* then waits until one exists.
|
||||
*/
|
||||
export function kickoffCreatedGoal(db: DatabaseSync, input: CreatedGoalKickoffInput): string | null {
|
||||
const assigneeAgentId = input.ownerAgentId ?? topLevelActiveAgentId(db, input.orgId);
|
||||
if (!assigneeAgentId) return null;
|
||||
const organization = db.prepare("select name from organizations where id = ?")
|
||||
.get(input.orgId) as { name: string };
|
||||
return createGoalKickoffTask(db, {
|
||||
orgId: input.orgId,
|
||||
goalId: input.goalId,
|
||||
goalTitle: input.title,
|
||||
goalDescription: input.description,
|
||||
companyName: organization.name,
|
||||
assigneeAgentId,
|
||||
projectId: null,
|
||||
workspaceId: primaryWorkspaceId(db, input.orgId),
|
||||
});
|
||||
}
|
||||
|
||||
function topLevelActiveAgentId(db: DatabaseSync, orgId: string): string | null {
|
||||
const row = db.prepare(`
|
||||
select id from agents
|
||||
where org_id = ? and manager_agent_id is null and lifecycle = 'active'
|
||||
order by created_at asc
|
||||
limit 1
|
||||
`).get(orgId) as { id: string } | undefined;
|
||||
return row?.id ?? null;
|
||||
}
|
||||
|
||||
function primaryWorkspaceId(db: DatabaseSync, orgId: string): string | null {
|
||||
const row = db.prepare("select id from workspaces where org_id = ? and is_primary = 1 order by created_at asc limit 1")
|
||||
.get(orgId) as { id: string } | undefined;
|
||||
return row?.id ?? null;
|
||||
}
|
||||
|
||||
export interface GoalKickoffTaskInput {
|
||||
orgId: string;
|
||||
goalId: string;
|
||||
|
||||
@@ -4,7 +4,7 @@ import { PROVIDER_CATALOG } from "../../shared/provider-catalog";
|
||||
import { createId } from "../../shared/ids";
|
||||
import type { CreateAgentInput, CreateGoalInput, CreateOrganizationInput, CreateProjectInput, CreateTaskInput, CreateWorkspaceInput, DeleteTaskInput, UpdateAgentInput, UpdateTaskInput } from "../../shared/commands";
|
||||
import { AuditService } from "./audit-service";
|
||||
import { createGoalKickoffTask } from "./goal-kickoff";
|
||||
import { kickoffCreatedGoal } from "./goal-kickoff";
|
||||
import { PolicyService } from "./policy-service";
|
||||
import { IdentityAccessService } from "./identity-access-service";
|
||||
import { OrgGovernanceService } from "./org-governance-service";
|
||||
@@ -402,47 +402,14 @@ export class OrganizationService {
|
||||
action: "created",
|
||||
payload: { title: input.title.trim() },
|
||||
});
|
||||
this.kickoffGoal(input, id);
|
||||
return id;
|
||||
}
|
||||
|
||||
/**
|
||||
* An active goal must reach an accountable agent — the chosen owner, or
|
||||
* the top-level agent (the CEO) — as a kickoff task the autonomous
|
||||
* heartbeat starts. Without this, a goal created outside onboarding would
|
||||
* sit inert forever.
|
||||
*/
|
||||
private kickoffGoal(input: CreateGoalInput, goalId: string): void {
|
||||
const assigneeAgentId = input.ownerAgentId ?? this.topLevelActiveAgentId(input.orgId);
|
||||
if (!assigneeAgentId) return;
|
||||
const organization = this.db.prepare("select name from organizations where id = ?")
|
||||
.get(input.orgId) as { name: string };
|
||||
createGoalKickoffTask(this.db, {
|
||||
kickoffCreatedGoal(this.db, {
|
||||
orgId: input.orgId,
|
||||
goalId,
|
||||
goalTitle: input.title,
|
||||
goalDescription: input.description,
|
||||
companyName: organization.name,
|
||||
assigneeAgentId,
|
||||
projectId: null,
|
||||
workspaceId: this.primaryWorkspaceId(input.orgId),
|
||||
goalId: id,
|
||||
title: input.title,
|
||||
description: input.description,
|
||||
ownerAgentId: input.ownerAgentId,
|
||||
});
|
||||
}
|
||||
|
||||
private topLevelActiveAgentId(orgId: string): string | null {
|
||||
const row = this.db.prepare(`
|
||||
select id from agents
|
||||
where org_id = ? and manager_agent_id is null and lifecycle = 'active'
|
||||
order by created_at asc
|
||||
limit 1
|
||||
`).get(orgId) as { id: string } | undefined;
|
||||
return row?.id ?? null;
|
||||
}
|
||||
|
||||
private primaryWorkspaceId(orgId: string): string | null {
|
||||
const row = this.db.prepare("select id from workspaces where org_id = ? and is_primary = 1 order by created_at asc limit 1")
|
||||
.get(orgId) as { id: string } | undefined;
|
||||
return row?.id ?? null;
|
||||
return id;
|
||||
}
|
||||
|
||||
createProject(input: CreateProjectInput): string {
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
import type { ProviderAuthType } from "../../shared/domain";
|
||||
import type { ProviderModelEntry } from "../../shared/provider-models";
|
||||
import type { ProviderObservedRateLimitState } from "../../shared/provider-runtime-domain";
|
||||
import type { ProviderAuthProfile } from "./provider-auth-profiles";
|
||||
import type { ProviderBackgroundCreateResult, ProviderBackgroundRetrieveResult } from "./provider-background";
|
||||
import type { ProviderAuthorizeHandler, ProviderRefreshHandler, ProviderRevokeHandler } from "./provider-auth-lifecycle";
|
||||
import type { ProviderCancelResult, ProviderStreamResult } from "./provider-streaming";
|
||||
|
||||
export interface ProviderExecutionInput {
|
||||
authProfile: ProviderAuthProfile;
|
||||
model: string;
|
||||
prompt: string;
|
||||
instructions: string | null;
|
||||
}
|
||||
|
||||
export interface ProviderExecutionResult {
|
||||
text: string;
|
||||
rawResponseJson: string;
|
||||
rateLimitState: ProviderObservedRateLimitState | null;
|
||||
}
|
||||
|
||||
export type ProviderStreamInput = ProviderExecutionInput;
|
||||
export type ProviderBackgroundCreateInput = ProviderExecutionInput;
|
||||
|
||||
export interface ProviderCancelInput {
|
||||
authProfile: ProviderAuthProfile;
|
||||
responseId: string;
|
||||
}
|
||||
|
||||
export interface ProviderBackgroundRetrieveInput {
|
||||
authProfile: ProviderAuthProfile;
|
||||
responseId: string;
|
||||
}
|
||||
|
||||
export interface ProviderHealthInput {
|
||||
authProfile: ProviderAuthProfile;
|
||||
model: string | null;
|
||||
}
|
||||
|
||||
export interface ProviderHealthResult {
|
||||
checkedAt: string;
|
||||
rateLimitState: ProviderObservedRateLimitState | null;
|
||||
}
|
||||
|
||||
export interface ProviderContext {
|
||||
authProfile: ProviderAuthProfile;
|
||||
model: string | null;
|
||||
}
|
||||
|
||||
export interface ProviderRateLimitState {
|
||||
requestLimit: number | null;
|
||||
remainingRequests: number | null;
|
||||
resetAt: string | null;
|
||||
retryAfterMs: number | null;
|
||||
rawStateJson: string;
|
||||
}
|
||||
|
||||
export interface ProviderAdapter {
|
||||
providerKey: string;
|
||||
authType: ProviderAuthType;
|
||||
authorize: ProviderAuthorizeHandler;
|
||||
refresh: ProviderRefreshHandler;
|
||||
revoke: ProviderRevokeHandler;
|
||||
parseAuthProfile(secret: string): ProviderAuthProfile;
|
||||
listModels(ctx: ProviderContext): Promise<ProviderModelEntry[]>;
|
||||
getRateLimitState(ctx: ProviderContext): Promise<ProviderRateLimitState | null>;
|
||||
checkHealth(input: ProviderHealthInput): Promise<ProviderHealthResult>;
|
||||
streamResponse(input: ProviderStreamInput): Promise<ProviderStreamResult>;
|
||||
cancelResponse(input: ProviderCancelInput): Promise<ProviderCancelResult>;
|
||||
createBackgroundResponse(input: ProviderBackgroundCreateInput): Promise<ProviderBackgroundCreateResult>;
|
||||
retrieveResponse(input: ProviderBackgroundRetrieveInput): Promise<ProviderBackgroundRetrieveResult>;
|
||||
createResponse?: (input: ProviderExecutionInput) => Promise<ProviderExecutionResult>;
|
||||
}
|
||||
|
||||
export type ProviderAdapterRegistry = Record<string, ProviderAdapter>;
|
||||
@@ -20,20 +20,8 @@ import {
|
||||
requireVertexProfile,
|
||||
type ProviderAuthProfile,
|
||||
} from "./provider-auth-profiles";
|
||||
import {
|
||||
backgroundUnsupported,
|
||||
retrieveUnsupported,
|
||||
type ProviderBackgroundCreateResult,
|
||||
type ProviderBackgroundRetrieveResult,
|
||||
} from "./provider-background";
|
||||
import {
|
||||
authorizeWithParser,
|
||||
refreshUnsupported,
|
||||
revokeLocalProfile,
|
||||
type ProviderAuthorizeHandler,
|
||||
type ProviderRefreshHandler,
|
||||
type ProviderRevokeHandler,
|
||||
} from "./provider-auth-lifecycle";
|
||||
import { backgroundUnsupported, retrieveUnsupported } from "./provider-background";
|
||||
import { authorizeWithParser, refreshUnsupported, revokeLocalProfile } from "./provider-auth-lifecycle";
|
||||
import { getJson, postEventStream, postJson, type JsonRecord } from "./provider-http";
|
||||
import { fetchAnthropicModels, fetchOpenAiModels } from "./provider-models-fetch";
|
||||
|
||||
@@ -45,80 +33,18 @@ import {
|
||||
mapOpenAiChatCompletionStream,
|
||||
mapOpenAiResponseStream,
|
||||
streamUnsupported,
|
||||
type ProviderCancelResult,
|
||||
type ProviderStreamResult,
|
||||
} from "./provider-streaming";
|
||||
|
||||
export { ProviderHttpError, providerRateLimitStateFromError } from "./provider-http";
|
||||
export type { ProviderAuthProfile } from "./provider-auth-profiles";
|
||||
export * from "./provider-adapter-contract";
|
||||
|
||||
export interface ProviderExecutionInput {
|
||||
authProfile: ProviderAuthProfile;
|
||||
model: string;
|
||||
prompt: string;
|
||||
instructions: string | null;
|
||||
}
|
||||
|
||||
export interface ProviderExecutionResult {
|
||||
text: string;
|
||||
rawResponseJson: string;
|
||||
rateLimitState: ProviderObservedRateLimitState | null;
|
||||
}
|
||||
|
||||
export type ProviderStreamInput = ProviderExecutionInput;
|
||||
export type ProviderBackgroundCreateInput = ProviderExecutionInput;
|
||||
|
||||
export interface ProviderCancelInput {
|
||||
authProfile: ProviderAuthProfile;
|
||||
responseId: string;
|
||||
}
|
||||
|
||||
export interface ProviderBackgroundRetrieveInput {
|
||||
authProfile: ProviderAuthProfile;
|
||||
responseId: string;
|
||||
}
|
||||
|
||||
export interface ProviderHealthInput {
|
||||
authProfile: ProviderAuthProfile;
|
||||
model: string | null;
|
||||
}
|
||||
|
||||
export interface ProviderHealthResult {
|
||||
checkedAt: string;
|
||||
rateLimitState: ProviderObservedRateLimitState | null;
|
||||
}
|
||||
|
||||
export interface ProviderContext {
|
||||
authProfile: ProviderAuthProfile;
|
||||
model: string | null;
|
||||
}
|
||||
|
||||
export interface ProviderRateLimitState {
|
||||
requestLimit: number | null;
|
||||
remainingRequests: number | null;
|
||||
resetAt: string | null;
|
||||
retryAfterMs: number | null;
|
||||
rawStateJson: string;
|
||||
}
|
||||
|
||||
export interface ProviderAdapter {
|
||||
providerKey: string;
|
||||
authType: ProviderAuthType;
|
||||
authorize: ProviderAuthorizeHandler;
|
||||
refresh: ProviderRefreshHandler;
|
||||
revoke: ProviderRevokeHandler;
|
||||
parseAuthProfile(secret: string): ProviderAuthProfile;
|
||||
listModels(ctx: ProviderContext): Promise<ProviderModelEntry[]>;
|
||||
getRateLimitState(ctx: ProviderContext): Promise<ProviderRateLimitState | null>;
|
||||
checkHealth(input: ProviderHealthInput): Promise<ProviderHealthResult>;
|
||||
streamResponse(input: ProviderStreamInput): Promise<ProviderStreamResult>;
|
||||
cancelResponse(input: ProviderCancelInput): Promise<ProviderCancelResult>;
|
||||
createBackgroundResponse(input: ProviderBackgroundCreateInput): Promise<ProviderBackgroundCreateResult>;
|
||||
retrieveResponse(input: ProviderBackgroundRetrieveInput): Promise<ProviderBackgroundRetrieveResult>;
|
||||
createResponse?: (input: ProviderExecutionInput) => Promise<ProviderExecutionResult>;
|
||||
}
|
||||
|
||||
export type ProviderAdapterRegistry = Record<string, ProviderAdapter>;
|
||||
import type {
|
||||
ProviderAdapter,
|
||||
ProviderAdapterRegistry,
|
||||
ProviderHealthResult,
|
||||
ProviderRateLimitState,
|
||||
} from "./provider-adapter-contract";
|
||||
|
||||
export const providerAdapters: ProviderAdapterRegistry = {
|
||||
openai: {
|
||||
|
||||
@@ -9,7 +9,19 @@ import { ChatService } from "./chat-service";
|
||||
import { SkillService } from "./skill-service";
|
||||
import { runAgentLoop } from "./agent-run-loop";
|
||||
import type { AgentToolContext } from "./agent-tools";
|
||||
import { canonicalMentionHandle } from "./mention-handles";
|
||||
import {
|
||||
buildAgentInstructions,
|
||||
buildTaskPrompt,
|
||||
type ChatContextBlock,
|
||||
type GoalRow,
|
||||
type ProjectRow,
|
||||
type SkillRuntimeSnapshotRow,
|
||||
type TaskExecutionAgentRow,
|
||||
type TaskExecutionProviderRow,
|
||||
type TaskExecutionTaskRow,
|
||||
type TaskRunIsolation,
|
||||
type WorkspaceRow,
|
||||
} from "./task-prompts";
|
||||
|
||||
const TASK_RUN_STATE = {
|
||||
QUEUED: "queued",
|
||||
@@ -18,82 +30,11 @@ const TASK_RUN_STATE = {
|
||||
FAILED: "failed",
|
||||
} as const;
|
||||
|
||||
interface TaskExecutionTaskRow {
|
||||
id: string;
|
||||
org_id: string;
|
||||
title: string;
|
||||
description: string;
|
||||
status: string;
|
||||
assignee_agent_id: string | null;
|
||||
risk: string;
|
||||
project_id: string | null;
|
||||
goal_id: string | null;
|
||||
workspace_id: string | null;
|
||||
priority: string;
|
||||
requires_filesystem: number;
|
||||
}
|
||||
|
||||
interface TaskExecutionAgentRow {
|
||||
id: string;
|
||||
name: string;
|
||||
role: string;
|
||||
department: string;
|
||||
title: string;
|
||||
role_level: string;
|
||||
model_provider_id: string | null;
|
||||
model_id: string | null;
|
||||
manager_agent_id: string | null;
|
||||
}
|
||||
|
||||
interface TaskExecutionProviderRow {
|
||||
id: string;
|
||||
label: string;
|
||||
provider_key: string;
|
||||
model: string | null;
|
||||
}
|
||||
|
||||
interface ProjectRow {
|
||||
name: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
interface GoalRow {
|
||||
title: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
interface WorkspaceRow {
|
||||
id: string;
|
||||
name: string;
|
||||
local_path: string;
|
||||
repo_url: string;
|
||||
repo_ref: string;
|
||||
}
|
||||
|
||||
interface TaskApprovalRow {
|
||||
id: string;
|
||||
state: string;
|
||||
}
|
||||
|
||||
interface SkillRuntimeSnapshotRow {
|
||||
id: string;
|
||||
compiled_prompt: string;
|
||||
tools_manifest: string;
|
||||
source_revision_ids: string;
|
||||
}
|
||||
|
||||
interface TaskRunIsolation {
|
||||
executionContextId: string;
|
||||
serviceIdentityId: string;
|
||||
workspaceMountJson: string;
|
||||
secretScopeJson: string;
|
||||
}
|
||||
|
||||
interface ChatContextBlock {
|
||||
threadId: string;
|
||||
transcript: string;
|
||||
}
|
||||
|
||||
interface RunRow {
|
||||
task_id: string;
|
||||
agent_id: string;
|
||||
@@ -510,93 +451,3 @@ export class TaskExecutionService {
|
||||
return row.id;
|
||||
}
|
||||
}
|
||||
|
||||
function buildAgentInstructions(agent: TaskExecutionAgentRow): string {
|
||||
return [
|
||||
`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, write_deliverable produces real files in the company workspace, create_document produces versioned company documents (PRDs, specs, memos, reports), and send_message posts coordination messages to a channel with optional file attachments.",
|
||||
"Communicate like a real team. Post to the channel that matches the work — 'engineering' for engineering, 'product' for product or design, otherwise 'general'. Use @mentions when addressing a teammate. When you hand work or files to a teammate, use send_message with /handoff @Name and attachment_file_ids containing the file_id returned by write_deliverable. To keep a hand-off as one conversation, reply in the same thread by passing the thread_id a previous send_message returned.",
|
||||
"Take concrete action with the tools and reserve narrative for brief summaries after coordination is posted.",
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
function buildTaskPrompt(input: {
|
||||
task: TaskExecutionTaskRow;
|
||||
agent: TaskExecutionAgentRow;
|
||||
provider: TaskExecutionProviderRow;
|
||||
project: ProjectRow | null;
|
||||
goal: GoalRow | null;
|
||||
workspace: WorkspaceRow | null;
|
||||
isolation: TaskRunIsolation;
|
||||
skillRuntimeSnapshot: SkillRuntimeSnapshotRow | null;
|
||||
isManager: boolean;
|
||||
hasReports: boolean;
|
||||
managerName: string | null;
|
||||
chatContext: ChatContextBlock | null;
|
||||
}): string {
|
||||
const lines = [
|
||||
"Execute this AgentCompany task.",
|
||||
"",
|
||||
`Task: ${input.task.title}`,
|
||||
`Description: ${input.task.description}`,
|
||||
`Status: ${input.task.status}`,
|
||||
`Priority: ${input.task.priority}`,
|
||||
`Risk: ${input.task.risk}`,
|
||||
`Requires filesystem: ${input.task.requires_filesystem === 1 ? "yes" : "no"}`,
|
||||
`Assignee: ${input.agent.name} (${input.agent.role})`,
|
||||
...(input.managerName ? [`Reports to: ${input.managerName}`] : []),
|
||||
`Provider: ${input.provider.label} ${input.provider.model ?? input.agent.model_id ?? ""}`.trim(),
|
||||
`Execution context: ${input.isolation.executionContextId}`,
|
||||
`Service identity: ${input.isolation.serviceIdentityId}`,
|
||||
];
|
||||
if (input.goal) lines.push("", `Goal: ${input.goal.title}`, `Goal detail: ${input.goal.description}`);
|
||||
if (input.project) lines.push("", `Project: ${input.project.name}`, `Project detail: ${input.project.description}`);
|
||||
if (input.workspace) {
|
||||
lines.push(
|
||||
"",
|
||||
`Workspace: ${input.workspace.name}`,
|
||||
`Local path: ${input.workspace.local_path}`,
|
||||
`Repo: ${input.workspace.repo_url}`,
|
||||
`Ref: ${input.workspace.repo_ref}`,
|
||||
`Mounts: ${input.isolation.workspaceMountJson}`,
|
||||
);
|
||||
}
|
||||
lines.push("", `Secret scope: ${input.isolation.secretScopeJson}`);
|
||||
if (input.skillRuntimeSnapshot) {
|
||||
lines.push(
|
||||
"",
|
||||
"Skill runtime:",
|
||||
input.skillRuntimeSnapshot.compiled_prompt,
|
||||
"",
|
||||
`Tool allowlist: ${input.skillRuntimeSnapshot.tools_manifest}`,
|
||||
`Skill revisions: ${input.skillRuntimeSnapshot.source_revision_ids}`,
|
||||
);
|
||||
}
|
||||
if (input.chatContext) {
|
||||
lines.push(
|
||||
"",
|
||||
"Chat thread you are replying to:",
|
||||
input.chatContext.transcript,
|
||||
"",
|
||||
`Reply in this thread by passing thread_id="${input.chatContext.threadId}" to send_message.`,
|
||||
);
|
||||
}
|
||||
if (input.isManager && !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 use send_message with /handoff @Name and attachment_file_ids to pass the operating plan to the relevant report. For EACH report, send_message to that report's department channel (engineering, product, or general) using an @mention and the work you assigned them — start a new thread per report and note the thread_id it returns. Post the company-wide kickoff to #general. Then summarize.",
|
||||
);
|
||||
} else {
|
||||
const manager = input.managerName ?? "your manager";
|
||||
const managerReference = input.managerName ? `${manager} (${agentMention(input.managerName)})` : manager;
|
||||
lines.push(
|
||||
"",
|
||||
`Complete this task: do the work, write your output as real files with write_deliverable, then send_message to your department channel (engineering, product, or general) reporting your progress and completion to ${managerReference}. Use /handoff with attachment_file_ids when passing files. Reply in the assignment thread if you were given a thread_id. Update the task status when done, and summarize.`,
|
||||
);
|
||||
}
|
||||
return lines.join("\n");
|
||||
}
|
||||
|
||||
function agentMention(name: string): string {
|
||||
return `@${canonicalMentionHandle(name)}`;
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
import { canonicalMentionHandle } from "./mention-handles";
|
||||
|
||||
export interface TaskExecutionTaskRow {
|
||||
id: string;
|
||||
org_id: string;
|
||||
title: string;
|
||||
description: string;
|
||||
status: string;
|
||||
assignee_agent_id: string | null;
|
||||
risk: string;
|
||||
project_id: string | null;
|
||||
goal_id: string | null;
|
||||
workspace_id: string | null;
|
||||
priority: string;
|
||||
requires_filesystem: number;
|
||||
}
|
||||
|
||||
export interface TaskExecutionAgentRow {
|
||||
id: string;
|
||||
name: string;
|
||||
role: string;
|
||||
department: string;
|
||||
title: string;
|
||||
role_level: string;
|
||||
model_provider_id: string | null;
|
||||
model_id: string | null;
|
||||
manager_agent_id: string | null;
|
||||
}
|
||||
|
||||
export interface TaskExecutionProviderRow {
|
||||
id: string;
|
||||
label: string;
|
||||
provider_key: string;
|
||||
model: string | null;
|
||||
}
|
||||
|
||||
export interface ProjectRow {
|
||||
name: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
export interface GoalRow {
|
||||
title: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
export interface WorkspaceRow {
|
||||
id: string;
|
||||
name: string;
|
||||
local_path: string;
|
||||
repo_url: string;
|
||||
repo_ref: string;
|
||||
}
|
||||
|
||||
export interface TaskRunIsolation {
|
||||
executionContextId: string;
|
||||
serviceIdentityId: string;
|
||||
workspaceMountJson: string;
|
||||
secretScopeJson: string;
|
||||
}
|
||||
|
||||
export interface SkillRuntimeSnapshotRow {
|
||||
id: string;
|
||||
compiled_prompt: string;
|
||||
tools_manifest: string;
|
||||
source_revision_ids: string;
|
||||
}
|
||||
|
||||
export interface ChatContextBlock {
|
||||
threadId: string;
|
||||
transcript: string;
|
||||
}
|
||||
|
||||
export function buildAgentInstructions(agent: TaskExecutionAgentRow): string {
|
||||
return [
|
||||
`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, write_deliverable produces real files in the company workspace, create_document produces versioned company documents (PRDs, specs, memos, reports), and send_message posts coordination messages to a channel with optional file attachments.",
|
||||
"Communicate like a real team. Post to the channel that matches the work — 'engineering' for engineering, 'product' for product or design, otherwise 'general'. Use @mentions when addressing a teammate. When you hand work or files to a teammate, use send_message with /handoff @Name and attachment_file_ids containing the file_id returned by write_deliverable. To keep a hand-off as one conversation, reply in the same thread by passing the thread_id a previous send_message returned.",
|
||||
"Take concrete action with the tools and reserve narrative for brief summaries after coordination is posted.",
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
export function buildTaskPrompt(input: {
|
||||
task: TaskExecutionTaskRow;
|
||||
agent: TaskExecutionAgentRow;
|
||||
provider: TaskExecutionProviderRow;
|
||||
project: ProjectRow | null;
|
||||
goal: GoalRow | null;
|
||||
workspace: WorkspaceRow | null;
|
||||
isolation: TaskRunIsolation;
|
||||
skillRuntimeSnapshot: SkillRuntimeSnapshotRow | null;
|
||||
isManager: boolean;
|
||||
hasReports: boolean;
|
||||
managerName: string | null;
|
||||
chatContext: ChatContextBlock | null;
|
||||
}): string {
|
||||
const lines = [
|
||||
"Execute this AgentCompany task.",
|
||||
"",
|
||||
`Task: ${input.task.title}`,
|
||||
`Description: ${input.task.description}`,
|
||||
`Status: ${input.task.status}`,
|
||||
`Priority: ${input.task.priority}`,
|
||||
`Risk: ${input.task.risk}`,
|
||||
`Requires filesystem: ${input.task.requires_filesystem === 1 ? "yes" : "no"}`,
|
||||
`Assignee: ${input.agent.name} (${input.agent.role})`,
|
||||
...(input.managerName ? [`Reports to: ${input.managerName}`] : []),
|
||||
`Provider: ${input.provider.label} ${input.provider.model ?? input.agent.model_id ?? ""}`.trim(),
|
||||
`Execution context: ${input.isolation.executionContextId}`,
|
||||
`Service identity: ${input.isolation.serviceIdentityId}`,
|
||||
];
|
||||
if (input.goal) lines.push("", `Goal: ${input.goal.title}`, `Goal detail: ${input.goal.description}`);
|
||||
if (input.project) lines.push("", `Project: ${input.project.name}`, `Project detail: ${input.project.description}`);
|
||||
if (input.workspace) {
|
||||
lines.push(
|
||||
"",
|
||||
`Workspace: ${input.workspace.name}`,
|
||||
`Local path: ${input.workspace.local_path}`,
|
||||
`Repo: ${input.workspace.repo_url}`,
|
||||
`Ref: ${input.workspace.repo_ref}`,
|
||||
`Mounts: ${input.isolation.workspaceMountJson}`,
|
||||
);
|
||||
}
|
||||
lines.push("", `Secret scope: ${input.isolation.secretScopeJson}`);
|
||||
if (input.skillRuntimeSnapshot) {
|
||||
lines.push(
|
||||
"",
|
||||
"Skill runtime:",
|
||||
input.skillRuntimeSnapshot.compiled_prompt,
|
||||
"",
|
||||
`Tool allowlist: ${input.skillRuntimeSnapshot.tools_manifest}`,
|
||||
`Skill revisions: ${input.skillRuntimeSnapshot.source_revision_ids}`,
|
||||
);
|
||||
}
|
||||
if (input.chatContext) {
|
||||
lines.push(
|
||||
"",
|
||||
"Chat thread you are replying to:",
|
||||
input.chatContext.transcript,
|
||||
"",
|
||||
`Reply in this thread by passing thread_id="${input.chatContext.threadId}" to send_message.`,
|
||||
);
|
||||
}
|
||||
if (input.isManager && !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 use send_message with /handoff @Name and attachment_file_ids to pass the operating plan to the relevant report. For EACH report, send_message to that report's department channel (engineering, product, or general) using an @mention and the work you assigned them — start a new thread per report and note the thread_id it returns. Post the company-wide kickoff to #general. Then summarize.",
|
||||
);
|
||||
} else {
|
||||
const manager = input.managerName ?? "your manager";
|
||||
const managerReference = input.managerName ? `${manager} (${agentMention(input.managerName)})` : manager;
|
||||
lines.push(
|
||||
"",
|
||||
`Complete this task: do the work, write your output as real files with write_deliverable, then send_message to your department channel (engineering, product, or general) reporting your progress and completion to ${managerReference}. Use /handoff with attachment_file_ids when passing files. Reply in the assignment thread if you were given a thread_id. Update the task status when done, and summarize.`,
|
||||
);
|
||||
}
|
||||
return lines.join("\n");
|
||||
}
|
||||
|
||||
function agentMention(name: string): string {
|
||||
return `@${canonicalMentionHandle(name)}`;
|
||||
}
|
||||
Reference in new issue
Block a user