Split task execution: run preparation, provider cooldowns, heartbeat
Pure move to keep task-execution-service under 500 lines. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
95e607bea7
commit
12e2d9dcd5
4 files changed
+381
-329
No files matched your search
@@ -0,0 +1,131 @@
|
||||
import type { DatabaseSync } from "node:sqlite";
|
||||
import { APPROVAL_STATE, RISK_LEVEL } from "../../shared/domain";
|
||||
import { createId } from "../../shared/ids";
|
||||
import type {
|
||||
GoalRow,
|
||||
ProjectRow,
|
||||
TaskExecutionAgentRow,
|
||||
TaskExecutionProviderRow,
|
||||
TaskExecutionTaskRow,
|
||||
TaskRunIsolation,
|
||||
WorkspaceRow,
|
||||
} from "./task-prompts";
|
||||
|
||||
interface TaskApprovalRow {
|
||||
id: string;
|
||||
state: string;
|
||||
}
|
||||
|
||||
export function requireTask(db: DatabaseSync, orgId: string, taskId: string): TaskExecutionTaskRow {
|
||||
const task = db.prepare("select * from tasks where id = ? and org_id = ?").get(taskId, orgId) as TaskExecutionTaskRow | undefined;
|
||||
if (!task) throw new Error("Task was not found.");
|
||||
return task;
|
||||
}
|
||||
|
||||
export function requireAssignedAgent(db: DatabaseSync, orgId: string, task: TaskExecutionTaskRow): TaskExecutionAgentRow {
|
||||
if (!task.assignee_agent_id) throw new Error("Task requires an assigned agent.");
|
||||
const agent = db.prepare(`
|
||||
select id, name, role, department, title, role_level, model_provider_id, model_id, manager_agent_id
|
||||
from agents
|
||||
where id = ? and org_id = ?
|
||||
`).get(task.assignee_agent_id, orgId) as TaskExecutionAgentRow | undefined;
|
||||
if (!agent) throw new Error("Task assignee was not found.");
|
||||
return agent;
|
||||
}
|
||||
|
||||
export function assertExecutionApproval(db: DatabaseSync, orgId: string, task: TaskExecutionTaskRow): void {
|
||||
if (task.risk !== RISK_LEVEL.HIGH && task.requires_filesystem !== 1) return;
|
||||
const approval = db.prepare(`
|
||||
select id, state
|
||||
from approvals
|
||||
where org_id = ?
|
||||
and source_entity_type = 'task'
|
||||
and source_entity_id = ?
|
||||
and kind in ('task_risk', 'task_execution')
|
||||
order by created_at desc
|
||||
limit 1
|
||||
`).get(orgId, task.id) as TaskApprovalRow | undefined;
|
||||
if (!approval || approval.state !== APPROVAL_STATE.APPROVED) {
|
||||
throw new Error("Approved task execution approval is required.");
|
||||
}
|
||||
}
|
||||
|
||||
export function requireAgentProvider(db: DatabaseSync, orgId: string, agent: TaskExecutionAgentRow): TaskExecutionProviderRow {
|
||||
if (!agent.model_provider_id) throw new Error("Task assignee requires a model provider.");
|
||||
const provider = db.prepare(`
|
||||
select id, label, provider_key, model, context_window_tokens
|
||||
from providers
|
||||
where id = ? and org_id = ?
|
||||
`).get(agent.model_provider_id, orgId) as TaskExecutionProviderRow | undefined;
|
||||
if (!provider) throw new Error("Task assignee provider was not found.");
|
||||
return provider;
|
||||
}
|
||||
|
||||
export function loadProject(db: DatabaseSync, projectId: string | null): ProjectRow | null {
|
||||
if (!projectId) return null;
|
||||
return (db.prepare("select name, description from projects where id = ?").get(projectId) as unknown as ProjectRow | undefined) ?? null;
|
||||
}
|
||||
|
||||
export function loadGoal(db: DatabaseSync, goalId: string | null): GoalRow | null {
|
||||
if (!goalId) return null;
|
||||
return (db.prepare("select title, description from goals where id = ?").get(goalId) as unknown as GoalRow | undefined) ?? null;
|
||||
}
|
||||
|
||||
export function loadWorkspace(db: DatabaseSync, workspaceId: string | null): WorkspaceRow | null {
|
||||
if (!workspaceId) return null;
|
||||
return (db.prepare("select id, name, local_path, repo_url, repo_ref from workspaces where id = ?").get(workspaceId) as unknown as WorkspaceRow | undefined) ?? null;
|
||||
}
|
||||
|
||||
export function createIsolationContext(
|
||||
db: DatabaseSync,
|
||||
orgId: string,
|
||||
task: TaskExecutionTaskRow,
|
||||
agent: TaskExecutionAgentRow,
|
||||
provider: TaskExecutionProviderRow,
|
||||
workspace: WorkspaceRow | null,
|
||||
): TaskRunIsolation {
|
||||
if (task.requires_filesystem === 1 && !workspace) throw new Error("Filesystem tasks require a workspace.");
|
||||
const serviceIdentityId = requireServiceIdentity(db, orgId, agent.id);
|
||||
const executionContextId = createId("ctx");
|
||||
const workspaceMounts = workspace ? [{
|
||||
workspaceId: workspace.id,
|
||||
name: workspace.name,
|
||||
localPath: workspace.local_path,
|
||||
repoRef: workspace.repo_ref,
|
||||
mode: task.requires_filesystem === 1 ? "read_write" : "read_only",
|
||||
}] : [];
|
||||
return {
|
||||
executionContextId,
|
||||
serviceIdentityId,
|
||||
workspaceMountJson: JSON.stringify(workspaceMounts),
|
||||
secretScopeJson: JSON.stringify({
|
||||
serviceIdentityId,
|
||||
providerIds: [provider.id],
|
||||
scope: "task_run",
|
||||
credentialAccess: "provider_runtime_only",
|
||||
...chatWakeSource(db, orgId, task),
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
/** A re-run of a chat wake answers the same thread. */
|
||||
function chatWakeSource(db: DatabaseSync, orgId: string, task: TaskExecutionTaskRow): { sourceThreadId: string; sourceMessageId: string } | Record<string, never> {
|
||||
if (task.origin !== "chat_wake") return {};
|
||||
const row = db.prepare(`
|
||||
select json_extract(secret_scope_json, '$.sourceThreadId') as thread_id,
|
||||
json_extract(secret_scope_json, '$.sourceMessageId') as message_id
|
||||
from task_runs
|
||||
where task_id = ? and org_id = ? and json_extract(secret_scope_json, '$.sourceThreadId') is not null
|
||||
order by created_at desc
|
||||
limit 1
|
||||
`).get(task.id, orgId) as { thread_id: string; message_id: string } | undefined;
|
||||
if (!row) throw new Error(`Chat wake task ${task.id} has no recorded source thread.`);
|
||||
return { sourceThreadId: row.thread_id, sourceMessageId: row.message_id };
|
||||
}
|
||||
|
||||
function requireServiceIdentity(db: DatabaseSync, orgId: string, agentId: string): string {
|
||||
const row = db.prepare("select id from service_identities where org_id = ? and agent_id = ?")
|
||||
.get(orgId, agentId) as { id: string } | undefined;
|
||||
if (!row) throw new Error("Agent service identity was not found.");
|
||||
return row.id;
|
||||
}
|
||||
Reference in new issue
Block a user