Q167: the model in effect and what a default change reaches are shown

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ZacharyZhang-NYandClaude Opus 5.5 committed 2026-10-09 03:44:23 -04:00
1 parent e8722cd815
commit 26c3365896
10 files changed
+97 -19

No files matched your search

+2 -1
View File
@@ -1,3 +1,4 @@
import { agentModel } from "../../shared/agent-model";
import { TASK_APPROVAL_KINDS_SQL, TASK_SNAPSHOT_SQL } from "./task-approval";
import type { DatabaseSync } from "node:sqlite";
import { APPROVAL_STATE, RISK_LEVEL } from "../../shared/domain";
@@ -73,7 +74,7 @@ export function assertModelSelectable(providerKey: string, agentModelId: string
/** The model a run calls: the agent's own choice, or else its provider's default. */
export function effectiveModel(agentModelId: string | null, providerModel: string | null): string | null {
return agentModelId?.trim() || providerModel;
return agentModel(agentModelId, providerModel).model;
}
export function requireAgentProvider(db: DatabaseSync, orgId: string, agent: TaskExecutionAgentRow): TaskExecutionProviderRow {
+4
View File
@@ -678,4 +678,8 @@ export const zh: Record<string, string> = {
"{passed}/{total} passed": "{passed}/{total} 通过",
"{pending} pending · {approved} approved this week": "{pending} 待处理 · 本周已批准 {approved}",
"{percent}% of cap": "上限的 {percent}%",
"Leave the model blank to use the provider's default. A changed model applies from the agent's next session.": "模型留空即使用 Provider 的默认模型。修改后的模型从该 Agent 的下一次会话起生效。",
"{model} (provider default)": "{model}(Provider 默认)",
"Chosen when the session starts": "会话开始时确定",
"The default model reaches {following} agents without their own model; {own} keep their own. Sessions already started keep the model they began with.": "默认模型作用于 {following} 个未单独设置模型的 Agent;{own} 个保留自己的模型。已开始的会话继续使用开始时的模型。",
};
+2 -1
View File
@@ -204,7 +204,8 @@ function RunDetail({ run, tasks, agents, providers, actions, onOpenTask }: {
<div style={{ display: "grid", gridTemplateColumns: "120px 1fr", gap: "8px 16px", fontSize: 14, marginBottom: 20 }}>
<MetaLabel>{t("Agent")}</MetaLabel><div>{agent?.name ?? "—"} ({agent?.role ?? "—"})</div>
<MetaLabel>{t("Provider")}</MetaLabel><div>{provider?.label ?? "—"} · {provider?.model ?? "—"}</div>
<MetaLabel>{t("Provider")}</MetaLabel><div>{provider?.label ?? "—"}</div>
<MetaLabel>{t("Model")}</MetaLabel><div>{run.runModel ?? t("Chosen when the session starts")}</div>
<MetaLabel>{t("Trigger")}</MetaLabel><div>{run.trigger}</div>
<MetaLabel>{t("Created")}</MetaLabel><div>{formatRelative(run.createdAt)}</div>
{run.startedAt && (<><MetaLabel>{t("Started")}</MetaLabel><div>{formatRelative(run.startedAt)}</div></>)}
+28 -15
View File
@@ -1,3 +1,4 @@
import { agentModel } from "../../../shared/agent-model";
import { useMemo, useState, type FormEvent } from "react";
import { Avatar, Button, Field, Icon, IconButton } from "../shared/ui";
import { AvatarField } from "../shared/AvatarField";
@@ -40,21 +41,20 @@ export function TeamPage({ snapshot, actions }: PageProps) {
const openHireDialog = () => {
const provider = providers[0] ?? null;
setHireProviderId(provider?.id ?? "");
setHireModelId(provider?.model ?? "");
setHireModelId("");
setHireOpen(true);
};
const updateHireProvider = (providerId: string) => {
const provider = providers.find((item) => item.id === providerId) ?? null;
setHireProviderId(providerId);
setHireModelId(provider?.model ?? "");
setHireModelId("");
};
const submit = (event: FormEvent<HTMLFormElement>) => {
event.preventDefault();
const form = new FormData(event.currentTarget);
const selectedProvider = providers.find((provider) => provider.id === hireProviderId) ?? null;
const modelId = selectedProvider ? hireModelId.trim() || selectedProvider.model : null;
const modelId = selectedProvider ? hireModelId.trim() || null : null;
void actions.run(() => window.agentCompany.createAgent({
orgId,
name: text(form, "name"),
@@ -148,8 +148,9 @@ export function TeamPage({ snapshot, actions }: PageProps) {
</select>
</Field>
<Field label={t("Model")}>
<input className="input" name="modelId" list="agent-model-suggestions" value={hireModelId} onChange={(event) => setHireModelId(event.currentTarget.value)} />
<input className="input" name="modelId" list="agent-model-suggestions" value={hireModelId} placeholder={providers.find((provider) => provider.id === hireProviderId)?.model ?? ""} onChange={(event) => setHireModelId(event.currentTarget.value)} />
</Field>
<p style={{ fontSize: 12, color: "var(--text-3)", margin: 0 }}>{t("Leave the model blank to use the provider's default. A changed model applies from the agent's next session.")}</p>
<Field label={t("Manager")}>
<select className="input" name="managerAgentId" defaultValue="">
<option value="">{t("None")}</option>
@@ -174,15 +175,8 @@ export function TeamPage({ snapshot, actions }: PageProps) {
<Field label={t("Title")}><input className="input" name="title" defaultValue={editingAgent.title} /></Field>
<Field label={t("Department")}><input className="input" name="department" defaultValue={editingAgent.department} required /></Field>
<Field label={t("Monthly budget")}><input className="input" name="budget" type="number" min="0" step="1" defaultValue={editingAgent.budgetCents / 100} /></Field>
<Field label={t("Provider")}>
<select className="input" name="modelProviderId" defaultValue={editingAgent.modelProviderId ?? ""}>
<option value="">{t("Unassigned")}</option>
{providers.map((provider) => <option key={provider.id} value={provider.id}>{provider.label}</option>)}
</select>
</Field>
<Field label={t("Model")}>
<input className="input" name="modelId" defaultValue={editingAgent.modelId ?? ""} list="agent-model-suggestions" />
</Field>
<EditModelFields agent={editingAgent} providers={providers} />
<p style={{ fontSize: 12, color: "var(--text-3)", margin: 0 }}>{t("Leave the model blank to use the provider's default. A changed model applies from the agent's next session.")}</p>
<Field label={t("Seniority")}>
{/* The CEO's rank carries company-wide powers, so it is shown, never picked. */}
{editingAgent.roleLevel === "c_suite" ? (
@@ -251,12 +245,13 @@ function buildRoster(
const cap = agents.reduce((max, agent) => (agent.budgetCents > max ? agent.budgetCents : max), 0);
for (const agent of agents) {
const provider = agent.modelProviderId ? providers.find((entry) => entry.id === agent.modelProviderId) : null;
const inEffect = agentModel(agent.modelId, provider?.model ?? null);
rows.push({
id: agent.id,
name: agent.name,
role: agent.role,
dept: agent.department || "—",
model: agent.modelId ?? (provider?.model ?? "—"),
model: inEffect.source === "provider_default" ? t("{model} (provider default)", { model: inEffect.model ?? "" }) : inEffect.model ?? "—",
providerLabel: provider?.label ?? "—",
budgetCents: agent.budgetCents,
budgetCapCents: cap > 0 ? cap : null,
@@ -267,6 +262,24 @@ function buildRoster(
return rows;
}
/** The placeholder names the default of the provider picked now, which is the model a blank field will follow. */
function EditModelFields({ agent, providers }: { agent: AgentRecord; providers: ProviderRecord[] }) {
const [providerId, setProviderId] = useState(agent.modelProviderId ?? "");
return (
<>
<Field label={t("Provider")}>
<select className="input" name="modelProviderId" value={providerId} onChange={(event) => setProviderId(event.currentTarget.value)}>
<option value="">{t("Unassigned")}</option>
{providers.map((provider) => <option key={provider.id} value={provider.id}>{provider.label}</option>)}
</select>
</Field>
<Field label={t("Model")}>
<input className="input" name="modelId" defaultValue={agent.modelId ?? ""} placeholder={providers.find((provider) => provider.id === providerId)?.model ?? ""} list="agent-model-suggestions" />
</Field>
</>
);
}
function RosterRow({ row, last, onEdit }: { row: RosterEntry; last: boolean; onEdit: (() => void) | null }) {
return (
<div
@@ -1,3 +1,4 @@
import { defaultModelReach } from "../../../../shared/agent-model";
import { ScopeNote } from "./ScopeNote";
import { useState, type FormEvent } from "react";
import { Button, Pill } from "../../shared/ui";
@@ -121,6 +122,11 @@ export function ProvidersPanel({ orgId, companyName, employeeId, providers, auth
<ProviderConnectFields provider={active} entry={activeCatalogEntry} />
)
)}
{active && (
<p style={{ fontSize: 12, color: "var(--text-3)", margin: 0 }}>
{t("The default model reaches {following} agents without their own model; {own} keep their own. Sessions already started keep the model they began with.", { following: defaultModelReach(agents, active.id).following, own: defaultModelReach(agents, active.id).ownModel })}
</p>
)}
</Dialog>
</div>
);
+17
View File
@@ -0,0 +1,17 @@
/** Where an agent's model comes from: its own setting, else its provider's default. */
export type ModelSource = "agent" | "provider_default" | "none";
/** The model a new session of the agent starts with; a running session keeps the one it pinned. */
export function agentModel(agentModelId: string | null, providerModel: string | null): { model: string | null; source: ModelSource } {
const own = agentModelId?.trim();
if (own) return { model: own, source: "agent" };
if (providerModel) return { model: providerModel, source: "provider_default" };
return { model: null, source: "none" };
}
/** Agents a change to this provider's default model reaches, and those that keep their own model. */
export function defaultModelReach(agents: Array<{ modelProviderId: string | null; modelId: string | null }>, providerId: string): { following: number; ownModel: number } {
const bound = agents.filter((agent) => agent.modelProviderId === providerId);
const ownModel = bound.filter((agent) => agent.modelId?.trim()).length;
return { following: bound.length - ownModel, ownModel };
}
+2
View File
@@ -211,6 +211,8 @@ export interface TaskRunRecord {
contextJson: string | null;
/** Capabilities the run could not load, one per line, so the user sees what it worked without. */
capabilityWarnings: string | null;
/** The model the session pinned when it started; null until then. */
runModel: string | null;
}
export interface ApprovalRecord {
+11 -1
View File
@@ -89,7 +89,9 @@ test("onboards, hires, and creates work in the Electron shell", async () => {
};
});
expect(hiredAgent.modelProviderId).toBeTruthy();
expect(hiredAgent.modelId).toBe("gpt-5.4");
// A hire without its own model follows the provider default, and the roster says so.
expect(hiredAgent.modelId).toBeNull();
await expect(page.getByText("gpt-5.4 (provider default)").first()).toBeVisible();
expect(hiredAgent.workspaceId).toBe(hiredAgent.primaryWorkspaceId);
expect(hiredAgent.heartbeatEnabled).toBe(true);
expect(hiredAgent.heartbeatIntervalSec).toBe(120);
@@ -98,6 +100,14 @@ test("onboards, hires, and creates work in the Electron shell", async () => {
await page.getByRole("button", { name: "Edit Alex Chen" }).click();
await page.locator(".modal").getByLabel("Name", { exact: true }).fill("Alex Turing");
await page.getByLabel("Avatar emoji").fill("🚀");
// The blank model field names the default of whichever provider is picked now.
const editModel = page.locator(".modal").getByLabel("Model", { exact: true });
await expect(editModel).toHaveAttribute("placeholder", "gpt-5.4");
const editProvider = page.locator(".modal select[name=modelProviderId]");
const savedProvider = await editProvider.inputValue();
await editProvider.selectOption("");
await expect(editModel).toHaveAttribute("placeholder", "");
await editProvider.selectOption(savedProvider);
await page.locator(".modal").getByRole("button", { name: "Save" }).click();
await expect(page.getByText("Alex Turing").first()).toBeVisible();
await expect(page.locator(".table-row", { hasText: "Alex Turing" }).locator(".avatar", { hasText: "🚀" })).toBeVisible();
+24
View File
@@ -0,0 +1,24 @@
import { describe, expect, it } from "vitest";
import { agentModel, defaultModelReach } from "../../src/shared/agent-model";
import { effectiveModel } from "../../src/main/services/task-run-preparation";
describe("agent model", () => {
it("names the model in effect and where it comes from", () => {
expect(agentModel("gpt-6", "gpt-5")).toEqual({ model: "gpt-6", source: "agent" });
expect(agentModel(" ", "gpt-5")).toEqual({ model: "gpt-5", source: "provider_default" });
expect(agentModel(null, null)).toEqual({ model: null, source: "none" });
// Runs start with exactly the model the UI shows.
expect(effectiveModel(" gpt-6 ", "gpt-5")).toBe("gpt-6");
expect(effectiveModel(null, "gpt-5")).toBe("gpt-5");
});
it("counts the agents a default-model change reaches", () => {
const agents = [
{ modelProviderId: "p1", modelId: null },
{ modelProviderId: "p1", modelId: "" },
{ modelProviderId: "p1", modelId: "own" },
{ modelProviderId: "p2", modelId: null },
];
expect(defaultModelReach(agents, "p1")).toEqual({ following: 2, ownModel: 1 });
});
});
+1 -1
View File
@@ -6,7 +6,7 @@ function run(agentId: string | null, contextJson: string | null): TaskRunRecord
return {
id: `run_${Math.random()}`, orgId: "org", taskId: "tsk", agentId, providerId: null, providerRequestId: null,
executionContextId: null, serviceIdentityId: null, workspaceMountJson: "[]", secretScopeJson: "{}",
trigger: "chat_mention", state: "completed", createdAt: "", startedAt: null, completedAt: null, errorMessage: null, contextJson, capabilityWarnings: null,
trigger: "chat_mention", state: "completed", createdAt: "", startedAt: null, completedAt: null, errorMessage: null, contextJson, capabilityWarnings: null, runModel: null,
};
}