Q3: record a structured handoff on every delegated task

create_task now pins upstream files by content hash and carries the
source task, source thread, verified facts, open questions and
acceptance criteria into the assignee's prompt.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ZacharyZhang-NYandClaude Opus 5.5 committed 2026-10-08 02:35:12 -04:00
1 parent 9f2adce7b3
commit 6570492daa
22 files changed
+219 -28

No files matched your search

@@ -0,0 +1,6 @@
export const taskHandoffSchema = `
alter table tasks add column handoff_json text;
insert into schema_migrations(version, name, applied_at)
values (59, 'task_handoff', datetime('now'));
`;
+2
View File
@@ -57,6 +57,7 @@ import { taskOriginSchema } from "./migrations/055-task-origin";
import { kimiProvidersSchema } from "./migrations/056-kimi-providers";
import { kimiCodeProviderSchema } from "./migrations/057-kimi-code-provider";
import { integrationKeysSchema } from "./migrations/058-integration-keys";
import { taskHandoffSchema } from "./migrations/059-task-handoff";
interface Migration {
version: number;
@@ -123,6 +124,7 @@ const MIGRATIONS: Migration[] = [
{ version: 56, name: "kimi_providers", sql: kimiProvidersSchema },
{ version: 57, name: "kimi_code_provider", sql: kimiCodeProviderSchema },
{ version: 58, name: "integration_keys", sql: integrationKeysSchema },
{ version: 59, name: "task_handoff", sql: taskHandoffSchema },
];
const SCHEMA_VERSION = MIGRATIONS[MIGRATIONS.length - 1].version;
+73
View File
@@ -0,0 +1,73 @@
import { createHash } from "node:crypto";
import { existsSync, readFileSync } from "node:fs";
import type { DatabaseSync } from "node:sqlite";
import { optionalStringArray } from "./agent-tool-args";
import type { JsonRecord } from "./provider-http";
export interface TaskHandoff {
sourceTaskId: string | null;
sourceThreadId: string | null;
inputs: Array<{ fileId: string; name: string; sha256: string }>;
facts: string[];
openQuestions: string[];
acceptanceCriteria: string[];
}
export const HANDOFF_TOOL_PROPERTIES = {
acceptance_criteria: { type: "array", items: { type: "string" }, description: "Checks that prove the task is done." },
input_file_ids: { type: "array", items: { type: "string" }, description: "file_ids of upstream deliverables the assignee builds on; their current version is pinned." },
facts: { type: "array", items: { type: "string" }, description: "Facts already verified, so the assignee does not redo them." },
open_questions: { type: "array", items: { type: "string" }, description: "Questions still unresolved." },
} as const;
export function buildTaskHandoff(
db: DatabaseSync,
orgId: string,
source: { taskId: string | null; threadId: string | null },
args: JsonRecord,
): TaskHandoff {
const inputs = optionalStringArray(args.input_file_ids, "input_file_ids").map((fileId) => {
const file = db.prepare("select id, name from file_objects where id = ? and org_id = ?")
.get(fileId, orgId) as { id: string; name: string } | undefined;
if (!file) throw new Error(`No file with file_id "${fileId}" exists in this company.`);
const sha256 = currentSha256(db, orgId, file.id);
if (!sha256) throw new Error(`File ${file.name} (file_id "${fileId}") is missing on disk.`);
return { fileId: file.id, name: file.name, sha256 };
});
return {
sourceTaskId: source.taskId,
sourceThreadId: source.threadId,
inputs,
facts: optionalStringArray(args.facts, "facts"),
openQuestions: optionalStringArray(args.open_questions, "open_questions"),
acceptanceCriteria: optionalStringArray(args.acceptance_criteria, "acceptance_criteria"),
};
}
export function renderTaskHandoff(db: DatabaseSync, orgId: string, handoffJson: string | null): string[] {
if (!handoffJson) return [];
const handoff = JSON.parse(handoffJson) as TaskHandoff;
const lines = ["", "Handoff:"];
if (handoff.sourceTaskId) {
const source = db.prepare("select title from tasks where id = ? and org_id = ?")
.get(handoff.sourceTaskId, orgId) as { title: string } | undefined;
lines.push(`- Delegated from task "${source?.title ?? "deleted task"}" (task_id=${handoff.sourceTaskId}).`);
}
if (handoff.sourceThreadId) lines.push(`- Source conversation: thread_id="${handoff.sourceThreadId}".`);
for (const input of handoff.inputs) {
const current = currentSha256(db, orgId, input.fileId);
const state = !current ? "deleted since the handoff" : current === input.sha256 ? "unchanged since the handoff" : "CHANGED since the handoff — re-check it";
lines.push(`- Input ${input.name} (file_id=${input.fileId}, sha256=${input.sha256.slice(0, 12)}): ${state}.`);
}
for (const fact of handoff.facts) lines.push(`- Verified: ${fact}`);
for (const question of handoff.openQuestions) lines.push(`- Open question: ${question}`);
for (const check of handoff.acceptanceCriteria) lines.push(`- Done when: ${check}`);
return lines;
}
function currentSha256(db: DatabaseSync, orgId: string, fileId: string): string | null {
const row = db.prepare("select storage_path from file_objects where id = ? and org_id = ?")
.get(fileId, orgId) as { storage_path: string } | undefined;
if (!row || !existsSync(row.storage_path)) return null;
return createHash("sha256").update(readFileSync(row.storage_path)).digest("hex");
}
+8 -1
View File
@@ -2,6 +2,7 @@ import type { DatabaseSync } from "node:sqlite";
import { RISK_LEVEL, TASK_PRIORITY, TASK_STATUS, type RiskLevel } from "../../shared/domain";
import { ORGANIZATION_DEPARTMENTS } from "./org-governance-service";
import type { OrganizationService } from "./organization-service";
import { buildTaskHandoff, HANDOFF_TOOL_PROPERTIES } from "./agent-task-handoff";
import type { ChatService } from "./chat-service";
import { canonicalMentionHandle } from "../../shared/mention-handles";
import { readMessagesTool } from "./agent-read-messages";
@@ -23,6 +24,9 @@ export interface AgentToolContext {
agent: { id: string; modelProviderId: string | null; modelId: string | null };
taskProjectId: string | null;
taskGoalId: string | null;
/** The task this run executes. */
taskId: string | null;
sourceThreadId: string | null;
/** writeAccess mirrors the task's approved filesystem grant (read_write mount) and gates run_command. */
workspace: { id: string; localPath: string; writeAccess: boolean } | null;
}
@@ -64,7 +68,7 @@ export const AGENT_TOOLS: ToolDefinition[] = [
},
{
name: "create_task",
description: "Create a task for the team and optionally assign it to one of your reports by name.",
description: "Create a task for the team and optionally assign it to one of your reports by name. Hand over what you know: acceptance criteria, upstream file_ids, verified facts, and open questions.",
parameters: {
type: "object",
additionalProperties: false,
@@ -77,6 +81,7 @@ export const AGENT_TOOLS: ToolDefinition[] = [
type: "boolean",
description: "Set true for engineering work that must change code or run commands (build, test) in the workspace. The assignee then gets run_command; the operator approves such tasks before they execute.",
},
...HANDOFF_TOOL_PROPERTIES,
},
required: ["title", "description"],
},
@@ -306,6 +311,7 @@ function createTask(deps: AgentToolDeps, ctx: AgentToolContext, args: JsonRecord
const description = requireString(args.description, "description");
const assigneeName = optionalString(args.assignee_agent_name);
const assignee = assigneeName ? requireRunnableAgentByName(deps.db, ctx.orgId, assigneeName) : null;
const handoff = buildTaskHandoff(deps.db, ctx.orgId, { taskId: ctx.taskId, threadId: ctx.sourceThreadId }, args);
const create = () => {
const taskId = deps.organization.createTask({
orgId: ctx.orgId,
@@ -320,6 +326,7 @@ function createTask(deps: AgentToolDeps, ctx: AgentToolContext, args: JsonRecord
priority: TASK_PRIORITY.NORMAL,
requiresFilesystem: args.requires_filesystem === true,
});
deps.db.prepare("update tasks set handoff_json = ? where id = ?").run(JSON.stringify(handoff), taskId);
const assignmentThread = options.announceTaskAssignments !== false && assignee && assignee.id !== ctx.agent.id
? postAssignmentMessage(deps, ctx, { taskId, title, description, assignee })
: null;
+5 -1
View File
@@ -24,6 +24,7 @@ import {
type TaskRunIsolation,
type WorkspaceRow,
} from "./task-prompts";
import { renderTaskHandoff } from "./agent-task-handoff";
import { loadComposioTools } from "./composio/composio-tool-bridge";
import { connectorDisplayName } from "./composio/connector-brands";
import { ConnectorGrantsService } from "./composio/connector-grants";
@@ -133,14 +134,16 @@ export class TaskExecutionService {
let skillRuntimeSnapshot: SkillRuntimeSnapshotRow | null = null;
try {
skillRuntimeSnapshot = this.resolveSkillRuntimeSnapshot(orgId, agent.id, runId);
const chatContext = this.loadChatContext(orgId, isolation);
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,
taskId: task.id,
sourceThreadId: chatContext?.threadId ?? null,
workspace: workspace ? { id: workspace.id, localPath: workspace.local_path, writeAccess: task.requires_filesystem === 1 } : null,
};
const chatContext = this.loadChatContext(orgId, isolation);
// The CEO is the c_suite role — the same predicate the grant system
// enforces — not "has no manager", which any board hire can match.
const isCeo = agent.role_level === "c_suite";
@@ -177,6 +180,7 @@ export class TaskExecutionService {
hasReports,
managerName: this.agentName(orgId, agent.manager_agent_id),
chatContext,
handoffLines: renderTaskHandoff(this.db, orgId, task.handoff_json),
priorCompletedRuns: this.completedRunCount(task.id, runId),
}),
toolContext,
+3
View File
@@ -13,6 +13,7 @@ export interface TaskExecutionTaskRow {
workspace_id: string | null;
priority: string;
requires_filesystem: number;
handoff_json: string | null;
}
export interface TaskExecutionAgentRow {
@@ -163,6 +164,7 @@ export function buildTaskPrompt(input: {
hasReports: boolean;
managerName: string | null;
chatContext: ChatContextBlock | null;
handoffLines: string[];
/** Completed runs this task already spent without reaching done. */
priorCompletedRuns: number;
}): string {
@@ -194,6 +196,7 @@ export function buildTaskPrompt(input: {
`Mounts: ${input.isolation.workspaceMountJson}`,
);
}
lines.push(...input.handoffLines);
lines.push("", `Secret scope: ${input.isolation.secretScopeJson}`);
if (input.skillRuntimeSnapshot) {
lines.push(
+3 -3
View File
@@ -82,7 +82,7 @@ describe("interactive execution lane", () => {
// Older autonomous work: the CEO DMs Dana, waking her.
dispatchAgentTool({ db: connection.db, organization, chat }, {
orgId, agent: { id: ceoId, modelProviderId: null, modelId: null },
taskProjectId: null, taskGoalId: null, workspace: null,
taskProjectId: null, taskGoalId: null, taskId: null, sourceThreadId: null, workspace: null,
}, call("send_message", { to: "Dana", body: "Grind through the migration backlog." }));
// Newer interactive work: the founder DMs the CEO.
@@ -169,7 +169,7 @@ describe("interactive execution lane", () => {
// Autonomous work that will trip the transient failure and park the provider.
dispatchAgentTool({ db: connection.db, organization, chat }, {
orgId, agent: { id: ceoId, modelProviderId: null, modelId: null },
taskProjectId: null, taskGoalId: null, workspace: null,
taskProjectId: null, taskGoalId: null, taskId: null, sourceThreadId: null, workspace: null,
}, call("send_message", { to: "Dana", body: "Grind through the migration backlog." }));
const stopWorker = startTaskQueueWorker({
@@ -232,7 +232,7 @@ describe("interactive execution lane", () => {
const taskExecution = new TaskExecutionService(connection.db, providers);
const wakeDana = () => dispatchAgentTool({ db: connection.db, organization, chat }, {
orgId, agent: { id: ceoId, modelProviderId: null, modelId: null },
taskProjectId: null, taskGoalId: null, workspace: null,
taskProjectId: null, taskGoalId: null, taskId: null, sourceThreadId: null, workspace: null,
}, call("send_message", { to: "Dana", body: "Grind through the migration backlog." }));
wakeDana();
@@ -34,7 +34,7 @@ describe("thread participant wake", () => {
const deps = { db: connection.db, organization, chat };
const ctx: AgentToolContext = {
orgId, agent: { id: ceoId, modelProviderId: providerId, modelId: "gpt-5.4" },
taskProjectId: null, taskGoalId: null, workspace: null,
taskProjectId: null, taskGoalId: null, taskId: null, sourceThreadId: null, workspace: null,
};
const firstResult = dispatchAgentTool(deps, ctx, call("send_message", { channel: "general", body: "The plan is ready for review." }));
+1 -1
View File
@@ -57,7 +57,7 @@ function harness(options: { enableProvider: boolean }): Harness {
orgId,
agent: { id: ceoId, modelProviderId: null, modelId: null },
taskProjectId: null,
taskGoalId: null,
taskGoalId: null, taskId: null, sourceThreadId: null,
workspace: null,
},
};
+2 -2
View File
@@ -70,7 +70,7 @@ function argsFor(h: Harness, agentId: string, auditor: Auditor, toolCall: ToolCa
orgId: h.orgId,
agent: { id: agentId, modelProviderId: h.providerId, modelId: "gpt-5.4" },
taskProjectId: null,
taskGoalId: h.goalId,
taskGoalId: h.goalId, taskId: null, sourceThreadId: null,
workspace: { id: "wsp_audit", localPath: workspaceRoot, writeAccess: true },
};
return { deps: h.deps, ctx, providers: { runToolTurn: auditor }, providerId: h.providerId, workspaceId: null, call: toolCall };
@@ -158,7 +158,7 @@ function toolCtx(h: Harness, agentId: string): AgentToolContext {
orgId: h.orgId,
agent: { id: agentId, modelProviderId: h.providerId, modelId: "gpt-5.4" },
taskProjectId: null,
taskGoalId: null,
taskGoalId: null, taskId: null, sourceThreadId: null,
workspace: null,
};
}
+2 -2
View File
@@ -29,7 +29,7 @@ describe("agent messaging", () => {
const deps = { db: connection.db, organization, chat: new ChatService(connection.db) };
const ctx: AgentToolContext = {
orgId, agent: { id: ceoId, modelProviderId: null, modelId: null },
taskProjectId: null, taskGoalId: null, workspace: null,
taskProjectId: null, taskGoalId: null, taskId: null, sourceThreadId: null, workspace: null,
};
const first = dispatchAgentTool(deps, ctx, call("send_message", { channel: "engineering", body: "Assigning the booking API to Dana." }));
@@ -85,7 +85,7 @@ describe("agent messaging", () => {
const deps = { db: connection.db, organization, chat: new ChatService(connection.db) };
const ctx: AgentToolContext = {
orgId, agent: { id: ceoId, modelProviderId: null, modelId: null },
taskProjectId: null, taskGoalId: null, workspace: null,
taskProjectId: null, taskGoalId: null, taskId: null, sourceThreadId: null, workspace: null,
};
const result = dispatchAgentTool(deps, ctx, call("send_message", { channel: "engineering", body: "@Dana can you take this?" }));
+4 -4
View File
@@ -66,7 +66,7 @@ describe("agent run loop (real tool dispatch)", () => {
orgId,
agent: { id: ceoId, modelProviderId: providerId, modelId: "gpt-5.4" },
taskProjectId: null,
taskGoalId: null,
taskGoalId: null, taskId: null, sourceThreadId: null,
workspace: { id: workspaceId, localPath: workspaceRoot, writeAccess: true },
};
@@ -139,7 +139,7 @@ describe("agent run loop (real tool dispatch)", () => {
});
const ctx: AgentToolContext = {
orgId, agent: { id: ceoId, modelProviderId: providerId, modelId: "gpt-5.4" },
taskProjectId: null, taskGoalId: null, workspace: { id: "wsp", localPath: workspaceRoot, writeAccess: true },
taskProjectId: null, taskGoalId: null, taskId: null, sourceThreadId: null, workspace: { id: "wsp", localPath: workspaceRoot, writeAccess: true },
};
const script: ScriptedTurn[] = [
{ text: "escape", toolCalls: [call("write_deliverable", { path: "../escape.txt", content: "x" })] },
@@ -174,7 +174,7 @@ describe("agent run loop (real tool dispatch)", () => {
});
const ctx: AgentToolContext = {
orgId, agent: { id: ceoId, modelProviderId: providerId, modelId: "gpt-5.4" },
taskProjectId: null, taskGoalId: null, workspace: null,
taskProjectId: null, taskGoalId: null, taskId: null, sourceThreadId: null, workspace: null,
};
const script: ScriptedTurn[] = [
{ text: "staff", toolCalls: [call("hire_agent", { name: "Alex Rivera", role: "Engineer", department: "Engineering" })] },
@@ -226,7 +226,7 @@ describe("agent run loop (real tool dispatch)", () => {
});
const ctx: AgentToolContext = {
orgId, agent: { id: ceoId, modelProviderId: providerId, modelId: "gpt-5.4" },
taskProjectId: null, taskGoalId: null, workspace: null,
taskProjectId: null, taskGoalId: null, taskId: null, sourceThreadId: null, workspace: null,
};
await expect(runAgentLoop(
{ providers: fakeProvider([{ text: "", toolCalls: [] }], connection.db), tools: { db: connection.db, organization, chat: new ChatService(connection.db) } },
+96
View File
@@ -0,0 +1,96 @@
import { createHash, randomUUID } from "node:crypto";
import { rmSync, writeFileSync } from "node:fs";
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 { ChatService } from "../../src/main/services/chat-service";
import { dispatchAgentTool, type AgentToolContext } from "../../src/main/services/agent-tools";
import { renderTaskHandoff } from "../../src/main/services/agent-task-handoff";
import { RISK_LEVEL, TASK_PRIORITY, TASK_STATUS } from "../../src/shared/domain";
import type { ToolCall } from "../../src/main/services/provider-tool-turn";
function sha256(text: string): string {
return createHash("sha256").update(text).digest("hex");
}
function call(name: string, args: Record<string, unknown>): ToolCall {
return { id: `call_${randomUUID()}`, name, arguments: args };
}
function harness() {
const connection = new DatabaseConnection(join(tmpdir(), `ac-handoff-${randomUUID()}.db`));
const organization = new OrganizationService(connection.db);
let orgId = "";
let ceoId = "";
let sourceTaskId = "";
connection.transaction(() => {
orgId = organization.createOrganization({ organizationName: "HandoffCo", employeeName: "Yang", mission: "m", goal: "g" });
const 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, roleLevel: "c_suite",
});
sourceTaskId = organization.createTask({
orgId, title: "Plan the launch", description: "d", status: TASK_STATUS.IN_PROGRESS, assigneeAgentId: ceoId,
risk: RISK_LEVEL.LOW, projectId: null, goalId: null, workspaceId: null, priority: TASK_PRIORITY.NORMAL, requiresFilesystem: false,
});
});
const specPath = join(tmpdir(), `ac-handoff-spec-${randomUUID()}.md`);
writeFileSync(specPath, "v1");
connection.db.prepare(`
insert into file_objects(id, org_id, name, kind, size_bytes, sha256, storage_path, owner_type, owner_id, created_at)
values ('fil_spec', ?, 'spec.md', 'document', 2, ?, ?, 'agent', ?, ?)
`).run(orgId, sha256("v1"), specPath, ceoId, new Date().toISOString());
const deps = { db: connection.db, organization, chat: new ChatService(connection.db) };
const ctx: AgentToolContext = {
orgId, agent: { id: ceoId, modelProviderId: null, modelId: null },
taskProjectId: null, taskGoalId: null, taskId: sourceTaskId, sourceThreadId: "thr_src", workspace: null,
};
return { connection, deps, ctx, orgId, sourceTaskId, specPath };
}
describe("structured task handoff", () => {
test("create_task pins inputs and carries facts, questions, and acceptance criteria", () => {
const h = harness();
const result = dispatchAgentTool(h.deps, h.ctx, call("create_task", {
title: "Build the API", description: "Ship it.",
input_file_ids: ["fil_spec"], facts: ["Postgres is the store"], open_questions: ["Rate limits?"],
acceptance_criteria: ["All endpoint tests pass"],
}));
expect(result).toContain("Created task");
const taskId = /task_id=([^)\s]+)\)/.exec(result)![1];
const row = h.connection.db.prepare("select handoff_json from tasks where id = ?").get(taskId) as { handoff_json: string };
expect(JSON.parse(row.handoff_json)).toEqual({
sourceTaskId: h.sourceTaskId,
sourceThreadId: "thr_src",
inputs: [{ fileId: "fil_spec", name: "spec.md", sha256: sha256("v1") }],
facts: ["Postgres is the store"],
openQuestions: ["Rate limits?"],
acceptanceCriteria: ["All endpoint tests pass"],
});
const before = renderTaskHandoff(h.connection.db, h.orgId, row.handoff_json).join("\n");
expect(before).toContain('Delegated from task "Plan the launch"');
expect(before).toContain('thread_id="thr_src"');
expect(before).toContain(`spec.md (file_id=fil_spec, sha256=${sha256("v1").slice(0, 12)}): unchanged since the handoff`);
expect(before).toContain("Verified: Postgres is the store");
expect(before).toContain("Open question: Rate limits?");
expect(before).toContain("Done when: All endpoint tests pass");
writeFileSync(h.specPath, "v2 edited outside the file registry");
expect(renderTaskHandoff(h.connection.db, h.orgId, row.handoff_json).join("\n")).toContain("CHANGED since the handoff");
rmSync(h.specPath);
expect(renderTaskHandoff(h.connection.db, h.orgId, row.handoff_json).join("\n")).toContain("deleted since the handoff");
h.connection.close();
});
test("an unknown input file id fails the call and creates nothing", () => {
const h = harness();
expect(dispatchAgentTool(h.deps, h.ctx, call("create_task", { title: "X", description: "d", input_file_ids: ["fil_missing"] })))
.toBe('ERROR: No file with file_id "fil_missing" exists in this company.');
expect(h.connection.db.prepare("select count(*) as n from tasks where title = 'X'").get()).toEqual({ n: 0 });
h.connection.close();
});
});
+2 -2
View File
@@ -30,7 +30,7 @@ function ctx(writeAccess: boolean): AgentToolContext {
orgId: "org_test",
agent: { id: "agt_test", modelProviderId: null, modelId: null },
taskProjectId: null,
taskGoalId: null,
taskGoalId: null, taskId: null, sourceThreadId: null,
workspace: { id: "wsp_test", localPath: workspaceRoot, writeAccess },
};
}
@@ -91,7 +91,7 @@ describe("agent workspace tools", () => {
const deps = { db: connection.db, organization, chat: new ChatService(connection.db) };
const toolCtx: AgentToolContext = {
orgId, agent: { id: ceoId, modelProviderId: providerId, modelId: "gpt-5.4" },
taskProjectId: null, taskGoalId: null, workspace: null,
taskProjectId: null, taskGoalId: null, taskId: null, sourceThreadId: null, workspace: null,
};
const result = dispatchAgentTool(deps, toolCtx, call("create_task", {
+1 -1
View File
@@ -162,7 +162,7 @@ describe("built-in skills", () => {
orgId: h.orgId,
agent: { id: ceoId, modelProviderId: providerId, modelId: "gpt-5.4" },
taskProjectId: null,
taskGoalId: null,
taskGoalId: null, taskId: null, sourceThreadId: null,
workspace: null,
};
const deps = { db: h.connection.db, organization: h.organization, chat: new ChatService(h.connection.db) };
+1 -1
View File
@@ -50,7 +50,7 @@ describe("queue claim priority", () => {
dispatchAgentTool(deps, {
orgId: h.orgId,
agent: { id: h.ceoId, modelProviderId: null, modelId: null },
taskProjectId: null, taskGoalId: null, workspace: null,
taskProjectId: null, taskGoalId: null, taskId: null, sourceThreadId: null, workspace: null,
}, { id: `call_${randomUUID()}`, name: "send_message", arguments: { to: "Dana", body: "Status?" } });
const danaRun = h.connection.db.prepare(
"select id from task_runs where org_id = ? and agent_id = ? and trigger = 'chat_mention'",
+1 -1
View File
@@ -44,7 +44,7 @@ function harness(): Harness {
orgId,
agent: { id: agentId, modelProviderId: null, modelId: null },
taskProjectId: null,
taskGoalId: null,
taskGoalId: null, taskId: null, sourceThreadId: null,
workspace: null,
});
return {
+1 -1
View File
@@ -24,7 +24,7 @@ describe("domain services", () => {
const connection = createConnection();
const rows = connection.db.prepare("select version from schema_migrations order by version asc").all() as Array<{ version: number }>;
expect(rows.map((row) => row.version)).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58]);
expect(rows.map((row) => row.version)).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59]);
connection.close();
});
+1 -1
View File
@@ -53,7 +53,7 @@ function harness(mode: "ai_ceo" | "ai_cofounder"): Harness {
orgId,
agent: { id: ceoId, modelProviderId: null, modelId: null },
taskProjectId: null,
taskGoalId: null,
taskGoalId: null, taskId: null, sourceThreadId: null,
workspace: { id: workspaceId, localPath: workspaceRoot, writeAccess: true },
},
deps: { db: connection.db, organization, chat: new ChatService(connection.db) },
+4 -4
View File
@@ -45,7 +45,7 @@ describe("mention resolution", () => {
const deps = { db: connection.db, organization, chat: new ChatService(connection.db) };
const ctx: AgentToolContext = {
orgId, agent: { id: ceoId, modelProviderId: providerId, modelId: "gpt-5.4" },
taskProjectId: null, taskGoalId: null, workspace: null,
taskProjectId: null, taskGoalId: null, taskId: null, sourceThreadId: null, workspace: null,
};
const result = dispatchAgentTool(deps, ctx, call("send_message", {
@@ -79,7 +79,7 @@ describe("mention resolution", () => {
const deps = { db: connection.db, organization, chat: new ChatService(connection.db) };
const ctx: AgentToolContext = {
orgId, agent: { id: ceoId, modelProviderId: null, modelId: null },
taskProjectId: null, taskGoalId: null, workspace: null,
taskProjectId: null, taskGoalId: null, taskId: null, sourceThreadId: null, workspace: null,
};
dispatchAgentTool(deps, ctx, call("send_message", { channel: "general", body: "Hello team!" }));
@@ -113,14 +113,14 @@ describe("mention resolution", () => {
const deps = { db: connection.db, organization, chat: new ChatService(connection.db) };
const ceoCtx: AgentToolContext = {
orgId, agent: { id: ceoId, modelProviderId: null, modelId: null },
taskProjectId: null, taskGoalId: null, workspace: null,
taskProjectId: null, taskGoalId: null, taskId: null, sourceThreadId: null, workspace: null,
};
dispatchAgentTool(deps, ceoCtx, call("send_message", { channel: "engineering", body: "@dana check the deployment" }));
const danaCtx: AgentToolContext = {
orgId, agent: { id: engineerId, modelProviderId: null, modelId: null },
taskProjectId: null, taskGoalId: null, workspace: null,
taskProjectId: null, taskGoalId: null, taskId: null, sourceThreadId: null, workspace: null,
};
const inbox = dispatchAgentTool(deps, danaCtx, call("read_messages", {}));
expect(inbox).toContain("check the deployment");
+1 -1
View File
@@ -31,7 +31,7 @@ describe("org structure", () => {
orgId,
agent: { id: agentId, modelProviderId: providerId, modelId: "gpt-5.4" },
taskProjectId: null,
taskGoalId: null,
taskGoalId: null, taskId: null, sourceThreadId: null,
workspace: null,
});
+1 -1
View File
@@ -44,7 +44,7 @@ function harness(): Harness {
orgId,
agent: { id: agentId, modelProviderId: null, modelId: null },
taskProjectId: null,
taskGoalId: null,
taskGoalId: null, taskId: null, sourceThreadId: null,
workspace: null,
});
return {