Q17: budget every model request against the provider's context window

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ZacharyZhang-NYandClaude Opus 5.5 committed 2026-10-08 04:40:02 -04:00
1 parent f42ab62dbd
commit f4d16eb46a
26 files changed
+247 -52

No files matched your search

@@ -0,0 +1,6 @@
export const providerContextWindowSchema = `
alter table providers add column context_window_tokens integer not null default 128000;
insert into schema_migrations(version, name, applied_at)
values (62, 'provider_context_window', datetime('now'));
`;
+2
View File
@@ -60,6 +60,7 @@ import { integrationKeysSchema } from "./migrations/058-integration-keys";
import { taskHandoffSchema } from "./migrations/059-task-handoff"; import { taskHandoffSchema } from "./migrations/059-task-handoff";
import { chatWakeTextPurgeSchema } from "./migrations/060-chat-wake-text-purge"; import { chatWakeTextPurgeSchema } from "./migrations/060-chat-wake-text-purge";
import { taskRunContextSchema } from "./migrations/061-task-run-context"; import { taskRunContextSchema } from "./migrations/061-task-run-context";
import { providerContextWindowSchema } from "./migrations/062-provider-context-window";
interface Migration { interface Migration {
version: number; version: number;
@@ -129,6 +130,7 @@ const MIGRATIONS: Migration[] = [
{ version: 59, name: "task_handoff", sql: taskHandoffSchema }, { version: 59, name: "task_handoff", sql: taskHandoffSchema },
{ version: 60, name: "chat_wake_text_purge", sql: chatWakeTextPurgeSchema }, { version: 60, name: "chat_wake_text_purge", sql: chatWakeTextPurgeSchema },
{ version: 61, name: "task_run_context", sql: taskRunContextSchema }, { version: 61, name: "task_run_context", sql: taskRunContextSchema },
{ version: 62, name: "provider_context_window", sql: providerContextWindowSchema },
]; ];
const SCHEMA_VERSION = MIGRATIONS[MIGRATIONS.length - 1].version; const SCHEMA_VERSION = MIGRATIONS[MIGRATIONS.length - 1].version;
+3
View File
@@ -1,3 +1,4 @@
import { fitContextBudget } from "./context-budget";
import { AuditService } from "./audit-service"; import { AuditService } from "./audit-service";
import { executeWorkspaceTool, WORKSPACE_TOOLS } from "./agent-workspace-tools"; import { executeWorkspaceTool, WORKSPACE_TOOLS } from "./agent-workspace-tools";
import { optionalString, requireString } from "./agent-tool-args"; import { optionalString, requireString } from "./agent-tool-args";
@@ -54,6 +55,7 @@ export interface GoalCompletionArgs {
providers: Pick<ProviderService, "runToolTurn">; providers: Pick<ProviderService, "runToolTurn">;
providerId: string; providerId: string;
workspaceId: string | null; workspaceId: string | null;
inputBudgetTokens: number;
call: ToolCall; call: ToolCall;
} }
@@ -129,6 +131,7 @@ async function auditGoalCompletion(
const instructions = "You are the independent completion auditor. You did not do the work and owe its author nothing; your only loyalty is to what actually exists. Be strict: an unverifiable claim is a false claim."; const instructions = "You are the independent completion auditor. You did not do the work and owe its author nothing; your only loyalty is to what actually exists. Be strict: an unverifiable claim is a false claim.";
for (let iteration = 1; iteration <= AUDIT_MAX_ITERATIONS; iteration += 1) { for (let iteration = 1; iteration <= AUDIT_MAX_ITERATIONS; iteration += 1) {
fitContextBudget(instructions, messages, auditTools, args.inputBudgetTokens, iteration);
const turn = await args.providers.runToolTurn({ const turn = await args.providers.runToolTurn({
orgId: args.ctx.orgId, orgId: args.ctx.orgId,
providerId: args.providerId, providerId: args.providerId,
+5
View File
@@ -7,6 +7,7 @@ import { canonicalMentionHandle } from "../../shared/mention-handles";
import type { AgentTurnMessage, ToolDefinition } from "./provider-tool-turn"; import type { AgentTurnMessage, ToolDefinition } from "./provider-tool-turn";
import { isComposioTool, executeComposioTool } from "./composio/composio-tool-bridge"; import { isComposioTool, executeComposioTool } from "./composio/composio-tool-bridge";
import { compactToolHistory } from "./run-context-compaction"; import { compactToolHistory } from "./run-context-compaction";
import { fitContextBudget } from "./context-budget";
const MAX_ITERATIONS = 12; const MAX_ITERATIONS = 12;
@@ -22,6 +23,8 @@ export interface AgentRunLoopInput {
pollNewMessages?: () => string | null; pollNewMessages?: () => string | null;
/** Called after each model turn the provider answered. */ /** Called after each model turn the provider answered. */
onModelTurn?: () => void; onModelTurn?: () => void;
/** Input tokens the provider's context window leaves after the output reserve. */
inputBudgetTokens: number;
} }
export interface AgentRunLoopResult { export interface AgentRunLoopResult {
@@ -56,6 +59,7 @@ export async function runAgentLoop(deps: AgentRunLoopDeps, input: AgentRunLoopIn
messages.splice(0, messages.length, ...compaction.messages); messages.splice(0, messages.length, ...compaction.messages);
console.info(`[run-loop] iteration ${iteration}: elided ${compaction.elided} earlier tool result(s)`); console.info(`[run-loop] iteration ${iteration}: elided ${compaction.elided} earlier tool result(s)`);
} }
fitContextBudget(input.instructions, messages, allTools, input.inputBudgetTokens, iteration);
const turn = await deps.providers.runToolTurn({ const turn = await deps.providers.runToolTurn({
orgId: input.orgId, orgId: input.orgId,
providerId: input.providerId, providerId: input.providerId,
@@ -102,6 +106,7 @@ export async function runAgentLoop(deps: AgentRunLoopDeps, input: AgentRunLoopIn
providers: deps.providers, providers: deps.providers,
providerId: input.providerId, providerId: input.providerId,
workspaceId: input.workspaceId, workspaceId: input.workspaceId,
inputBudgetTokens: input.inputBudgetTokens,
call, call,
}); });
} else { } else {
+39
View File
@@ -0,0 +1,39 @@
import { compactToolHistory } from "./run-context-compaction";
import type { AgentTurnMessage, ToolDefinition } from "./provider-tool-turn";
/** Tokens kept free for the model's answer. */
export const OUTPUT_RESERVE_TOKENS = 16_000;
/** Share of the input budget the compiled skills may take. */
export const SKILL_BUDGET_SHARE = 0.4;
const CJK = /[\u3000-\u9fff\uac00-\ud7af\uf900-\ufaff\uff00-\uffef]/g;
/** Conservative: CJK counts one token per character, other text one per three. */
export function estimateTokens(text: string): number {
const cjk = text.match(CJK)?.length ?? 0;
return cjk + Math.ceil((text.length - cjk) / 3);
}
export function inputBudgetTokens(contextWindowTokens: number): number {
return contextWindowTokens - OUTPUT_RESERVE_TOKENS;
}
export function skillBudgetTokens(contextWindowTokens: number): number {
return Math.floor(inputBudgetTokens(contextWindowTokens) * SKILL_BUDGET_SHARE);
}
export /** Shrinks history to fit the window, or fails with the numbers. */
function fitContextBudget(instructions: string, messages: AgentTurnMessage[], tools: ToolDefinition[], budget: number, iteration: number): void {
const fixed = estimateTokens(instructions) + estimateTokens(JSON.stringify(tools));
let total = fixed + estimateTokens(JSON.stringify(messages));
if (total > budget) {
messages.splice(0, messages.length, ...compactToolHistory(messages, 1).messages);
const compacted = fixed + estimateTokens(JSON.stringify(messages));
console.warn(`[run-loop] iteration ${iteration}: ~${total} tokens over the ${budget} budget; aggressive compaction brought it to ~${compacted}`);
total = compacted;
}
if (total > budget) {
throw new Error(`This run needs ~${total} input tokens but the provider's context window leaves ${budget}. Raise the provider's context window in Settings > Advanced if the model supports more, or split the task.`);
}
}
+10 -2
View File
@@ -36,6 +36,9 @@ export type { ProviderResponseCreateInput };
const AUTH_SESSION_TTL_MS = 15 * 60 * 1000; const AUTH_SESSION_TTL_MS = 15 * 60 * 1000;
/** Room for instructions, the output reserve, and some history. */
const MIN_CONTEXT_WINDOW_TOKENS = 24_000;
export class ProviderService { export class ProviderService {
private readonly audit: AuditService; private readonly audit: AuditService;
private readonly authBroker: AuthBrokerService; private readonly authBroker: AuthBrokerService;
@@ -254,6 +257,10 @@ export class ProviderService {
updatePolicy(input: UpdateProviderPolicyInput): void { updatePolicy(input: UpdateProviderPolicyInput): void {
const monthlyRequestLimit = Math.trunc(input.monthlyRequestLimit); const monthlyRequestLimit = Math.trunc(input.monthlyRequestLimit);
if (monthlyRequestLimit < 1) throw new Error("Monthly request limit must be at least 1."); if (monthlyRequestLimit < 1) throw new Error("Monthly request limit must be at least 1.");
const contextWindowTokens = Math.trunc(input.contextWindowTokens);
if (!(contextWindowTokens >= MIN_CONTEXT_WINDOW_TOKENS)) {
throw new Error(`Context window must be at least ${MIN_CONTEXT_WINDOW_TOKENS} tokens.`);
}
if (!Object.values(PROVIDER_DATA_POLICY).includes(input.dataPolicy)) throw new Error("Provider data policy is invalid."); if (!Object.values(PROVIDER_DATA_POLICY).includes(input.dataPolicy)) throw new Error("Provider data policy is invalid.");
if (!WORKSPACE_SCOPES.has(input.workspaceScope)) throw new Error("Provider workspace scope is invalid."); if (!WORKSPACE_SCOPES.has(input.workspaceScope)) throw new Error("Provider workspace scope is invalid.");
if (input.dataPolicy === PROVIDER_DATA_POLICY.ZERO_DATA_RETENTION && input.backgroundEnabled) { if (input.dataPolicy === PROVIDER_DATA_POLICY.ZERO_DATA_RETENTION && input.backgroundEnabled) {
@@ -265,9 +272,9 @@ export class ProviderService {
this.db.prepare(` this.db.prepare(`
update providers update providers
set data_policy = ?, background_enabled = ?, monthly_request_limit = ?, workspace_scope = ? set data_policy = ?, background_enabled = ?, monthly_request_limit = ?, workspace_scope = ?, context_window_tokens = ?
where id = ? and org_id = ? where id = ? and org_id = ?
`).run(input.dataPolicy, input.backgroundEnabled ? 1 : 0, monthlyRequestLimit, input.workspaceScope, input.providerId, input.orgId); `).run(input.dataPolicy, input.backgroundEnabled ? 1 : 0, monthlyRequestLimit, input.workspaceScope, contextWindowTokens, input.providerId, input.orgId);
this.audit.record({ this.audit.record({
orgId: input.orgId, orgId: input.orgId,
entityType: "provider", entityType: "provider",
@@ -281,6 +288,7 @@ export class ProviderService {
backgroundEnabled: input.backgroundEnabled, backgroundEnabled: input.backgroundEnabled,
monthlyRequestLimit, monthlyRequestLimit,
workspaceScope: input.workspaceScope, workspaceScope: input.workspaceScope,
contextWindowTokens,
}, },
}); });
} }
+3 -3
View File
@@ -5,10 +5,10 @@ const ELIDE_ABOVE_CHARS = 600;
const PREVIEW_CHARS = 300; const PREVIEW_CHARS = 300;
/** Older tool results shrink to a preview; the latest turns stay verbatim. */ /** Older tool results shrink to a preview; the latest turns stay verbatim. */
export function compactToolHistory(messages: AgentTurnMessage[]): { messages: AgentTurnMessage[]; elided: number } { export function compactToolHistory(messages: AgentTurnMessage[], keepRecent = KEEP_RECENT_TOOL_TURNS): { messages: AgentTurnMessage[]; elided: number } {
const toolTurnIndexes = messages.flatMap((message, index) => (message.role === "tool" ? [index] : [])); const toolTurnIndexes = messages.flatMap((message, index) => (message.role === "tool" ? [index] : []));
const compactBefore = toolTurnIndexes.length > KEEP_RECENT_TOOL_TURNS const compactBefore = toolTurnIndexes.length > keepRecent
? toolTurnIndexes[toolTurnIndexes.length - KEEP_RECENT_TOOL_TURNS] ? toolTurnIndexes[toolTurnIndexes.length - keepRecent]
: 0; : 0;
let elided = 0; let elided = 0;
const compacted = messages.map((message, index) => { const compacted = messages.map((message, index) => {
+7 -12
View File
@@ -1,3 +1,4 @@
import { estimateTokens } from "./context-budget";
import { existsSync, readFileSync } from "node:fs"; import { existsSync, readFileSync } from "node:fs";
import { createHash } from "node:crypto"; import { createHash } from "node:crypto";
import { normalize } from "node:path"; import { normalize } from "node:path";
@@ -34,13 +35,6 @@ export function forcedSkillSlugCandidates(language: string): string[] {
return language === "zh" ? [HUMANIZER_SLUG_ZH, HUMANIZER_SLUG_EN] : [HUMANIZER_SLUG_EN, HUMANIZER_SLUG_ZH]; return language === "zh" ? [HUMANIZER_SLUG_ZH, HUMANIZER_SLUG_EN] : [HUMANIZER_SLUG_EN, HUMANIZER_SLUG_ZH];
} }
/**
* Compiled skill prompts stop packing past this size so one over-granted
* agent cannot overflow the model's context window. Skills pack whole, forced
* skills first, and anything omitted is named in the prompt.
*/
export const MAX_COMPILED_PROMPT_CHARS = 200_000;
export class SkillService { export class SkillService {
private readonly audit: AuditService; private readonly audit: AuditService;
private readonly operations: OperationsService; private readonly operations: OperationsService;
@@ -243,8 +237,8 @@ export class SkillService {
* Freezes the exact skill set this run carries: forced house-style skills * Freezes the exact skill set this run carries: forced house-style skills
* first (identical for everyone, CEO included), then the agent's enabled * first (identical for everyone, CEO included), then the agent's enabled
* bindings in priority order, deduplicated per skill. Skills pack whole * bindings in priority order, deduplicated per skill. Skills pack whole
* until MAX_COMPILED_PROMPT_CHARS; whatever does not fit is named in the * until input.maxTokens; whatever does not fit is named in the compiled
* compiled prompt instead of silently overflowing the model's context. * prompt instead of silently overflowing the model's context.
*/ */
resolveRuntimeSnapshot(input: ResolveAgentSkillsInput): string { resolveRuntimeSnapshot(input: ResolveAgentSkillsInput): string {
// Idempotent per run: a run interrupted after resolving (crash, quit) is // Idempotent per run: a run interrupted after resolving (crash, quit) is
@@ -280,7 +274,7 @@ export class SkillService {
const tools = new Set<string>(); const tools = new Set<string>();
const revisionIds: string[] = []; const revisionIds: string[] = [];
const omitted: string[] = []; const omitted: string[] = [];
let compiledLength = 0; let compiledTokens = 0;
for (const row of rows) { for (const row of rows) {
if (!existsSync(row.object_key)) throw new Error(`Skill artifact is missing: ${row.object_key}`); if (!existsSync(row.object_key)) throw new Error(`Skill artifact is missing: ${row.object_key}`);
const manifest = JSON.parse(row.manifest_json) as SkillManifest; const manifest = JSON.parse(row.manifest_json) as SkillManifest;
@@ -289,14 +283,15 @@ export class SkillService {
`Version: ${row.version_semver}`, `Version: ${row.version_semver}`,
readFileSync(row.object_key, "utf8").trim(), readFileSync(row.object_key, "utf8").trim(),
].join("\n\n"); ].join("\n\n");
if (compiledParts.length > 0 && compiledLength + part.length > MAX_COMPILED_PROMPT_CHARS) { const partTokens = estimateTokens(part);
if (compiledParts.length > 0 && compiledTokens + partTokens > input.maxTokens) {
omitted.push(manifest.name); omitted.push(manifest.name);
continue; continue;
} }
for (const tool of manifest.requiredTools) tools.add(tool); for (const tool of manifest.requiredTools) tools.add(tool);
revisionIds.push(row.revision_id); revisionIds.push(row.revision_id);
compiledParts.push(part); compiledParts.push(part);
compiledLength += part.length; compiledTokens += partTokens;
} }
if (omitted.length > 0) { if (omitted.length > 0) {
compiledParts.push( compiledParts.push(
+6 -4
View File
@@ -26,6 +26,7 @@ import {
import { renderTaskHandoff } from "./agent-task-handoff"; import { renderTaskHandoff } from "./agent-task-handoff";
import { createRunContextRecorder, createRunMessagePoller, loadChatWakeContext } from "./chat-wake-context"; import { createRunContextRecorder, createRunMessagePoller, loadChatWakeContext } from "./chat-wake-context";
import { runningWorkspaceWriter } from "./workspace-write-lock"; import { runningWorkspaceWriter } from "./workspace-write-lock";
import { inputBudgetTokens, skillBudgetTokens } from "./context-budget";
import { loadComposioTools } from "./composio/composio-tool-bridge"; import { loadComposioTools } from "./composio/composio-tool-bridge";
import { connectorDisplayName } from "./composio/connector-brands"; import { connectorDisplayName } from "./composio/connector-brands";
import { ConnectorGrantsService } from "./composio/connector-grants"; import { ConnectorGrantsService } from "./composio/connector-grants";
@@ -155,7 +156,7 @@ export class TaskExecutionService {
let providerRequestId: string | null = null; let providerRequestId: string | null = null;
let skillRuntimeSnapshot: SkillRuntimeSnapshotRow | null = null; let skillRuntimeSnapshot: SkillRuntimeSnapshotRow | null = null;
try { try {
skillRuntimeSnapshot = this.resolveSkillRuntimeSnapshot(orgId, agent.id, runId); skillRuntimeSnapshot = this.resolveSkillRuntimeSnapshot(orgId, agent.id, runId, skillBudgetTokens(provider.context_window_tokens));
// Watermark for reply delivery and mid-run message polling. // Watermark for reply delivery and mid-run message polling.
const runStartedAt = new Date().toISOString(); const runStartedAt = new Date().toISOString();
const chatContext = loadChatWakeContext(this.db, orgId, agent.id, isolation.secretScopeJson); const chatContext = loadChatWakeContext(this.db, orgId, agent.id, isolation.secretScopeJson);
@@ -208,6 +209,7 @@ export class TaskExecutionService {
toolContext, toolContext,
externalTools, externalTools,
onModelTurn: contextRecorder.commit, onModelTurn: contextRecorder.commit,
inputBudgetTokens: inputBudgetTokens(provider.context_window_tokens),
pollNewMessages: createRunMessagePoller(this.db, { orgId, onInjected: contextRecorder.addMidRun, agentId: agent.id, sourceThreadId: chatContext?.threadId ?? null, runStartedAt }), pollNewMessages: createRunMessagePoller(this.db, { orgId, onInjected: contextRecorder.addMidRun, agentId: agent.id, sourceThreadId: chatContext?.threadId ?? null, runStartedAt }),
}, },
); );
@@ -312,7 +314,7 @@ export class TaskExecutionService {
private requireAgentProvider(orgId: string, agent: TaskExecutionAgentRow): TaskExecutionProviderRow { private requireAgentProvider(orgId: string, agent: TaskExecutionAgentRow): TaskExecutionProviderRow {
if (!agent.model_provider_id) throw new Error("Task assignee requires a model provider."); if (!agent.model_provider_id) throw new Error("Task assignee requires a model provider.");
const provider = this.db.prepare(` const provider = this.db.prepare(`
select id, label, provider_key, model select id, label, provider_key, model, context_window_tokens
from providers from providers
where id = ? and org_id = ? where id = ? and org_id = ?
`).get(agent.model_provider_id, orgId) as TaskExecutionProviderRow | undefined; `).get(agent.model_provider_id, orgId) as TaskExecutionProviderRow | undefined;
@@ -614,9 +616,9 @@ export class TaskExecutionService {
`).run(TASK_RUN_STATE.FAILED, providerRequestId, new Date().toISOString(), errorMessage, runId, orgId); `).run(TASK_RUN_STATE.FAILED, providerRequestId, new Date().toISOString(), errorMessage, runId, orgId);
} }
private resolveSkillRuntimeSnapshot(orgId: string, agentId: string, runId: string): SkillRuntimeSnapshotRow | null { private resolveSkillRuntimeSnapshot(orgId: string, agentId: string, runId: string, maxTokens: number): SkillRuntimeSnapshotRow | null {
if (!this.skills.hasRuntimeSkills(orgId, agentId)) return null; if (!this.skills.hasRuntimeSkills(orgId, agentId)) return null;
const snapshotId = this.skills.resolveRuntimeSnapshot({ orgId, agentId, runId }); const snapshotId = this.skills.resolveRuntimeSnapshot({ orgId, agentId, runId, maxTokens });
const snapshot = this.db.prepare(` const snapshot = this.db.prepare(`
select id, compiled_prompt, tools_manifest, source_revision_ids select id, compiled_prompt, tools_manifest, source_revision_ids
from skill_runtime_snapshots from skill_runtime_snapshots
+1
View File
@@ -34,6 +34,7 @@ export interface TaskExecutionProviderRow {
label: string; label: string;
provider_key: string; provider_key: string;
model: string | null; model: string | null;
context_window_tokens: number;
} }
export interface ProjectRow { export interface ProjectRow {
+3
View File
@@ -28,6 +28,9 @@ export const zh: Record<string, string> = {
"Advanced": "高级", "Advanced": "高级",
"Integrations": "集成密钥", "Integrations": "集成密钥",
"Read by {names}": "已读:{names}", "Read by {names}": "已读:{names}",
"Context window (tokens)": "上下文窗口(token)",
"{label} context window": "{label} 上下文窗口",
"The model's context window. Runs keep prompts, skills, history, and tool results inside it.": "模型的上下文窗口。运行时提示词、技能、历史和工具结果都要放进这个范围。",
"Keys are stored on this device, encrypted by the operating system, and shared by every company.": "密钥保存在本机,由操作系统加密,所有公司共用。", "Keys are stored on this device, encrypted by the operating system, and shared by every company.": "密钥保存在本机,由操作系统加密,所有公司共用。",
"Composio API key": "Composio API 密钥", "Composio API key": "Composio API 密钥",
"Powers the Connectors page and agent connector tools. Create one at platform.composio.dev.": "用于连接器页面和 Agent 的连接器工具。在 platform.composio.dev 创建。", "Powers the Connectors page and agent connector tools. Create one at platform.composio.dev.": "用于连接器页面和 Agent 的连接器工具。在 platform.composio.dev 创建。",
@@ -26,7 +26,7 @@ interface ProvidersPanelProps {
actions: SnapshotActions; actions: SnapshotActions;
} }
const POLICY_GRID = "minmax(150px, 1.2fr) minmax(118px, 0.8fr) minmax(140px, 1fr) 112px 124px 92px"; const POLICY_GRID = "minmax(150px, 1.2fr) minmax(118px, 0.8fr) minmax(140px, 1fr) 112px 124px 124px 92px";
type WorkspaceScope = "organization" | "workspace"; type WorkspaceScope = "organization" | "workspace";
export function ProvidersPanel({ orgId, employeeId, providers, authProfiles, catalog, agents, providerRateLimits, actions }: ProvidersPanelProps) { export function ProvidersPanel({ orgId, employeeId, providers, authProfiles, catalog, agents, providerRateLimits, actions }: ProvidersPanelProps) {
@@ -138,9 +138,9 @@ function ProviderPolicyTable({
<div className="card" style={{ padding: 0, overflowX: "auto", marginBottom: 28 }}> <div className="card" style={{ padding: 0, overflowX: "auto", marginBottom: 28 }}>
<div <div
className="table-row head" className="table-row head"
style={{ gridTemplateColumns: POLICY_GRID, minWidth: 780 }} style={{ gridTemplateColumns: POLICY_GRID, minWidth: 900 }}
> >
<div>{t("Provider")}</div><div>{t("Scope")}</div><div>{t("Data policy")}</div><div>{t("Background")}</div><div>{t("Monthly limit")}</div><div>{t("Usage")}</div> <div>{t("Provider")}</div><div>{t("Scope")}</div><div>{t("Data policy")}</div><div>{t("Background")}</div><div>{t("Monthly limit")}</div><div>{t("Context window (tokens)")}</div><div>{t("Usage")}</div>
</div> </div>
{providers.map((provider, index) => { {providers.map((provider, index) => {
const rateLimit = requireProviderRateLimit(provider, providerRateLimits); const rateLimit = requireProviderRateLimit(provider, providerRateLimits);
@@ -176,6 +176,7 @@ function ProviderPolicyRow({
const [dataPolicy, setDataPolicy] = useState(provider.dataPolicy); const [dataPolicy, setDataPolicy] = useState(provider.dataPolicy);
const [backgroundEnabled, setBackgroundEnabled] = useState(provider.backgroundEnabled); const [backgroundEnabled, setBackgroundEnabled] = useState(provider.backgroundEnabled);
const [monthlyRequestLimit, setMonthlyRequestLimit] = useState(String(provider.monthlyRequestLimit)); const [monthlyRequestLimit, setMonthlyRequestLimit] = useState(String(provider.monthlyRequestLimit));
const [contextWindowTokens, setContextWindowTokens] = useState(String(provider.contextWindowTokens));
const [workspaceScope, setWorkspaceScope] = useState<WorkspaceScope>(provider.workspaceScope === "workspace" ? "workspace" : "organization"); const [workspaceScope, setWorkspaceScope] = useState<WorkspaceScope>(provider.workspaceScope === "workspace" ? "workspace" : "organization");
const savePolicy = () => { const savePolicy = () => {
@@ -186,6 +187,7 @@ function ProviderPolicyRow({
backgroundEnabled, backgroundEnabled,
monthlyRequestLimit: Number(monthlyRequestLimit), monthlyRequestLimit: Number(monthlyRequestLimit),
workspaceScope, workspaceScope,
contextWindowTokens: Number(contextWindowTokens),
})); }));
}; };
@@ -199,7 +201,7 @@ function ProviderPolicyRow({
className="table-row" className="table-row"
style={{ style={{
gridTemplateColumns: POLICY_GRID, gridTemplateColumns: POLICY_GRID,
minWidth: 780, minWidth: 900,
borderBottom: last ? "none" : "1px solid var(--border-soft)", borderBottom: last ? "none" : "1px solid var(--border-soft)",
}} }}
> >
@@ -242,6 +244,16 @@ function ProviderPolicyRow({
value={monthlyRequestLimit} value={monthlyRequestLimit}
onChange={(event) => setMonthlyRequestLimit(event.target.value)} onChange={(event) => setMonthlyRequestLimit(event.target.value)}
/> />
<input
className="input"
aria-label={t("{label} context window", { label: provider.label })}
title={t("The model's context window. Runs keep prompts, skills, history, and tool results inside it.")}
type="number"
min={24000}
step={1000}
value={contextWindowTokens}
onChange={(event) => setContextWindowTokens(event.target.value)}
/>
<div> <div>
<div className="num mono">{rateLimit.requestCount} / {rateLimit.monthlyRequestLimit}</div> <div className="num mono">{rateLimit.requestCount} / {rateLimit.monthlyRequestLimit}</div>
<button type="button" className="button small" aria-label={t("Save {label} policy", { label: provider.label })} onClick={savePolicy}> <button type="button" className="button small" aria-label={t("Save {label} policy", { label: provider.label })} onClick={savePolicy}>
+1
View File
@@ -61,6 +61,7 @@ export interface UpdateProviderPolicyInput {
backgroundEnabled: boolean; backgroundEnabled: boolean;
monthlyRequestLimit: number; monthlyRequestLimit: number;
workspaceScope: "organization" | "workspace"; workspaceScope: "organization" | "workspace";
contextWindowTokens: number;
} }
export interface CreateProviderResponseInput { export interface CreateProviderResponseInput {
+2
View File
@@ -19,6 +19,8 @@ export interface ResolveAgentSkillsInput {
orgId: string; orgId: string;
agentId: string; agentId: string;
runId: string; runId: string;
/** Token budget for the compiled skills. */
maxTokens: number;
} }
export interface RunSkillTestsInput { export interface RunSkillTestsInput {
+2
View File
@@ -284,6 +284,8 @@ export interface ProviderRecord {
backgroundEnabled: boolean; backgroundEnabled: boolean;
monthlyRequestLimit: number; monthlyRequestLimit: number;
workspaceScope: string; workspaceScope: string;
/** Model context window the run budget is computed against. */
contextWindowTokens: number;
createdAt: string; createdAt: string;
} }
+1 -1
View File
@@ -98,7 +98,7 @@ describe("collaboration integration flow", () => {
revisionId = skills.importDirectory({ orgId, directoryPath: createSkillDirectory(), versionSemver: "1.0.0" }); revisionId = skills.importDirectory({ orgId, directoryPath: createSkillDirectory(), versionSemver: "1.0.0" });
skillId = (connection.db.prepare("select skill_id from skill_revisions where id = ?").get(revisionId) as { skill_id: string }).skill_id; skillId = (connection.db.prepare("select skill_id from skill_revisions where id = ?").get(revisionId) as { skill_id: string }).skill_id;
skills.bind({ orgId, agentId, skillId, revisionId }); skills.bind({ orgId, agentId, skillId, revisionId });
skills.resolveRuntimeSnapshot({ orgId, agentId, runId: `run_${randomUUID().replaceAll("-", "")}` }); skills.resolveRuntimeSnapshot({ orgId, agentId, runId: `run_${randomUUID().replaceAll("-", "")}`, maxTokens: 44_800 });
suiteId = (connection.db.prepare("select id from eval_suites where org_id = ? and key = ?") suiteId = (connection.db.prepare("select id from eval_suites where org_id = ? and key = ?")
.get(orgId, EVAL_SUITE_KEY.INTEGRATION) as { id: string }).id; .get(orgId, EVAL_SUITE_KEY.INTEGRATION) as { id: string }).id;
evalRunId = sdlc.runEvalSuite({ orgId, suiteId }); evalRunId = sdlc.runEvalSuite({ orgId, suiteId });
+16 -2
View File
@@ -65,7 +65,7 @@ function call(args: Record<string, unknown>): ToolCall {
return { id: `call_${randomUUID()}`, name: "complete_goal", arguments: args }; return { id: `call_${randomUUID()}`, name: "complete_goal", arguments: args };
} }
function argsFor(h: Harness, agentId: string, auditor: Auditor, toolCall: ToolCall): GoalCompletionArgs { function argsFor(h: Harness, agentId: string, auditor: Auditor, toolCall: ToolCall, inputBudgetTokens = 112_000): GoalCompletionArgs {
const ctx: AgentToolContext = { const ctx: AgentToolContext = {
orgId: h.orgId, orgId: h.orgId,
agent: { id: agentId, modelProviderId: h.providerId, modelId: "gpt-5.4" }, agent: { id: agentId, modelProviderId: h.providerId, modelId: "gpt-5.4" },
@@ -73,7 +73,7 @@ function argsFor(h: Harness, agentId: string, auditor: Auditor, toolCall: ToolCa
taskGoalId: h.goalId, taskId: null, sourceThreadId: null, taskGoalId: h.goalId, taskId: null, sourceThreadId: null,
workspace: { id: "wsp_audit", localPath: workspaceRoot, writeAccess: true }, workspace: { id: "wsp_audit", localPath: workspaceRoot, writeAccess: true },
}; };
return { deps: h.deps, ctx, providers: { runToolTurn: auditor }, providerId: h.providerId, workspaceId: null, call: toolCall }; return { deps: h.deps, ctx, providers: { runToolTurn: auditor }, providerId: h.providerId, workspaceId: null, inputBudgetTokens, call: toolCall };
} }
function goalStatus(h: Harness): string { function goalStatus(h: Harness): string {
@@ -265,4 +265,18 @@ describe("company_status", () => {
expect(output).toContain("Ask the CEO"); expect(output).toContain("Ask the CEO");
h.connection.close(); h.connection.close();
}); });
test("an audit that outgrows the context budget fails as an error instead of sending an oversized request", async () => {
const h = createHarness();
let calls = 0;
const auditor = async (): Promise<AuditorTurn> => {
calls += 1;
return { text: "", toolCalls: [], rawResponseJson: "{}", requestId: "r" };
};
const output = await completeGoalTool(argsFor(h, h.ceoId, auditor, call({ evidence: "证".repeat(5_000) }), 3_000));
expect(output).toMatch(/^ERROR: This run needs ~\d+ input tokens but the provider's context window leaves 3000/);
expect(calls).toBe(0);
expect(goalStatus(h)).toBe("active");
h.connection.close();
});
}); });
+4 -4
View File
@@ -92,7 +92,7 @@ describe("agent run loop (real tool dispatch)", () => {
tools: { db: connection.db, organization, chat: new ChatService(connection.db) }, tools: { db: connection.db, organization, chat: new ChatService(connection.db) },
onToolResults: () => { toolResultNotifications += 1; }, onToolResults: () => { toolResultNotifications += 1; },
}, },
{ orgId, providerId, workspaceId, instructions: "You are the CEO.", initialUserTurn: "Start the company.", toolContext }, { orgId, providerId, inputBudgetTokens: 112_000, workspaceId, instructions: "You are the CEO.", initialUserTurn: "Start the company.", toolContext },
); );
expect(result.iterations).toBe(4); expect(result.iterations).toBe(4);
@@ -147,7 +147,7 @@ describe("agent run loop (real tool dispatch)", () => {
]; ];
const result = await runAgentLoop( const result = await runAgentLoop(
{ providers: fakeProvider(script, connection.db), tools: { db: connection.db, organization, chat: new ChatService(connection.db) } }, { providers: fakeProvider(script, connection.db), tools: { db: connection.db, organization, chat: new ChatService(connection.db) } },
{ orgId, providerId, workspaceId: null, instructions: "i", initialUserTurn: "u", toolContext: ctx }, { orgId, providerId, inputBudgetTokens: 112_000, workspaceId: null, instructions: "i", initialUserTurn: "u", toolContext: ctx },
); );
// The tool returns an error string to the model; nothing is written outside the root. // The tool returns an error string to the model; nothing is written outside the root.
expect(existsSync(join(workspaceRoot, "..", "escape.txt"))).toBe(false); expect(existsSync(join(workspaceRoot, "..", "escape.txt"))).toBe(false);
@@ -184,7 +184,7 @@ describe("agent run loop (real tool dispatch)", () => {
await runAgentLoop( await runAgentLoop(
{ providers: fakeProvider(script, connection.db), tools: { db: connection.db, organization, chat: new ChatService(connection.db) } }, { providers: fakeProvider(script, connection.db), tools: { db: connection.db, organization, chat: new ChatService(connection.db) } },
{ orgId, providerId, workspaceId: null, instructions: "i", initialUserTurn: "u", toolContext: ctx }, { orgId, providerId, inputBudgetTokens: 112_000, workspaceId: null, instructions: "i", initialUserTurn: "u", toolContext: ctx },
); );
const alex = connection.db.prepare("select id from agents where org_id = ? and name = 'Alex Rivera'").get(orgId) as { id: string }; const alex = connection.db.prepare("select id from agents where org_id = ? and name = 'Alex Rivera'").get(orgId) as { id: string };
@@ -230,7 +230,7 @@ describe("agent run loop (real tool dispatch)", () => {
}; };
await expect(runAgentLoop( await expect(runAgentLoop(
{ providers: fakeProvider([{ text: "", toolCalls: [] }], connection.db), tools: { db: connection.db, organization, chat: new ChatService(connection.db) } }, { providers: fakeProvider([{ text: "", toolCalls: [] }], connection.db), tools: { db: connection.db, organization, chat: new ChatService(connection.db) } },
{ orgId, providerId, workspaceId: null, instructions: "i", initialUserTurn: "u", toolContext: ctx }, { orgId, providerId, inputBudgetTokens: 112_000, workspaceId: null, instructions: "i", initialUserTurn: "u", toolContext: ctx },
)).rejects.toThrow(/empty turn/); )).rejects.toThrow(/empty turn/);
connection.close(); connection.close();
}); });
+9 -6
View File
@@ -6,7 +6,10 @@ import { describe, expect, test } from "vitest";
import { DatabaseConnection } from "../../src/main/database/connection"; import { DatabaseConnection } from "../../src/main/database/connection";
import { OrganizationService } from "../../src/main/services/organization-service"; import { OrganizationService } from "../../src/main/services/organization-service";
import { ChatService } from "../../src/main/services/chat-service"; import { ChatService } from "../../src/main/services/chat-service";
import { SkillService, MAX_COMPILED_PROMPT_CHARS } from "../../src/main/services/skill-service"; import { SkillService } from "../../src/main/services/skill-service";
import { skillBudgetTokens } from "../../src/main/services/context-budget";
const SKILL_TOKENS = skillBudgetTokens(128_000);
import { importStagedSkills, parseFrontmatter, stageBuiltinSkillsLibrary } from "../../src/main/services/builtin-skills"; import { importStagedSkills, parseFrontmatter, stageBuiltinSkillsLibrary } from "../../src/main/services/builtin-skills";
import { dispatchAgentTool, type AgentToolContext } from "../../src/main/services/agent-tools"; import { dispatchAgentTool, type AgentToolContext } from "../../src/main/services/agent-tools";
@@ -141,7 +144,7 @@ describe("built-in skills", () => {
// A third, oversized flat skill to prove the compiled-prompt cap. // A third, oversized flat skill to prove the compiled-prompt cap.
writeFileSync( writeFileSync(
join(h.sourceRoot, "engineering", "engineering-epic-novelist.md"), join(h.sourceRoot, "engineering", "engineering-epic-novelist.md"),
`---\nname: Epic Novelist\ndescription: too big to pack\n---\n\n${"x".repeat(MAX_COMPILED_PROMPT_CHARS + 10)}\n`, `---\nname: Epic Novelist\ndescription: too big to pack\n---\n\n${"x".repeat(SKILL_TOKENS * 3 + 30)}\n`,
"utf8", "utf8",
); );
const staged = stageBuiltinSkillsLibrary(h.sourceRoot, h.libraryRoot); const staged = stageBuiltinSkillsLibrary(h.sourceRoot, h.libraryRoot);
@@ -175,17 +178,17 @@ describe("built-in skills", () => {
expect(hired).toContain("Unknown skills skipped: no-such-skill"); expect(hired).toContain("Unknown skills skipped: no-such-skill");
const danaId = (h.connection.db.prepare("select id from agents where org_id = ? and name = 'Dana Lee'").get(h.orgId) as { id: string }).id; const danaId = (h.connection.db.prepare("select id from agents where org_id = ? and name = 'Dana Lee'").get(h.orgId) as { id: string }).id;
const danaSnapshot = h.connection.transaction(() => h.skills.resolveRuntimeSnapshot({ orgId: h.orgId, agentId: danaId, runId: "run_dana" })); const danaSnapshot = h.connection.transaction(() => h.skills.resolveRuntimeSnapshot({ orgId: h.orgId, agentId: danaId, runId: "run_dana", maxTokens: SKILL_TOKENS }));
const danaPrompt = (h.connection.db.prepare("select compiled_prompt from skill_runtime_snapshots where id = ?").get(danaSnapshot) as { compiled_prompt: string }).compiled_prompt; const danaPrompt = (h.connection.db.prepare("select compiled_prompt from skill_runtime_snapshots where id = ?").get(danaSnapshot) as { compiled_prompt: string }).compiled_prompt;
expect(danaPrompt.indexOf("# Skill: humanizer")).toBe(0); expect(danaPrompt.indexOf("# Skill: humanizer")).toBe(0);
expect(danaPrompt).toContain("# Skill: Backend Architect"); expect(danaPrompt).toContain("# Skill: Backend Architect");
expect(danaPrompt).toContain("Skills omitted for size"); expect(danaPrompt).toContain("Skills omitted for size");
expect(danaPrompt).toContain("Epic Novelist"); expect(danaPrompt).toContain("Epic Novelist");
expect(danaPrompt.length).toBeLessThan(MAX_COMPILED_PROMPT_CHARS + 5_000); expect(danaPrompt.length).toBeLessThan(SKILL_TOKENS * 3 + 5_000);
// The CEO carries humanizer too - forced by resolution, not by binding. // The CEO carries humanizer too - forced by resolution, not by binding.
expect(h.skills.hasRuntimeSkills(h.orgId, ceoId)).toBe(true); expect(h.skills.hasRuntimeSkills(h.orgId, ceoId)).toBe(true);
const ceoSnapshot = h.connection.transaction(() => h.skills.resolveRuntimeSnapshot({ orgId: h.orgId, agentId: ceoId, runId: "run_ceo" })); const ceoSnapshot = h.connection.transaction(() => h.skills.resolveRuntimeSnapshot({ orgId: h.orgId, agentId: ceoId, runId: "run_ceo", maxTokens: SKILL_TOKENS }));
const ceoPrompt = (h.connection.db.prepare("select compiled_prompt from skill_runtime_snapshots where id = ?").get(ceoSnapshot) as { compiled_prompt: string }).compiled_prompt; const ceoPrompt = (h.connection.db.prepare("select compiled_prompt from skill_runtime_snapshots where id = ?").get(ceoSnapshot) as { compiled_prompt: string }).compiled_prompt;
expect(ceoPrompt).toContain("# Skill: humanizer"); expect(ceoPrompt).toContain("# Skill: humanizer");
expect(ceoPrompt).not.toContain("# Skill: Backend Architect"); expect(ceoPrompt).not.toContain("# Skill: Backend Architect");
@@ -210,7 +213,7 @@ describe("built-in skills", () => {
}); });
}); });
const promptFor = (runId: string) => { const promptFor = (runId: string) => {
const snapshotId = h.connection.transaction(() => h.skills.resolveRuntimeSnapshot({ orgId: h.orgId, agentId, runId })); const snapshotId = h.connection.transaction(() => h.skills.resolveRuntimeSnapshot({ orgId: h.orgId, agentId, runId, maxTokens: SKILL_TOKENS }));
return (h.connection.db.prepare("select compiled_prompt from skill_runtime_snapshots where id = ?").get(snapshotId) as { compiled_prompt: string }).compiled_prompt; return (h.connection.db.prepare("select compiled_prompt from skill_runtime_snapshots where id = ?").get(snapshotId) as { compiled_prompt: string }).compiled_prompt;
}; };
+97
View File
@@ -0,0 +1,97 @@
import { randomUUID } from "node:crypto";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { describe, expect, test } from "vitest";
import { estimateTokens, inputBudgetTokens, skillBudgetTokens } from "../../src/main/services/context-budget";
import { runAgentLoop } from "../../src/main/services/agent-run-loop";
import { DatabaseConnection } from "../../src/main/database/connection";
import { OrganizationService } from "../../src/main/services/organization-service";
import { ChatService } from "../../src/main/services/chat-service";
import { IntegrationKeyStore } from "../../src/main/services/integration-key-store";
import { ProviderService } from "../../src/main/services/provider-service";
import type { AgentTurnMessage, AgentTurnResult } from "../../src/main/services/provider-tool-turn";
import { AGENT_TOOLS } from "../../src/main/services/agent-tools";
import { WORKSPACE_TOOLS } from "../../src/main/services/agent-workspace-tools";
import { WEB_TOOLS } from "../../src/main/services/agent-web-tools";
const FIXED_TOKENS = estimateTokens("i") + estimateTokens(JSON.stringify([...AGENT_TOOLS, ...WORKSPACE_TOOLS, ...WEB_TOOLS]));
describe("token estimates", () => {
test("count CJK per character and other text per three characters", () => {
expect(estimateTokens("abcdef")).toBe(2);
expect(estimateTokens("abcdefg")).toBe(3);
expect(estimateTokens("技能规则")).toBe(4);
expect(estimateTokens("规则 rule")).toBe(4);
expect(inputBudgetTokens(128_000)).toBe(112_000);
expect(skillBudgetTokens(128_000)).toBe(44_800);
});
});
async function loop(budget: number, sent: AgentTurnMessage[][]) {
const connection = new DatabaseConnection(join(tmpdir(), `ac-budget-${randomUUID()}.db`));
const organization = new OrganizationService(connection.db);
let orgId = "";
let agentId = "";
connection.transaction(() => {
orgId = organization.createOrganization({ organizationName: "B", employeeName: "Y", mission: "m", goal: "g" });
agentId = organization.createAgent({ orgId, name: "Ana", role: "Analyst", department: "Operations", modelProviderId: null, modelId: null, budgetCents: 1000, managerAgentId: null });
});
const chat = new ChatService(connection.db);
const general = connection.db.prepare("select id from chat_spaces where org_id = ? and name = 'general'").get(orgId) as { id: string };
chat.sendMessage({ orgId, spaceId: general.id, threadId: null, senderType: "agent", senderId: agentId, bodyMd: "y".repeat(9000), attachmentFileIds: [] });
let turn = 0;
try {
return await runAgentLoop(
{
providers: {
integrationKeys: new IntegrationKeyStore(connection.db),
runToolTurn: async (input): Promise<AgentTurnResult & { requestId: string }> => {
sent.push(input.messages.map((message) => ({ ...message })));
turn += 1;
return turn <= 2
? { text: "", toolCalls: [{ id: `c${turn}`, name: "read_messages", arguments: { channel: "general" } }], rawResponseJson: "{}", requestId: `r${turn}` }
: { text: "done", toolCalls: [], rawResponseJson: "{}", requestId: `r${turn}` };
},
},
tools: { db: connection.db, organization, chat },
},
{
orgId, providerId: "p", inputBudgetTokens: budget, workspaceId: null, instructions: "i", initialUserTurn: "go",
toolContext: { orgId, agent: { id: agentId, modelProviderId: null, modelId: null }, taskProjectId: null, taskGoalId: null, taskId: null, sourceThreadId: null, workspace: null },
},
);
} finally {
connection.close();
}
}
describe("run context budget", () => {
test("over budget, older tool results are elided before the next call", async () => {
const sent: AgentTurnMessage[][] = [];
const result = await loop(FIXED_TOKENS + 5_000, sent);
expect(result.finalText).toBe("done");
const outputs = sent[2].flatMap((message) => (message.role === "tool" ? message.results.map((item) => item.output) : []));
expect(outputs[0]).toMatch(/^\[Earlier result of read_messages elided/);
expect(outputs[1]).toContain("y".repeat(9000));
});
test("a request that cannot fit even after compaction fails loudly with the numbers", async () => {
await expect(loop(2_000, [])).rejects.toThrow(/This run needs ~\d+ input tokens but the provider's context window leaves 2000/);
});
test("the provider context window has a floor", () => {
const connection = new DatabaseConnection(join(tmpdir(), `ac-window-${randomUUID()}.db`));
const organization = new OrganizationService(connection.db);
let orgId = "";
connection.transaction(() => {
orgId = organization.createOrganization({ organizationName: "W", employeeName: "Y", 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;
const providers = new ProviderService(connection.db);
const policy = { orgId, providerId, dataPolicy: "standard" as const, backgroundEnabled: false, monthlyRequestLimit: 100, workspaceScope: "organization" as const };
expect(() => providers.updatePolicy({ ...policy, contextWindowTokens: 8_000 })).toThrow("Context window must be at least 24000 tokens.");
providers.updatePolicy({ ...policy, contextWindowTokens: 200_000 });
expect(connection.db.prepare("select context_window_tokens from providers where id = ?").get(providerId)).toEqual({ context_window_tokens: 200_000 });
connection.close();
});
});
+4 -4
View File
@@ -24,7 +24,7 @@ describe("domain services", () => {
const connection = createConnection(); const connection = createConnection();
const rows = connection.db.prepare("select version from schema_migrations order by version asc").all() as Array<{ version: number }>; 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, 59, 60, 61]); 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, 60, 61, 62]);
connection.close(); connection.close();
}); });
@@ -186,7 +186,7 @@ describe("domain services", () => {
skills.bind({ orgId, agentId, skillId: skill.id, revisionId }); skills.bind({ orgId, agentId, skillId: skill.id, revisionId });
rmSync(skillDir, { recursive: true, force: true }); rmSync(skillDir, { recursive: true, force: true });
skills.runTests({ orgId, revisionId }); skills.runTests({ orgId, revisionId });
skills.resolveRuntimeSnapshot({ orgId, agentId, runId: "run-review-1" }); skills.resolveRuntimeSnapshot({ orgId, agentId, runId: "run-review-1", maxTokens: 44_800 });
}); });
// Catalogue metadata ships in the snapshot; skill internals stay in the // Catalogue metadata ships in the snapshot; skill internals stay in the
@@ -247,10 +247,10 @@ describe("domain services", () => {
.get(orgA, "review-routes") as { id: string }).id; .get(orgA, "review-routes") as { id: string }).id;
expect(() => skills.bind({ orgId: orgA, agentId: agentB, skillId, revisionId })).toThrow("Agent was not found."); expect(() => skills.bind({ orgId: orgA, agentId: agentB, skillId, revisionId })).toThrow("Agent was not found.");
skills.bind({ orgId: orgA, agentId: agentA, skillId, revisionId }); skills.bind({ orgId: orgA, agentId: agentA, skillId, revisionId });
const firstResolve = skills.resolveRuntimeSnapshot({ orgId: orgA, agentId: agentA, runId: "run-review-1" }); const firstResolve = skills.resolveRuntimeSnapshot({ orgId: orgA, agentId: agentA, runId: "run-review-1", maxTokens: 44_800 });
// Idempotent per run: a recovered run reuses its frozen snapshot // Idempotent per run: a recovered run reuses its frozen snapshot
// instead of failing on the uniqueness guard. // instead of failing on the uniqueness guard.
expect(skills.resolveRuntimeSnapshot({ orgId: orgA, agentId: agentA, runId: "run-review-1" })).toBe(firstResolve); expect(skills.resolveRuntimeSnapshot({ orgId: orgA, agentId: agentA, runId: "run-review-1", maxTokens: 44_800 })).toBe(firstResolve);
expect(connection.db.prepare("select count(*) as n from skill_runtime_snapshots where run_id = 'run-review-1'").get()) expect(connection.db.prepare("select count(*) as n from skill_runtime_snapshots where run_id = 'run-review-1'").get())
.toEqual({ n: 1 }); .toEqual({ n: 1 });
}); });
+1 -1
View File
@@ -85,7 +85,7 @@ describe("operations service", () => {
dataPolicy: PROVIDER_DATA_POLICY.STANDARD, dataPolicy: PROVIDER_DATA_POLICY.STANDARD,
backgroundEnabled: true, backgroundEnabled: true,
monthlyRequestLimit: 10, monthlyRequestLimit: 10,
workspaceScope: "organization", workspaceScope: "organization", contextWindowTokens: 128_000,
}); });
operations.setFeatureFlag({ orgId, flagId, enabled: false }); operations.setFeatureFlag({ orgId, flagId, enabled: false });
}); });
@@ -52,7 +52,7 @@ describe("provider background service", () => {
dataPolicy: PROVIDER_DATA_POLICY.STANDARD, dataPolicy: PROVIDER_DATA_POLICY.STANDARD,
backgroundEnabled: true, backgroundEnabled: true,
monthlyRequestLimit: 10, monthlyRequestLimit: 10,
workspaceScope: "organization", workspaceScope: "organization", contextWindowTokens: 128_000,
}); });
}); });
@@ -115,7 +115,7 @@ describe("provider background service", () => {
dataPolicy: PROVIDER_DATA_POLICY.STANDARD, dataPolicy: PROVIDER_DATA_POLICY.STANDARD,
backgroundEnabled: true, backgroundEnabled: true,
monthlyRequestLimit: 10, monthlyRequestLimit: 10,
workspaceScope: "organization", workspaceScope: "organization", contextWindowTokens: 128_000,
}); });
}); });
@@ -152,7 +152,7 @@ describe("provider background service", () => {
dataPolicy: PROVIDER_DATA_POLICY.STANDARD, dataPolicy: PROVIDER_DATA_POLICY.STANDARD,
backgroundEnabled: true, backgroundEnabled: true,
monthlyRequestLimit: 10, monthlyRequestLimit: 10,
workspaceScope: "organization", workspaceScope: "organization", contextWindowTokens: 128_000,
}); });
}); });
+3 -3
View File
@@ -448,7 +448,7 @@ describe("provider service", () => {
dataPolicy: PROVIDER_DATA_POLICY.STANDARD, dataPolicy: PROVIDER_DATA_POLICY.STANDARD,
backgroundEnabled: false, backgroundEnabled: false,
monthlyRequestLimit: 1, monthlyRequestLimit: 1,
workspaceScope: "organization", workspaceScope: "organization", contextWindowTokens: 128_000,
}); });
}); });
await providers.createResponse({ await providers.createResponse({
@@ -524,7 +524,7 @@ describe("provider service", () => {
dataPolicy: PROVIDER_DATA_POLICY.STANDARD, dataPolicy: PROVIDER_DATA_POLICY.STANDARD,
backgroundEnabled: false, backgroundEnabled: false,
monthlyRequestLimit: 10, monthlyRequestLimit: 10,
workspaceScope: "workspace", workspaceScope: "workspace", contextWindowTokens: 128_000,
}); });
}); });
@@ -582,7 +582,7 @@ describe("provider service", () => {
dataPolicy: PROVIDER_DATA_POLICY.ZERO_DATA_RETENTION, dataPolicy: PROVIDER_DATA_POLICY.ZERO_DATA_RETENTION,
backgroundEnabled: false, backgroundEnabled: false,
monthlyRequestLimit: 10, monthlyRequestLimit: 10,
workspaceScope: "organization", workspaceScope: "organization", contextWindowTokens: 128_000,
}); });
}); });
+1 -1
View File
@@ -72,7 +72,7 @@ describe("tool history compaction", () => {
tools: { db: connection.db, organization, chat }, tools: { db: connection.db, organization, chat },
}, },
{ {
orgId, providerId: "p", workspaceId: null, instructions: "i", initialUserTurn: "go", orgId, providerId: "p", inputBudgetTokens: 112_000, workspaceId: null, instructions: "i", initialUserTurn: "go",
toolContext: { orgId, agent: { id: agentId, modelProviderId: null, modelId: null }, taskProjectId: null, taskGoalId: null, taskId: null, sourceThreadId: null, workspace: null }, toolContext: { orgId, agent: { id: agentId, modelProviderId: null, modelId: null }, taskProjectId: null, taskGoalId: null, taskId: null, sourceThreadId: null, workspace: null },
}, },
); );
+2 -2
View File
@@ -408,7 +408,7 @@ describe("task execution service", () => {
dataPolicy: PROVIDER_DATA_POLICY.STANDARD, dataPolicy: PROVIDER_DATA_POLICY.STANDARD,
backgroundEnabled: false, backgroundEnabled: false,
monthlyRequestLimit: 5, monthlyRequestLimit: 5,
workspaceScope: "organization", workspaceScope: "organization", contextWindowTokens: 128_000,
}); });
const agentId = organization.createAgent({ const agentId = organization.createAgent({
orgId, orgId,
@@ -452,7 +452,7 @@ describe("task execution service", () => {
dataPolicy: PROVIDER_DATA_POLICY.STANDARD, dataPolicy: PROVIDER_DATA_POLICY.STANDARD,
backgroundEnabled: false, backgroundEnabled: false,
monthlyRequestLimit: 50, monthlyRequestLimit: 50,
workspaceScope: "organization", workspaceScope: "organization", contextWindowTokens: 128_000,
})); }));
const enqueued = connection.transaction(() => execution.enqueueHeartbeatRuns()); const enqueued = connection.transaction(() => execution.enqueueHeartbeatRuns());
expect(enqueued).toHaveLength(1); expect(enqueued).toHaveLength(1);