This commit is contained in:
ZacharyZhang-NY committed 2026-05-15 19:40:20 -04:00
1 parent 2ab001b18a
commit 810e2d5967
28 files changed
+1015 -85

No files matched your search

+49 -4
View File
@@ -121,6 +121,34 @@ export class RuntimeBridge {
this.getOrCreateStore(context.taskSessionId);
}
private evictActiveRun(runId: string): ActiveRunContext | null {
const ctx = this.activeRuns.get(runId) ?? null;
this.activeRuns.delete(runId);
this.inboundCorrelations.deleteRun(runId);
if (ctx) this.sessionStores.delete(ctx.taskSessionId);
return ctx;
}
private assertRunContextCurrent(
ctx: ActiveRunContext,
operation: string,
): { ok: true } | { ok: false; error: string } {
const runState = this.deps.db.core.runs.getRunAuthState(ctx.runId);
if (!runState) {
this.evictActiveRun(ctx.runId);
const error = `${operation}: stale run ${ctx.runId} no longer exists.`;
this.deps.logger.warn(`[runtime-bridge] ${error}`);
return { ok: false, error };
}
if (runState.companyId !== ctx.companyId || runState.agentId !== ctx.agentId) {
this.evictActiveRun(ctx.runId);
const error = `${operation}: stale run ${ctx.runId} context does not match the persisted run.`;
this.deps.logger.warn(`[runtime-bridge] ${error}`);
return { ok: false, error };
}
return { ok: true };
}
/**
* Evicts the per-run `activeRuns` context, the per-session store cache,
* and the inbound-correlation log. Callers must invoke after a
@@ -136,12 +164,14 @@ export class RuntimeBridge {
const ctx = this.activeRuns.get(runId);
let result: RunResourceReleaseResult = { ok: true };
try {
if (ctx) result = this.reconcileArtifactsForRun(ctx);
if (ctx) {
const current = this.assertRunContextCurrent(ctx, "releaseRunResources");
if (!current.ok) return { ok: true };
result = this.reconcileArtifactsForRun(ctx);
}
return result;
} finally {
this.activeRuns.delete(runId);
this.inboundCorrelations.deleteRun(runId);
if (ctx) this.sessionStores.delete(ctx.taskSessionId);
this.evictActiveRun(runId);
}
}
@@ -256,6 +286,10 @@ export class RuntimeBridge {
if (envelope.event.type === "message_end") {
const ctx = this.activeRuns.get(envelope.runId);
if (ctx) {
const current = this.assertRunContextCurrent(ctx, "agent-event");
if (!current.ok) return;
}
const messageRaw = envelope.event.message as
| {
role?: string;
@@ -330,6 +364,8 @@ export class RuntimeBridge {
if (!ctx) {
return { ok: false, error: `invokeInternalTool: no active run ${runId}` };
}
const current = this.assertRunContextCurrent(ctx, "invokeInternalTool");
if (!current.ok) return { ok: false, error: current.error };
const callbacks = this.internalToolCallbacks;
if (!callbacks) {
return {
@@ -374,6 +410,13 @@ export class RuntimeBridge {
workspacePath: string | null;
workspaceMode: "exclusive" | "shared" | "none";
};
const runState = this.deps.db.core.runs.getRunAuthState(runId);
if (!runState) {
return { ok: false, error: `prepareRun: stale run ${runId} no longer exists.` };
}
if (runState.companyId !== payload.companyId || runState.agentId !== payload.agentId) {
return { ok: false, error: `prepareRun: run ${runId} context does not match the persisted run.` };
}
const baseline = captureBaselineIfRequired(payload.workspacePath, this.deps.logger);
this.registerActiveRun({
runId,
@@ -454,6 +497,8 @@ export class RuntimeBridge {
if (!ctx) {
return { ok: false, error: `beforeToolCall: no active run ${runId}` };
}
const current = this.assertRunContextCurrent(ctx, "beforeToolCall");
if (!current.ok) return { ok: false, error: current.error };
const result = handleBeforeToolCall(this.deps, ctx, payload);
return { ok: true, result };
}
+32 -3
View File
@@ -5,6 +5,7 @@ import { IMPORTANT_DOC_TYPES } from "@shared/document-categories";
import { canAssignTask, canUpdateTask } from "../agent-permissions";
import { inferTaskType } from "../connector-matching";
import { validateTaskCreation } from "../task-quality-gates";
import { resolveTaskWorkspaceBinding } from "../task-workspace-binding";
import { requireParam, type AgentApiServer } from "./server-core";
const taskUpdateSchema = z.object({
@@ -146,6 +147,21 @@ export function registerTaskRoutes(server: AgentApiServer) {
reviewRequest = { reviewerAgentName: actorAgent.name, documents: docs };
}
const nextRequiresFilesystem = update.requiresFilesystem ?? task.requiresFilesystem;
const workspaceBinding = resolveTaskWorkspaceBinding(db, {
companyId: task.companyId,
requiresFilesystem: nextRequiresFilesystem,
explicitWorkspaceId: task.workspaceId,
projectId: task.projectId,
assigneeAgentId: newAssignee,
creatorAgentId: token.agentId,
currentRunId: token.runId ?? null,
});
if (!workspaceBinding.ok) {
server.json(res, 422, { error: workspaceBinding.error });
return;
}
const updateResult = db.core.tasks.updateTaskWithReviewSideEffects({
taskId: task.id,
companyId: task.companyId,
@@ -160,8 +176,8 @@ export function registerTaskRoutes(server: AgentApiServer) {
goalId: task.goalId,
parentId: task.parentId,
assigneeAgentId: newAssignee,
workspaceId: task.workspaceId,
requiresFilesystem: update.requiresFilesystem ?? task.requiresFilesystem,
workspaceId: workspaceBinding.workspaceId,
requiresFilesystem: nextRequiresFilesystem,
},
comment: update.comment,
reviewRequest,
@@ -297,6 +313,19 @@ export function registerTaskRoutes(server: AgentApiServer) {
let id: string;
try {
const workspaceBinding = resolveTaskWorkspaceBinding(db, {
companyId,
requiresFilesystem: input.requiresFilesystem,
explicitWorkspaceId: input.workspaceId ?? null,
projectId: input.projectId ?? null,
assigneeAgentId: input.assigneeAgentId ?? null,
creatorAgentId: token.agentId,
currentRunId: token.runId ?? null,
});
if (!workspaceBinding.ok) {
server.json(res, 422, { error: workspaceBinding.error });
return;
}
id = db.core.tasks.saveTask({
companyId,
title: input.title,
@@ -307,7 +336,7 @@ export function registerTaskRoutes(server: AgentApiServer) {
goalId: input.goalId ?? null,
priority: input.priority,
status: input.status,
workspaceId: input.workspaceId ?? null,
workspaceId: workspaceBinding.workspaceId,
requiresFilesystem: input.requiresFilesystem,
});
} catch (error) {
@@ -1,6 +1,7 @@
import type { AutomationAction } from "@shared/types";
import { dispatchAgentMessage } from "../messaging/dispatch";
import { validateTaskCreation } from "../task-quality-gates";
import { resolveTaskWorkspaceBinding } from "../task-workspace-binding";
import {
createAutomationActionHandler,
type AutomationActionHandler,
@@ -106,12 +107,26 @@ function handleCreateTask({ deps, callbacks, ctx, config }: TypedAutomationActio
return;
}
const workspaceBinding = resolveTaskWorkspaceBinding(db, {
companyId: ctx.companyId,
requiresFilesystem: config.requiresFilesystem,
explicitWorkspaceId: config.workspaceId ?? null,
projectId: ctx.projectId ?? config.projectId ?? null,
assigneeAgentId,
creatorAgentId: ctx.agentId ?? null,
});
if (!workspaceBinding.ok) {
logger.warn(`[automation] Skipping create_task - ${workspaceBinding.error}`);
return;
}
const taskId = db.core.tasks.saveTask({
companyId: ctx.companyId,
projectId: ctx.projectId ?? config.projectId ?? null,
title,
description,
assigneeAgentId,
workspaceId: workspaceBinding.workspaceId,
priority,
status: assigneeAgentId ? "todo" : "backlog",
requiresFilesystem: config.requiresFilesystem,
@@ -24,6 +24,7 @@ export const createTaskConfigSchema = z.object({
priority: taskPrioritySchema.optional(),
assigneeAgentId: z.string().nullable().optional(),
projectId: z.string().optional(),
workspaceId: z.string().nullable().optional(),
requiresFilesystem: z.boolean().default(false),
});
@@ -0,0 +1,130 @@
import type { DatabaseSync } from "node:sqlite";
import { nowIso, tableExists } from "./types";
export const FILESYSTEM_TASK_WORKSPACES_SCHEMA_VERSION = 60;
export const FILESYSTEM_TASK_WORKSPACES_SCHEMA_NAME = "filesystem_task_workspaces";
const ACTIVE_TASK_STATUSES = "'backlog', 'todo', 'in_progress', 'blocked'";
export function migrateFilesystemTaskWorkspacesV60(db: DatabaseSync): void {
backfillProjectWorkspaces(db);
backfillAssigneeWorkspaces(db);
backfillSingleCompanyWorkspaces(db);
db
.prepare("insert or replace into schema_migrations (version, name, applied_at) values (?, ?, ?)")
.run(FILESYSTEM_TASK_WORKSPACES_SCHEMA_VERSION, FILESYSTEM_TASK_WORKSPACES_SCHEMA_NAME, nowIso());
assertFilesystemTaskWorkspacesV60(db);
}
export function assertFilesystemTaskWorkspacesV60(db: DatabaseSync): void {
if (!tableExists(db, "tasks") || !tableExists(db, "workspaces")) {
throw new Error("Schema v60 drift: tasks/workspaces tables are missing.");
}
const unresolved = db
.prepare(
`select count(*) as count
from tasks t
where t.requires_filesystem = 1
and t.workspace_id is null
and t.status in (${ACTIVE_TASK_STATUSES})
and (
select count(*)
from workspaces w
where w.company_id = t.company_id
) = 1`,
)
.get() as { count: number } | undefined;
if ((unresolved?.count ?? 0) > 0) {
throw new Error("Schema v60 drift: single-workspace companies still have unresolved filesystem tasks.");
}
}
function backfillProjectWorkspaces(db: DatabaseSync): void {
if (!hasRequiredTables(db)) return;
db
.prepare(
`update tasks
set workspace_id = (
select w.id
from workspaces w
where w.company_id = tasks.company_id
and w.project_id = tasks.project_id
order by w.is_primary desc, w.created_at asc
limit 1
),
updated_at = ?
where requires_filesystem = 1
and workspace_id is null
and status in (${ACTIVE_TASK_STATUSES})
and project_id is not null
and exists (
select 1
from workspaces w
where w.company_id = tasks.company_id
and w.project_id = tasks.project_id
)`,
)
.run(nowIso());
}
function backfillAssigneeWorkspaces(db: DatabaseSync): void {
if (!hasRequiredTables(db) || !tableExists(db, "agents")) return;
db
.prepare(
`update tasks
set workspace_id = (
select a.workspace_id
from agents a
join workspaces w
on w.id = a.workspace_id
and w.company_id = a.company_id
where a.id = tasks.assignee_agent_id
and a.company_id = tasks.company_id
limit 1
),
updated_at = ?
where requires_filesystem = 1
and workspace_id is null
and status in (${ACTIVE_TASK_STATUSES})
and assignee_agent_id is not null
and exists (
select 1
from agents a
join workspaces w
on w.id = a.workspace_id
and w.company_id = a.company_id
where a.id = tasks.assignee_agent_id
and a.company_id = tasks.company_id
)`,
)
.run(nowIso());
}
function backfillSingleCompanyWorkspaces(db: DatabaseSync): void {
if (!hasRequiredTables(db)) return;
db
.prepare(
`update tasks
set workspace_id = (
select w.id
from workspaces w
where w.company_id = tasks.company_id
order by w.is_primary desc, w.created_at asc
limit 1
),
updated_at = ?
where requires_filesystem = 1
and workspace_id is null
and status in (${ACTIVE_TASK_STATUSES})
and (
select count(*)
from workspaces w
where w.company_id = tasks.company_id
) = 1`,
)
.run(nowIso());
}
function hasRequiredTables(db: DatabaseSync): boolean {
return tableExists(db, "tasks") && tableExists(db, "workspaces");
}
+21 -6
View File
@@ -92,12 +92,27 @@ export function buildModelCapabilityMethods(
*/
function seedModelCapabilities(rows: Array<Omit<ModelCapability, "updatedAt">>): number {
return transaction(() => {
const existing = readAll<{ cnt: number }>(
"SELECT COUNT(*) AS cnt FROM model_capabilities",
)[0]?.cnt ?? 0;
if (existing > 0) return 0;
for (const row of rows) upsertModelCapability(row);
return rows.length;
let inserted = 0;
const ts = nowIso();
const stmt = getDb().prepare(
`INSERT OR IGNORE INTO model_capabilities (
provider, model_id, reasoning, coding, writing, tier, cost_per_mtok_out, updated_at
) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
);
for (const row of rows) {
const result = stmt.run(
row.provider,
row.modelId,
row.reasoning,
row.coding,
row.writing,
row.tier,
row.costPerMtokOut,
ts,
);
inserted += Number(result.changes ?? 0);
}
return inserted;
});
}
+23 -1
View File
@@ -24,12 +24,17 @@ import {
assertSocialCompanyScopeV59,
migrateSocialCompanyScopeV59,
} from "./migrations/v59-social-company-scope";
import {
FILESYSTEM_TASK_WORKSPACES_SCHEMA_VERSION,
assertFilesystemTaskWorkspacesV60,
migrateFilesystemTaskWorkspacesV60,
} from "./migrations/v60-filesystem-task-workspaces";
import {
type ReadAll,
type Transaction,
} from "./migrations/types";
const LATEST_SCHEMA_VERSION = SOCIAL_COMPANY_SCOPE_SCHEMA_VERSION;
const LATEST_SCHEMA_VERSION = FILESYSTEM_TASK_WORKSPACES_SCHEMA_VERSION;
export function initializeDatabase(
db: DatabaseSync,
@@ -50,6 +55,18 @@ export function initializeDatabase(
assertTaskLeasesV57(db);
assertApprovalPoliciesV58(db);
assertSocialCompanyScopeV59(db);
assertFilesystemTaskWorkspacesV60(db);
});
return;
}
if (currentVersion === SOCIAL_COMPANY_SCOPE_SCHEMA_VERSION) {
tx(() => {
seedSettings(db);
assertTaskLeasesV57(db);
assertApprovalPoliciesV58(db);
assertSocialCompanyScopeV59(db);
migrateFilesystemTaskWorkspacesV60(db);
});
return;
}
@@ -60,6 +77,7 @@ export function initializeDatabase(
assertTaskLeasesV57(db);
assertApprovalPoliciesV58(db);
migrateSocialCompanyScopeV59(db);
migrateFilesystemTaskWorkspacesV60(db);
});
return;
}
@@ -70,6 +88,7 @@ export function initializeDatabase(
assertTaskLeasesV57(db);
migrateApprovalPoliciesV58(db);
migrateSocialCompanyScopeV59(db);
migrateFilesystemTaskWorkspacesV60(db);
});
return;
}
@@ -83,6 +102,7 @@ export function initializeDatabase(
migrateTaskLeasesV57(db);
migrateApprovalPoliciesV58(db);
migrateSocialCompanyScopeV59(db);
migrateFilesystemTaskWorkspacesV60(db);
});
return;
}
@@ -97,6 +117,7 @@ export function initializeDatabase(
migrateTaskLeasesV57(db);
migrateApprovalPoliciesV58(db);
migrateSocialCompanyScopeV59(db);
migrateFilesystemTaskWorkspacesV60(db);
});
return;
}
@@ -109,6 +130,7 @@ export function initializeDatabase(
migrateTaskLeasesV57(db);
migrateApprovalPoliciesV58(db);
migrateSocialCompanyScopeV59(db);
migrateFilesystemTaskWorkspacesV60(db);
});
}
+18 -1
View File
@@ -7,6 +7,7 @@ import {
} from "@shared/contracts";
import { fail, ok, type RegisterCoreHandlersDependencies } from "./ipc-core-common";
import { validateTaskCreation, checkCapabilityMatch } from "./task-quality-gates";
import { resolveTaskWorkspaceBinding } from "./task-workspace-binding";
export function registerCoreTaskGraphHandlers({
registerHandle,
@@ -109,6 +110,7 @@ export function registerCoreTaskGraphHandlers({
}
let previousStatus: string | null = null;
let previousAssigneeAgentId: string | null = null;
let existingWorkspaceId: string | null = null;
if (!isNew) {
const taskId = parsed.id;
if (!taskId) return fail("INVALID_TASK_STATE", "Task ID required for updates.");
@@ -116,15 +118,30 @@ export function registerCoreTaskGraphHandlers({
const existingTask = db.core.entity.getTask(taskId);
previousStatus = existingTask.status;
previousAssigneeAgentId = existingTask.assigneeAgentId;
existingWorkspaceId = existingTask.workspaceId;
} catch (err) {
logger.warn(`[ipc/task-graph] getTask failed for ${taskId}; treating as new task:`, err instanceof Error ? err.message : String(err));
previousStatus = null;
previousAssigneeAgentId = null;
existingWorkspaceId = null;
}
}
let id: string;
try {
id = db.core.tasks.saveTask(parsed);
const workspaceBinding = resolveTaskWorkspaceBinding(db, {
companyId: parsed.companyId,
requiresFilesystem: parsed.requiresFilesystem,
explicitWorkspaceId: parsed.workspaceId ?? (parsed.requiresFilesystem ? existingWorkspaceId : null),
projectId: parsed.projectId ?? null,
assigneeAgentId: parsed.assigneeAgentId ?? null,
});
if (!workspaceBinding.ok) {
return fail("INVALID_TASK_STATE", workspaceBinding.error);
}
id = db.core.tasks.saveTask({
...parsed,
workspaceId: workspaceBinding.workspaceId,
});
} catch (error) {
return fail("INVALID_TASK_STATE", error instanceof Error ? error.message : "Task could not be saved.");
}
+175 -44
View File
@@ -1,31 +1,14 @@
import type { ModelCapability } from "@shared/types";
import { CONNECTORS } from "./connector-definitions";
import { resolveModel, resolveModelCostFallback, resolveModels } from "./model-resolver";
/**
* Initial capability matrix written into the DB on first boot when the
* table is empty. The user's five-axis ranking from the dispatch design
* discussion is compressed to three axes:
*
* reasoning = architecture/review ∧ math ∧ research
* coding = coding (unchanged)
* writing = language/copy (unchanged — the other two collapsed upward)
*
* Scores are 1..5 on each axis. Tier is the hard lower bound the dispatcher
* clears before considering `costPerMtokOut` — a tier-3 (`hard`) task never
* runs on a tier-1 (`trivial`) model even if the trivial model's domain
* score happens to clear the numeric threshold.
*
* Cost-per-Mtok-output figures are the public list price at the provider's
* own API as of 2026-04 (approximate; the table is user-editable after
* seeding so prices can be corrected in-app without a code change).
*
* Adding a new model: append a row here AND publish a migration or an IPC
* call that writes it through `upsertModelCapability` — the seed only runs
* when the table is empty, so it does not back-fill on upgrade.
*/
type ModelCapabilitySeedRow = Omit<ModelCapability, "updatedAt">;
type CapabilityProfile = Pick<ModelCapabilitySeedRow, "reasoning" | "coding" | "writing" | "tier">;
type CatalogModel = { id: string; name: string };
export const MODEL_CAPABILITY_SEED: Array<Omit<ModelCapability, "updatedAt">> = [
// ── Anthropic (Claude) ────────────────────────────────────────────────────
// Claude Opus 4.7: strong across the board, top coding, top writing.
const DEFAULT_COST_PER_MTOK_OUT = 1;
const CURATED_MODEL_CAPABILITY_SEED: ModelCapabilitySeedRow[] = [
{
provider: "anthropic",
modelId: "claude-opus-4-7",
@@ -50,17 +33,9 @@ export const MODEL_CAPABILITY_SEED: Array<Omit<ModelCapability, "updatedAt">> =
reasoning: 3,
coding: 4,
writing: 4,
// Haiku sits at the standard tier. Its scores already clear the
// standard domain floor (>=3), so marking the tier any lower would
// exclude it from standard tasks in the dispatcher's tier-rank guard.
// The cost advantage (~5x cheaper than Sonnet) is the whole point.
tier: "standard",
costPerMtokOut: 5,
},
// ── OpenAI (GPT) ──────────────────────────────────────────────────────────
// GPT family leads reasoning/architecture/math/research; coding trails
// Claude; writing trails Claude and Kimi.
{
provider: "openai",
modelId: "gpt-5",
@@ -85,15 +60,9 @@ export const MODEL_CAPABILITY_SEED: Array<Omit<ModelCapability, "updatedAt">> =
reasoning: 3,
coding: 3,
writing: 3,
// Same reasoning as Haiku: the model clears the standard domain
// floor, so it should live at the standard tier. Llama-70B below is
// the only genuinely trivial-tier seed row.
tier: "standard",
costPerMtokOut: 2,
},
// ── Fireworks curated (open-weight) ───────────────────────────────────────
// Kimi K2: strong writing and coding, near-Claude. Reasoning solid.
{
provider: "fireworks",
modelId: "accounts/fireworks/models/kimi-k2-instruct",
@@ -103,7 +72,6 @@ export const MODEL_CAPABILITY_SEED: Array<Omit<ModelCapability, "updatedAt">> =
tier: "standard",
costPerMtokOut: 3,
},
// Kimi K2.6 Turbo via Fire Pass router — newer, faster, similar quality.
{
provider: "fireworks-firepass",
modelId: "accounts/fireworks/routers/kimi-k2p6-turbo",
@@ -113,7 +81,6 @@ export const MODEL_CAPABILITY_SEED: Array<Omit<ModelCapability, "updatedAt">> =
tier: "standard",
costPerMtokOut: 3,
},
// GLM 4.5: mid-tier across reasoning and coding, decent writing.
{
provider: "fireworks",
modelId: "accounts/fireworks/models/glm-4p5",
@@ -123,7 +90,6 @@ export const MODEL_CAPABILITY_SEED: Array<Omit<ModelCapability, "updatedAt">> =
tier: "standard",
costPerMtokOut: 2,
},
// Qwen3 Coder 480B: coding specialist.
{
provider: "fireworks",
modelId: "accounts/fireworks/models/qwen3-coder-480b-a35b-instruct",
@@ -133,7 +99,6 @@ export const MODEL_CAPABILITY_SEED: Array<Omit<ModelCapability, "updatedAt">> =
tier: "standard",
costPerMtokOut: 3,
},
// DeepSeek V3.1: strong reasoning, capable coder, writing ok.
{
provider: "fireworks",
modelId: "accounts/fireworks/models/deepseek-v3p1",
@@ -143,7 +108,6 @@ export const MODEL_CAPABILITY_SEED: Array<Omit<ModelCapability, "updatedAt">> =
tier: "standard",
costPerMtokOut: 2,
},
// Llama 3.3 70B: generalist trivial-tier fallback.
{
provider: "fireworks",
modelId: "accounts/fireworks/models/llama-v3p3-70b-instruct",
@@ -154,3 +118,170 @@ export const MODEL_CAPABILITY_SEED: Array<Omit<ModelCapability, "updatedAt">> =
costPerMtokOut: 1,
},
];
const CURATED_MODEL_CAPABILITY_BY_KEY = new Map(
CURATED_MODEL_CAPABILITY_SEED.map((row) => [capabilityKey(row.provider, row.modelId), row]),
);
function capabilityKey(provider: string, modelId: string): string {
return `${provider}::${modelId}`;
}
function profile(reasoning: number, coding: number, writing: number, tier: CapabilityProfile["tier"]): CapabilityProfile {
return { reasoning, coding, writing, tier };
}
function searchableText(provider: string, model: CatalogModel): string {
return `${provider} ${model.id} ${model.name}`.toLowerCase().replace(/[^a-z0-9]+/g, " ").trim();
}
function hasAny(text: string, terms: string[]): boolean {
return terms.some((term) => text.includes(term));
}
function hasWord(text: string, word: string): boolean {
return new RegExp(`\\b${word}\\b`).test(text);
}
function selectCapabilityProfile(text: string): CapabilityProfile {
if (text.includes("claude") && text.includes("opus")) return profile(5, 5, 5, "hard");
if (text.includes("claude") && text.includes("sonnet")) return profile(4, 5, 5, "standard");
if (text.includes("claude") && text.includes("haiku")) return profile(3, 4, 4, "standard");
if (text.includes("gpt 5") && text.includes("codex")) return profile(5, 5, 3, "hard");
if (hasAny(text, ["gpt 5", "gpt 4", "gpt 4o", "o3", "o4"])) return profile(5, 4, 3, "hard");
if (text.includes("gpt oss 120b")) return profile(4, 4, 3, "standard");
if (text.includes("gpt oss")) return profile(3, 3, 3, "standard");
if (text.includes("gemini") && hasAny(text, ["pro", "ultra"])) return profile(5, 4, 4, "hard");
if (text.includes("gemini")) return profile(3, 3, 3, "standard");
if (text.includes("grok 4") || text.includes("grok 3")) return profile(5, 4, 4, "hard");
if (text.includes("grok")) return profile(4, 3, 4, "standard");
if (hasAny(text, ["glm 5", "zai glm 5"])) return profile(5, 4, 4, "hard");
if (hasAny(text, ["glm 4 7", "glm 4 6", "zai glm 4 7"])) return profile(4, 4, 4, "standard");
if (hasAny(text, ["glm 4 5", "glm 4p5"])) return profile(3, 3, 4, "standard");
if (text.includes("kimi") && text.includes("thinking")) return profile(5, 4, 5, "hard");
if (text.includes("kimi")) return profile(4, 4, 5, "standard");
if (text.includes("deepseek") && hasAny(text, ["r1", "reasoner"])) return profile(5, 4, 3, "hard");
if (hasAny(text, ["deepseek", "deepresearch", "qwq", "thinking"])) return profile(4, 4, 3, "standard");
if (text.includes("coder") || text.includes("codestral") || text.includes("devstral")) return profile(3, 5, 3, "standard");
if (text.includes("qwen")) return profile(4, 4, 3, "standard");
if (text.includes("minimax") || text.includes("mimo")) return profile(3, 4, 4, "standard");
if (hasAny(text, ["mistral large", "mistral medium", "magistral"])) return profile(4, 4, 4, "standard");
if (text.includes("mistral") || text.includes("ministral")) return profile(3, 3, 3, "standard");
if (text.includes("nova premier") || text.includes("nova pro")) return profile(4, 3, 3, "standard");
if (text.includes("nova")) return profile(2, 2, 2, "trivial");
if (text.includes("llama 4") || text.includes("llama 70b") || text.includes("llama v3p3 70b")) {
return profile(3, 3, 3, "standard");
}
if (text.includes("llama")) return profile(2, 2, 2, "trivial");
return profile(3, 3, 3, "standard");
}
function applySizeAdjustment(selected: CapabilityProfile, text: string): CapabilityProfile {
if (hasWord(text, "nano") || hasWord(text, "micro") || hasWord(text, "3b") || hasWord(text, "7b") || hasWord(text, "8b")) {
return profile(2, 2, 2, "trivial");
}
if (
hasWord(text, "mini") ||
hasWord(text, "lite") ||
hasWord(text, "flash") ||
hasWord(text, "flashx") ||
hasWord(text, "air") ||
hasWord(text, "small") ||
hasWord(text, "instant") ||
text.includes("compound mini")
) {
return profile(
Math.min(selected.reasoning, 3),
Math.min(selected.coding, 3),
Math.min(selected.writing, 3),
selected.tier === "trivial" ? "trivial" : "standard",
);
}
return selected;
}
function inferCapabilityProfile(provider: string, model: CatalogModel): CapabilityProfile {
const text = searchableText(provider, model);
return applySizeAdjustment(selectCapabilityProfile(text), text);
}
function normalizeCost(value: number): number {
if (!Number.isFinite(value) || value < 0) return DEFAULT_COST_PER_MTOK_OUT;
return Number(value.toFixed(4));
}
function resolveCatalogCostPerMtokOut(provider: string, modelId: string): number {
const resolved = resolveModel(provider, modelId);
if (!resolved) return DEFAULT_COST_PER_MTOK_OUT;
const priced = resolveModelCostFallback(resolved, provider);
return normalizeCost(priced.cost.output);
}
export function deriveModelCapabilitySeed(provider: string, model: CatalogModel): ModelCapabilitySeedRow {
const curated = CURATED_MODEL_CAPABILITY_BY_KEY.get(capabilityKey(provider, model.id));
const selectedProfile = curated ?? inferCapabilityProfile(provider, model);
return {
provider,
modelId: model.id,
reasoning: selectedProfile.reasoning,
coding: selectedProfile.coding,
writing: selectedProfile.writing,
tier: selectedProfile.tier,
costPerMtokOut: resolveCatalogCostPerMtokOut(provider, model.id),
};
}
function listStaticCatalogModels(): Array<{ provider: string; model: CatalogModel }> {
const modelsByKey = new Map<string, { provider: string; model: CatalogModel }>();
for (const connector of CONNECTORS) {
for (const model of resolveModels(connector.provider)) {
modelsByKey.set(capabilityKey(connector.provider, model.id), {
provider: connector.provider,
model: { id: model.id, name: model.name },
});
}
if (connector.auth.type !== "local" && connector.defaultModel.trim()) {
modelsByKey.set(capabilityKey(connector.provider, connector.defaultModel), {
provider: connector.provider,
model: { id: connector.defaultModel, name: connector.defaultModel },
});
}
}
return Array.from(modelsByKey.values());
}
export function buildModelCapabilitySeed(): ModelCapabilitySeedRow[] {
const rowsByKey = new Map<string, ModelCapabilitySeedRow>();
for (const row of CURATED_MODEL_CAPABILITY_SEED) {
rowsByKey.set(capabilityKey(row.provider, row.modelId), row);
}
for (const entry of listStaticCatalogModels()) {
rowsByKey.set(capabilityKey(entry.provider, entry.model.id), deriveModelCapabilitySeed(entry.provider, entry.model));
}
return Array.from(rowsByKey.values()).sort((a, b) => {
const providerOrder = a.provider.localeCompare(b.provider);
if (providerOrder !== 0) return providerOrder;
return a.modelId.localeCompare(b.modelId);
});
}
export const MODEL_CAPABILITY_SEED: ModelCapabilitySeedRow[] = buildModelCapabilitySeed();
+17
View File
@@ -127,6 +127,22 @@ export function createOnboardingService({
});
}
function bindWorkspaceToProject(workspaceId: string | null, projectId: string) {
if (!workspaceId) return;
const workspace = db.core.workspaces.getWorkspace(workspaceId);
if (!workspace || workspace.projectId === projectId) return;
db.core.workspaces.saveWorkspace({
id: workspace.id,
companyId: workspace.companyId,
name: workspace.name,
localPath: workspace.localPath,
repoUrl: workspace.repoUrl,
repoRef: workspace.repoRef,
projectId,
isPrimary: workspace.isPrimary,
});
}
function createInitialExecutionLoop(companyId: string, workspaceId: string | null): InitialExecutionLoopResult {
const companyRows = db.queryAll<{ id: string; name: string; description: string }>(
"select id, name, description from companies where id = ? limit 1",
@@ -171,6 +187,7 @@ export function createOnboardingService({
targetDate: null,
status: "in_progress",
});
bindWorkspaceToProject(workspaceId, projectId);
const kickoffLocale = getLocale();
const kickoffTaskTitle = activeGoal
+1 -1
View File
@@ -147,7 +147,7 @@ export function createHeartbeat(
}
const pick = pickModelForAgent(agent, {
getConnector: (id) => db.core.connectors.getConnector(id),
getDisabledModels: (id) => db.core.settings.getDisabledModels(agent.companyId, id),
getDisabledModels: (companyId, connectorId) => db.core.settings.getDisabledModels(companyId, connectorId),
listModelCapabilities: () => db.core.modelCapabilities.listModelCapabilities(),
readConnectors: () => db.core.connectors.readConnectors(),
});
+1 -1
View File
@@ -59,7 +59,7 @@ export function createTaskRunner(
}
const pick = pickModelForAgent(agent, {
getConnector: (id) => db.core.connectors.getConnector(id),
getDisabledModels: (id) => db.core.settings.getDisabledModels(agent.companyId, id),
getDisabledModels: (companyId, connectorId) => db.core.settings.getDisabledModels(companyId, connectorId),
listModelCapabilities: () => db.core.modelCapabilities.listModelCapabilities(),
readConnectors: () => db.core.connectors.readConnectors(),
});
+1 -1
View File
@@ -351,7 +351,7 @@ export function buildRunPayload(params: RunDispatchParams, deps: TurnContextDeps
})),
companyWorkspaces: snapshot.workspaces
.filter((workspace) => workspace.companyId === companyId)
.map((workspace) => ({ name: workspace.name, localPath: workspace.localPath })),
.map((workspace) => ({ id: workspace.id, name: workspace.name, localPath: workspace.localPath })),
companyDocuments: snapshot.documents.filter((d) => d.companyId === companyId),
}) : null;
@@ -16,11 +16,13 @@ Body: {
"parentId": "{parentIssueId — for subtasks}",
"projectId": "{projectId — for traceability}",
"goalId": "{goalId — for strategic alignment}",
"workspaceId": "{workspaceId — required for filesystem work when the task cannot inherit one}",
"requiresFilesystem": true,
"priority": "critical|high|medium|low",
"status": "todo"
}
Always set parentId when creating subtasks. Always set projectId and goalId when available.
Set workspaceId for filesystem tasks when a workspace id is listed in the Company Capability Registry.
Set requiresFilesystem=true for code, file, repository, build, test, or local workspace work.
Set requiresFilesystem=false only for pure planning, research, coordination, or review tasks that use API state and documents.
Example: Create a task assigned to a report:
@@ -15,7 +15,7 @@ export function buildCapabilityRegistrySection(ctx: Pick<PromptContext,
if (ctx.companyWorkspaces && ctx.companyWorkspaces.length > 0) {
const workspaces = ctx.companyWorkspaces
.slice(0, 6)
.map((workspace) => `- ${workspace.name}: ${workspace.localPath}`);
.map((workspace) => `- ${workspace.name} (\`${workspace.id}\`): ${workspace.localPath}`);
parts.push(`## Workspaces\n${workspaces.join("\n")}`);
}
+1 -1
View File
@@ -44,7 +44,7 @@ export interface PromptContext {
/** Company-wide skill catalog for capability-aware delegation. */
companySkillCatalog?: Array<{ name: string; slug: string; description: string | null }>;
/** Company workspaces visible to the current run. */
companyWorkspaces?: Array<{ name: string; localPath: string }>;
companyWorkspaces?: Array<{ id: string; name: string; localPath: string }>;
}
export function buildTaskPrompt(ctx: PromptContext): string {
+103
View File
@@ -0,0 +1,103 @@
import type { AppDatabase } from "./database";
type WorkspaceBindingSource =
| "explicit"
| "project"
| "assignee"
| "run"
| "creator"
| "single_company"
| "none";
export type TaskWorkspaceBindingResult =
| { ok: true; workspaceId: string | null; source: WorkspaceBindingSource }
| { ok: false; error: string };
export interface TaskWorkspaceBindingInput {
companyId: string;
requiresFilesystem: boolean;
explicitWorkspaceId?: string | null;
projectId?: string | null;
assigneeAgentId?: string | null;
creatorAgentId?: string | null;
currentRunId?: string | null;
}
export function resolveTaskWorkspaceBinding(
db: AppDatabase,
input: TaskWorkspaceBindingInput,
): TaskWorkspaceBindingResult {
if (input.explicitWorkspaceId) {
const explicit = getCompanyWorkspace(db, input.companyId, input.explicitWorkspaceId);
if (!explicit) {
return { ok: false, error: "Selected workspace does not belong to this company." };
}
return { ok: true, workspaceId: explicit.id, source: "explicit" };
}
if (!input.requiresFilesystem) {
return {
ok: true,
workspaceId: null,
source: "none",
};
}
const project = db.core.workspaces.getProjectPrimaryWorkspace(input.projectId ?? null, input.companyId);
if (project) return { ok: true, workspaceId: project.id, source: "project" };
const assignee = getAgentWorkspace(db, input.companyId, input.assigneeAgentId ?? null);
if (assignee) return { ok: true, workspaceId: assignee.id, source: "assignee" };
const run = getRunWorkspace(db, input.companyId, input.currentRunId ?? null);
if (run) return { ok: true, workspaceId: run.id, source: "run" };
const creator = getAgentWorkspace(db, input.companyId, input.creatorAgentId ?? null);
if (creator) return { ok: true, workspaceId: creator.id, source: "creator" };
const singleCompanyWorkspace = getSingleCompanyWorkspace(db, input.companyId);
if (singleCompanyWorkspace) {
return { ok: true, workspaceId: singleCompanyWorkspace.id, source: "single_company" };
}
return {
ok: false,
error: "Filesystem tasks require a workspace. Provide workspaceId, bind the project to a workspace, or bind the assignee/creator agent to a workspace.",
};
}
function getCompanyWorkspace(db: AppDatabase, companyId: string, workspaceId: string | null): { id: string } | null {
if (!workspaceId) return null;
const workspace = db.core.workspaces.getWorkspace(workspaceId);
return workspace && workspace.companyId === companyId ? workspace : null;
}
function getAgentWorkspace(db: AppDatabase, companyId: string, agentId: string | null): { id: string } | null {
if (!agentId) return null;
try {
const agent = db.core.entity.getAgent(agentId);
if (agent.companyId !== companyId) return null;
return getCompanyWorkspace(db, companyId, agent.workspaceId);
} catch {
return null;
}
}
function getRunWorkspace(db: AppDatabase, companyId: string, runId: string | null): { id: string } | null {
if (!runId) return null;
try {
const run = db.core.runs.getRun(runId);
if (run.companyId !== companyId) return null;
return getCompanyWorkspace(db, companyId, run.workspaceId);
} catch {
return null;
}
}
function getSingleCompanyWorkspace(db: AppDatabase, companyId: string): { id: string } | null {
const workspaces = db.projection.snapshot
.listSnapshot({ companyId })
.workspaces
.filter((workspace) => workspace.companyId === companyId);
return workspaces.length === 1 ? workspaces[0]! : null;
}