From 483d04838189df088b71e5fb78e1cce54a6124a9 Mon Sep 17 00:00:00 2001 From: hetaoBackend <45447813+hetaoBackend@users.noreply.github.com> Date: Fri, 9 Oct 2026 14:12:29 +0800 Subject: [PATCH] perf: drop always-on tools from the fixed prompt - Only wire website_deploy (and its deploy-website / edit-deployed-website skills) when a MiniMax account access token is present. - Only expose goal tools when the session has a goal: none for no goal, get_goal only once complete, both otherwise. - Cap per-skill catalog descriptions at 300 code points (was 1024); the 5000-token total cap and protected skills are unchanged. --- .../src/agent/feature-owned-skills.ts | 18 +++++++ .../api/hosted-agent-skill-capabilities.ts | 6 ++- .../src/api/local-native-tools.ts | 25 +++++++--- packages/local-runtime/src/skills/catalog.ts | 2 +- .../src/thread-goal/lifecycle.ts | 20 ++++++-- ...eature-owned-skills-website-deploy.test.ts | 47 +++++++++++++++++++ .../test/unit/local-native-tools.test.ts | 30 ++++++++++++ .../skills-catalog-description-cap.test.ts | 28 +++++++++++ .../goal-lifecycle-runtime-tools.test.ts | 47 +++++++++++++++++++ .../host-integration-settlement.test.ts | 14 +++++- release/public-source.json | 3 ++ test/vitest-suites.json | 3 ++ 12 files changed, 229 insertions(+), 14 deletions(-) create mode 100644 packages/local-runtime/test/unit/feature-owned-skills-website-deploy.test.ts create mode 100644 packages/local-runtime/test/unit/skills-catalog-description-cap.test.ts create mode 100644 packages/local-runtime/test/unit/thread-goal/goal-lifecycle-runtime-tools.test.ts diff --git a/packages/local-runtime/src/agent/feature-owned-skills.ts b/packages/local-runtime/src/agent/feature-owned-skills.ts index a8f83a744..36cf82c11 100644 --- a/packages/local-runtime/src/agent/feature-owned-skills.ts +++ b/packages/local-runtime/src/agent/feature-owned-skills.ts @@ -13,6 +13,13 @@ export function resolveFeatureAwareBuiltinSkillNames( gates: { cuModeActive: boolean; resumeCodexAvailable?: boolean; + /** + * Whether the `website_deploy` tool is wired for this Turn. It publishes + * through the managed MiniMax account gateway, so `false` (no account + * session) also drops the Skills that only make sense with that tool. + * Left undefined, the capability config alone decides. + */ + websiteDeployAvailable?: boolean; miniappAvailable?: boolean; canonicalRole?: string; builtinAgent?: boolean; @@ -44,6 +51,10 @@ export function resolveFeatureAwareBuiltinSkillNames( if (gates.resumeCodexAvailable === true && isAgentBuiltinToolEnabled(capabilities, 'bash')) { selected.add('resume-codex'); } + if (gates.websiteDeployAvailable === false) { + selected.delete('deploy-website'); + selected.delete('edit-deployed-website'); + } for (const skillName of gates.disabledSkillNames ?? []) selected.delete(skillName); return [...selected]; } @@ -96,3 +107,10 @@ function hasTools( ): boolean { return toolNames.every((toolName) => isAgentBuiltinToolEnabled(capabilities, toolName)); } + +/** True when the host holds a MiniMax account session that can call the managed gateway. */ +export function hasManagedAccountSession( + authContext: { accessToken?: string } | undefined, +): boolean { + return Boolean(authContext?.accessToken?.trim()); +} diff --git a/packages/local-runtime/src/api/hosted-agent-skill-capabilities.ts b/packages/local-runtime/src/api/hosted-agent-skill-capabilities.ts index 232a6451a..416fc8e8b 100644 --- a/packages/local-runtime/src/api/hosted-agent-skill-capabilities.ts +++ b/packages/local-runtime/src/api/hosted-agent-skill-capabilities.ts @@ -1,6 +1,9 @@ import type { ResolvedAgentCapabilities } from '@mavis/config'; -import { resolveFeatureAwareBuiltinSkillNames } from '../agent/feature-owned-skills.js'; +import { + hasManagedAccountSession, + resolveFeatureAwareBuiltinSkillNames, +} from '../agent/feature-owned-skills.js'; import type { LocalRuntimeSkillScope, LocalSkillCatalogScope } from '../skills/skill-service.js'; import { applyHostedCapabilityRestrictions } from './hosted-agent-capability-restrictions.js'; import { resolveHostedCuModeActive } from './hosted-agent-turn-runtime-facts.js'; @@ -90,6 +93,7 @@ function resolveHostedSkillScope( builtinAgent: scope.agentPolicy.builtinAgent, disabledSkillNames: restrictions.disabledBuiltinSkillNames, resumeCodexAvailable: restrictions.resumeCodexAvailable === true, + websiteDeployAvailable: hasManagedAccountSession(host.authContextGetter?.()), }), cuModeActive, }; diff --git a/packages/local-runtime/src/api/local-native-tools.ts b/packages/local-runtime/src/api/local-native-tools.ts index 943fd3e02..4a065e68b 100644 --- a/packages/local-runtime/src/api/local-native-tools.ts +++ b/packages/local-runtime/src/api/local-native-tools.ts @@ -27,7 +27,10 @@ import { type AgentBuiltinSkillId, type ResolvedAgentCapabilities, } from "@mavis/config"; -import { resolveFeatureAwareBuiltinSkillNames } from "../agent/feature-owned-skills.js"; +import { + hasManagedAccountSession, + resolveFeatureAwareBuiltinSkillNames, +} from "../agent/feature-owned-skills.js"; import type { ModuleMetricsReporter } from "../common/metrics.js"; import { CU_DESKTOP_SKILL_NAME } from "../cu/gate.js"; import type { LocalMemoryFacade } from "../memory/local-memory-facade.js"; @@ -124,6 +127,7 @@ export function buildLocalNativeRuntimeTools(input: { ...(input.miniappAvailable === true ? { miniappAvailable: true } : {}), disabledSkillNames: input.disabledBuiltinSkillNames, resumeCodexAvailable: input.resumeCodexAvailable === true, + websiteDeployAvailable: hasManagedAccountSession(input.authContext), }); const nativeTools = [ ...buildLocalToolRegistry({ @@ -228,12 +232,19 @@ export function buildLocalNativeRuntimeTools(input: { mavisCronAdapter: input.mavisCronAdapter, mavisMcpAdapter: input.mavisMcpAdapter, mavisSessionAdapter: input.mavisSessionAdapter, - websiteDeployAdapter: new LocalWebsiteDeployClient({ - ...(input.authContext ? { authContext: input.authContext } : {}), - ...(input.dataDir ? { dataDir: input.dataDir } : {}), - ...(input.fetchImpl ? { fetchImpl: input.fetchImpl } : {}), - routingContextGetter: input.routingContextGetter, - }), + // website_deploy publishes through the managed MiniMax account gateway, + // so it is only useful (and only worth its ~860-token schema plus the + // dependent skills) when an account access token is present. + ...(hasManagedAccountSession(input.authContext) + ? { + websiteDeployAdapter: new LocalWebsiteDeployClient({ + authContext: input.authContext, + ...(input.dataDir ? { dataDir: input.dataDir } : {}), + ...(input.fetchImpl ? { fetchImpl: input.fetchImpl } : {}), + routingContextGetter: input.routingContextGetter, + }), + } + : {}), codeReviewAdapter: input.codeReviewAdapter, }).values(), ]; diff --git a/packages/local-runtime/src/skills/catalog.ts b/packages/local-runtime/src/skills/catalog.ts index 79b614891..093e19e38 100644 --- a/packages/local-runtime/src/skills/catalog.ts +++ b/packages/local-runtime/src/skills/catalog.ts @@ -1,7 +1,7 @@ import { createDefaultTokenEstimator, type TokenEstimator } from '@mavis/context-manager'; export const LOCAL_SKILL_CATALOG_MAX_BUDGET_TOKENS = 5_000; -export const LOCAL_SKILL_DESCRIPTION_MAX_CODE_POINTS = 1_024; +export const LOCAL_SKILL_DESCRIPTION_MAX_CODE_POINTS = 300; const PROTECTED_SKILL_NAMES = new Set(['mcode-tools-master', 'minimax-code-product']); const PARTIAL_DESCRIPTION_NOTICE = diff --git a/packages/local-runtime/src/thread-goal/lifecycle.ts b/packages/local-runtime/src/thread-goal/lifecycle.ts index 7862f905e..9deb2ac13 100644 --- a/packages/local-runtime/src/thread-goal/lifecycle.ts +++ b/packages/local-runtime/src/thread-goal/lifecycle.ts @@ -342,12 +342,24 @@ export class GoalLifecycle { async runtimeToolsFor( disabled: boolean, - _sessionId: string, + sessionId: string, ): Promise[]> { if (!this.deps.runtime.isEnabled() || disabled) return []; - return buildThreadGoalRuntimeTools(this.deps.store(), false, this.deps.signalCollector, { - updateTokenBudget: (context, input) => this.updateTokenBudget(context, input), - }); + // Goals are created by the user (`/goal` / REST), never by the model, so + // a session without a goal record has nothing for the model to read or + // update. Skip the ~800-token tool schemas in that case. + const goal = await this.deps.store().getBySession(sessionId); + if (!goal) return []; + const tools = buildThreadGoalRuntimeTools( + this.deps.store(), + false, + this.deps.signalCollector, + { updateTokenBudget: (context, input) => this.updateTokenBudget(context, input) }, + ); + // A completed goal can still be inspected but no longer updated. + return goal.status === 'complete' + ? tools.filter((tool) => tool.def.name === 'get_goal') + : tools; } tallyTurnUsage(messages: PiAgentMessage[]): number { diff --git a/packages/local-runtime/test/unit/feature-owned-skills-website-deploy.test.ts b/packages/local-runtime/test/unit/feature-owned-skills-website-deploy.test.ts new file mode 100644 index 000000000..9c835b948 --- /dev/null +++ b/packages/local-runtime/test/unit/feature-owned-skills-website-deploy.test.ts @@ -0,0 +1,47 @@ +import { resolveAgentCapabilities } from "@mavis/config"; +import { describe, expect, it } from "vitest"; + +import { + hasManagedAccountSession, + resolveFeatureAwareBuiltinSkillNames, +} from "../../src/agent/feature-owned-skills.js"; + +const gates = { cuModeActive: false }; + +describe("website deploy skill gating", () => { + const capabilities = resolveAgentCapabilities(); + + it("keeps the deploy skills when the capability config alone decides", () => { + const names = resolveFeatureAwareBuiltinSkillNames(capabilities, gates); + expect(names).toContain("deploy-website"); + expect(names).toContain("edit-deployed-website"); + }); + + it("keeps the deploy skills when website_deploy is wired", () => { + const names = resolveFeatureAwareBuiltinSkillNames(capabilities, { + ...gates, + websiteDeployAvailable: true, + }); + expect(names).toContain("deploy-website"); + expect(names).toContain("edit-deployed-website"); + }); + + it("drops the deploy skills when website_deploy is not wired", () => { + const names = resolveFeatureAwareBuiltinSkillNames(capabilities, { + ...gates, + websiteDeployAvailable: false, + }); + expect(names).not.toContain("deploy-website"); + expect(names).not.toContain("edit-deployed-website"); + expect(names).toContain("code-review"); + }); +}); + +describe("hasManagedAccountSession", () => { + it("requires a non-blank access token", () => { + expect(hasManagedAccountSession(undefined)).toBe(false); + expect(hasManagedAccountSession({})).toBe(false); + expect(hasManagedAccountSession({ accessToken: " " })).toBe(false); + expect(hasManagedAccountSession({ accessToken: "tok" })).toBe(true); + }); +}); diff --git a/packages/local-runtime/test/unit/local-native-tools.test.ts b/packages/local-runtime/test/unit/local-native-tools.test.ts index fb166822d..6ff7ff5df 100644 --- a/packages/local-runtime/test/unit/local-native-tools.test.ts +++ b/packages/local-runtime/test/unit/local-native-tools.test.ts @@ -319,6 +319,36 @@ describe('local native turn tools', () => { expect(appendMemory).not.toHaveBeenCalled(); }); + it.each([ + ['no auth context (API-key / custom-provider path)', undefined, false], + ['an auth context without an access token', { userName: 'u' }, false], + ['a blank access token', { accessToken: ' ' }, false], + ['a MiniMax account access token', { accessToken: 'account-token' }, true], + ])('only offers website_deploy (and its skills) with %s', async (_label, authContext, expected) => { + const dataDir = await mkdtemp(join(tmpdir(), 'local-native-website-deploy-')); + try { + const builtinCapabilities = resolveAgentCapabilities({}); + const { nativeTools } = await buildLocalTurnToolSources({ + toolsDisabled: false, + workspaceRoot: process.cwd(), + agentName: 'mavis', + dataDir, + sessionId: 'website-deploy-gate', + mcpService: new LocalMcpService(() => dataDir), + threadGoal: { runtimeToolsFor: () => [] } as never, + emitBusEvent: () => {}, + builtinCapabilities, + ...(authContext ? { authContext } : {}), + taskAdapter: {} as never, + taskControlAdapter: {} as never, + mavisAgentAdapter: {} as never, + }); + expect(nativeTools.map((tool) => tool.def.name).includes('website_deploy')).toBe(expected); + } finally { + await rm(dataDir, { recursive: true, force: true }); + } + }); + it('filters configurable built-in tools and only the delegation entry point', async () => { const dataDir = await mkdtemp(join(tmpdir(), 'local-native-capability-tools-')); try { diff --git a/packages/local-runtime/test/unit/skills-catalog-description-cap.test.ts b/packages/local-runtime/test/unit/skills-catalog-description-cap.test.ts new file mode 100644 index 000000000..eb68f92ba --- /dev/null +++ b/packages/local-runtime/test/unit/skills-catalog-description-cap.test.ts @@ -0,0 +1,28 @@ +import { describe, expect, it } from "vitest"; + +import { + LOCAL_SKILL_DESCRIPTION_MAX_CODE_POINTS, + renderLocalSkillsCatalogResult, +} from "../../src/skills/catalog.js"; + +describe("local skills catalog description cap", () => { + it("caps ordinary skill descriptions at 300 code points", () => { + expect(LOCAL_SKILL_DESCRIPTION_MAX_CODE_POINTS).toBe(300); + const result = renderLocalSkillsCatalogResult([ + { name: "long-skill", description: "x".repeat(1_000), builtin: true }, + ]); + expect(result.descriptionCapTruncated).toBe(true); + expect(result.catalog).toContain("x".repeat(300)); + expect(result.catalog).not.toContain("x".repeat(301)); + }); + + it("keeps descriptions at the cap intact and never trims protected skills", () => { + const result = renderLocalSkillsCatalogResult([ + { name: "fits", description: "y".repeat(300), builtin: true }, + { name: "minimax-code-product", description: "p".repeat(900), builtin: true }, + ]); + expect(result.descriptionCapTruncated).toBe(false); + expect(result.catalog).toContain("y".repeat(300)); + expect(result.catalog).toContain("p".repeat(900)); + }); +}); diff --git a/packages/local-runtime/test/unit/thread-goal/goal-lifecycle-runtime-tools.test.ts b/packages/local-runtime/test/unit/thread-goal/goal-lifecycle-runtime-tools.test.ts new file mode 100644 index 000000000..fa3c915b0 --- /dev/null +++ b/packages/local-runtime/test/unit/thread-goal/goal-lifecycle-runtime-tools.test.ts @@ -0,0 +1,47 @@ +import { describe, expect, it, vi } from "vitest"; + +import type { ThreadGoalState, ThreadGoalStatus } from "@mavis/goal"; + +import { GoalLifecycle } from "../../../src/thread-goal/lifecycle.js"; + +function lifecycleWith(goal: Partial | undefined, enabled = true) { + const getBySession = vi.fn(async () => goal as ThreadGoalState | undefined); + const lifecycle = new GoalLifecycle({ + runtime: { isEnabled: () => enabled }, + store: () => ({ getBySession }), + signalCollector: { collect: () => "accepted" }, + } as never); + return { lifecycle, getBySession }; +} + +async function names(lifecycle: GoalLifecycle): Promise { + return (await lifecycle.runtimeToolsFor(false, "sess_a")).map((tool) => tool.def.name).sort(); +} + +describe("GoalLifecycle.runtimeToolsFor", () => { + it("exposes no goal tools when the session has no goal", async () => { + const { lifecycle, getBySession } = lifecycleWith(undefined); + expect(await names(lifecycle)).toEqual([]); + expect(getBySession).toHaveBeenCalledWith("sess_a"); + }); + + it("exposes nothing when the feature is disabled, without reading the store", async () => { + const { lifecycle, getBySession } = lifecycleWith({ status: "active" }, false); + expect(await names(lifecycle)).toEqual([]); + expect(await lifecycle.runtimeToolsFor(true, "sess_a")).toEqual([]); + expect(getBySession).not.toHaveBeenCalled(); + }); + + it("exposes only get_goal for a completed goal", async () => { + const { lifecycle } = lifecycleWith({ status: "complete" }); + expect(await names(lifecycle)).toEqual(["get_goal"]); + }); + + it.each(["active", "paused", "budget_limited", "blocked", "usage_limited"])( + "exposes both tools for an unfinished %s goal", + async (status) => { + const { lifecycle } = lifecycleWith({ status }); + expect(await names(lifecycle)).toEqual(["get_goal", "update_goal"]); + }, + ); +}); diff --git a/packages/local-runtime/test/unit/thread-goal/host-integration-settlement.test.ts b/packages/local-runtime/test/unit/thread-goal/host-integration-settlement.test.ts index 666c77201..04ef9c28b 100644 --- a/packages/local-runtime/test/unit/thread-goal/host-integration-settlement.test.ts +++ b/packages/local-runtime/test/unit/thread-goal/host-integration-settlement.test.ts @@ -516,7 +516,19 @@ describe("LocalThreadGoalIntegration injected v2 Turn settlement", () => { expect(active.status).toBe("active"); }); - it.each(["active", "complete"] as const)( + it("does not expose update_goal for a complete Goal", async () => { + const { integration } = makeIntegration({ + store: makeStore({ getBySession: async () => goal({ status: "complete" }) }), + }); + + const names = (await integration.runtimeToolsFor(false, "sess_int")).map( + (tool) => tool.def.name, + ); + + expect(names).toEqual(["get_goal"]); + }); + + it.each(["active"] as const)( "rejects an unbound proposal without ending the user Turn or changing the %s Goal", async (status) => { const currentGoal = goal({ status }); diff --git a/release/public-source.json b/release/public-source.json index 7539f8cd2..e5bee0416 100644 --- a/release/public-source.json +++ b/release/public-source.json @@ -2709,6 +2709,7 @@ "packages/local-runtime/test/unit/content-safety-turn-v2.test.ts", "packages/local-runtime/test/unit/cron/once-time.test.ts", "packages/local-runtime/test/unit/error-reporting-privacy.test.ts", + "packages/local-runtime/test/unit/feature-owned-skills-website-deploy.test.ts", "packages/local-runtime/test/unit/local-background-task-task-output-polling-reminder.test.ts", "packages/local-runtime/test/unit/local-background-task-user-stop-cascade.test.ts", "packages/local-runtime/test/unit/local-memory-edit-literal.test.ts", @@ -2724,10 +2725,12 @@ "packages/local-runtime/test/unit/metrics-telemetry-privacy.test.ts", "packages/local-runtime/test/unit/session-diff-fallback.test.ts", "packages/local-runtime/test/unit/skill-directory-symlinks.test.ts", + "packages/local-runtime/test/unit/skills-catalog-description-cap.test.ts", "packages/local-runtime/test/unit/thread-goal-kickoff-host.test.ts", "packages/local-runtime/test/unit/thread-goal-verifier-contract.test.ts", "packages/local-runtime/test/unit/thread-goal/contract.test.ts", "packages/local-runtime/test/unit/thread-goal/gate.test.ts", + "packages/local-runtime/test/unit/thread-goal/goal-lifecycle-runtime-tools.test.ts", "packages/local-runtime/test/unit/thread-goal/host-integration-explicit-reset-rearm.test.ts", "packages/local-runtime/test/unit/thread-goal/host-integration-four-fix-combination.test.ts", "packages/local-runtime/test/unit/thread-goal/host-integration-settlement.test.ts", diff --git a/test/vitest-suites.json b/test/vitest-suites.json index a30ece859..056df3c8e 100644 --- a/test/vitest-suites.json +++ b/test/vitest-suites.json @@ -12,6 +12,9 @@ "packages/agent-tools/src/shared/replace-all-edit.test.ts", "packages/agent-tools/src/shared/cron-schedule-input.test.ts", "packages/agent-tools/src/desktop/edit-diff-bounds.test.ts", + "packages/local-runtime/test/unit/feature-owned-skills-website-deploy.test.ts", + "packages/local-runtime/test/unit/skills-catalog-description-cap.test.ts", + "packages/local-runtime/test/unit/thread-goal/goal-lifecycle-runtime-tools.test.ts", "packages/tui/test/unit/tui-tool-preview-diff-omitted.test.ts", "packages/local-runtime-v2/test/unit/agent/agent-import.test.ts", "packages/tui/test/unit/auth-application.test.ts",