mirror of
https://github.com/MoonshotAI/kimi-code.git
synced 2026-08-18 13:15:51 +00:00
fix(agent-core-v2): degrade media tool registration when the bound model alias is stale (#2985)
* fix(agent-core-v2): degrade media tool registration when the bound model alias is stale A restored session replays its persisted profile.bind without catalog validation, so the profile can carry a model alias that no longer resolves (e.g. the managed kimi-code models were removed from config.toml on logout). AgentMediaToolsRegistrar.refresh() called modelCatalog.getRequester() unguarded on that alias; the throw escaped the agent.status.updated listener and was reported as an [unexpected] Error2 (config.invalid) on startup. Catch the resolution failure and degrade to "no model": media tools stay registered off the profile-reported capabilities, just without a model-bound video uploader, matching the tryResolveRawModel style used elsewhere in the profile service. * test(agent-core-v2): reproduce the stale-alias regression with production-consistent collaborators A stale alias makes the real AgentProfileService report UNKNOWN_CAPABILITY, so the regression now binds unknown capabilities, asserts the tool stays unregistered without surfacing an [unexpected] error, and covers recovery once the alias resolves again. The rationale moves into the mediaToolsRegistrar file header per the package comment conventions. --------- Co-authored-by: Mira <bj456736@users.noreply.github.com>
This commit is contained in:
parent
5dffed2545
commit
a7dc1ea284
3 changed files with 58 additions and 7 deletions
5
.changeset/media-registrar-stale-alias.md
Normal file
5
.changeset/media-registrar-stale-alias.md
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
"@moonshot-ai/kimi-code": patch
|
||||
---
|
||||
|
||||
Fix an `[unexpected] Error2: Model "<alias>" is not configured in config.toml` error printed on startup when a restored session references a model that is no longer configured (e.g. after logging out of the managed Kimi Code account). Media tool registration now degrades gracefully instead of throwing from the `agent.status.updated` listener.
|
||||
|
|
@ -17,6 +17,14 @@
|
|||
* converts `video_url` takes the inline fallback when no upload hook
|
||||
* exists.
|
||||
*
|
||||
* The alias re-resolved on every refresh comes from persisted profile
|
||||
* state that resume replays without catalog validation, so it may no
|
||||
* longer resolve (e.g. its config.toml entry was removed on logout).
|
||||
* A failed resolution degrades to "no model" — registration proceeds
|
||||
* from the profile-reported capabilities without a model-bound video
|
||||
* uploader — instead of throwing out of the event listener, where the
|
||||
* escape would surface as an `[unexpected]` error.
|
||||
*
|
||||
* The plain-data state (`registeredKey`) is registered into `agentState`
|
||||
* (`IAgentStateService`) and read/written through it; `registration` stays an
|
||||
* instance field (the live `IDisposable` tool-registration handle, not plain
|
||||
|
|
@ -125,8 +133,13 @@ export class AgentMediaToolsRegistrar extends Service implements IAgentMediaTool
|
|||
let requester: ModelRequester | undefined;
|
||||
let model: Model | undefined;
|
||||
if (modelAlias !== '') {
|
||||
requester = this.modelCatalog.getRequester(modelAlias);
|
||||
model = requester.model;
|
||||
try {
|
||||
requester = this.modelCatalog.getRequester(modelAlias);
|
||||
model = requester.model;
|
||||
} catch {
|
||||
requester = undefined;
|
||||
model = undefined;
|
||||
}
|
||||
}
|
||||
this.registration = registerMediaTools(this.toolRegistry, {
|
||||
runtime,
|
||||
|
|
|
|||
|
|
@ -9,13 +9,17 @@
|
|||
|
||||
import * as posixPath from 'node:path/posix';
|
||||
|
||||
import type { ModelCapability } from '#/kosong/contract/capability';
|
||||
import { UNKNOWN_CAPABILITY, type ModelCapability } from '#/kosong/contract/capability';
|
||||
import type { ContentPart } from '#/kosong/contract/message';
|
||||
import { VideoUploadUnsupportedError } from '#/kosong/contract/errors';
|
||||
import { Jimp } from 'jimp';
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { Emitter } from '#/_base/event';
|
||||
import {
|
||||
resetUnexpectedErrorHandler,
|
||||
setUnexpectedErrorHandler,
|
||||
} from '#/_base/errors/unexpectedError';
|
||||
import type { IHostFileSystem } from '#/os/interface/hostFileSystem';
|
||||
import type { IHostEnvironment } from '#/os/interface/hostEnvironment';
|
||||
import type { IAgentRuntimeService } from '#/agent/runtimeBinding/agentRuntime';
|
||||
|
|
@ -873,10 +877,14 @@ describe('AgentMediaToolsRegistrar', () => {
|
|||
getModelCapabilities: () => state.capabilities,
|
||||
getModel: () => state.alias,
|
||||
} as unknown as IAgentProfileService;
|
||||
const brokenAliases = new Set<string>();
|
||||
const modelCatalog = {
|
||||
getRequester: (id: string) => ({
|
||||
model: { id, name: id, providerName: 'test', protocol: 'openai' },
|
||||
}),
|
||||
getRequester: (id: string) => {
|
||||
if (brokenAliases.has(id)) {
|
||||
throw new Error(`Model "${id}" is not configured in config.toml.`);
|
||||
}
|
||||
return { model: { id, name: id, providerName: 'test', protocol: 'openai' } };
|
||||
},
|
||||
} as unknown as IModelCatalog;
|
||||
const workspaceCtx = {
|
||||
workDir: '/workspace',
|
||||
|
|
@ -916,7 +924,13 @@ describe('AgentMediaToolsRegistrar', () => {
|
|||
runtimeAvailable = available;
|
||||
runtimeChanges.fire();
|
||||
};
|
||||
return { registry, registrar, bindModel, setRuntimeAvailable };
|
||||
const breakAlias = (alias: string): void => {
|
||||
brokenAliases.add(alias);
|
||||
};
|
||||
const healAlias = (alias: string): void => {
|
||||
brokenAliases.delete(alias);
|
||||
};
|
||||
return { registry, registrar, bindModel, setRuntimeAvailable, breakAlias, healAlias };
|
||||
}
|
||||
|
||||
it('registers nothing until a media-capable model binds, then registers ReadMediaFile', () => {
|
||||
|
|
@ -970,6 +984,25 @@ describe('AgentMediaToolsRegistrar', () => {
|
|||
expect(registry.resolve('ReadMediaFile')).toBe(first);
|
||||
});
|
||||
|
||||
it('survives an unconfigured bound alias and recovers when it resolves again', () => {
|
||||
const unexpected: unknown[] = [];
|
||||
setUnexpectedErrorHandler((err) => unexpected.push(err));
|
||||
try {
|
||||
const { registry, bindModel, breakAlias, healAlias } = createRegistrarHarness();
|
||||
breakAlias('stale-model');
|
||||
bindModel('stale-model', UNKNOWN_CAPABILITY);
|
||||
expect(unexpected).toHaveLength(0);
|
||||
expect(registry.resolve('ReadMediaFile')).toBeUndefined();
|
||||
|
||||
healAlias('stale-model');
|
||||
bindModel('stale-model', capabilities({ image_in: true, video_in: true }));
|
||||
expect(registry.resolve('ReadMediaFile')).toBeInstanceOf(ReadMediaFileTool);
|
||||
expect(unexpected).toHaveLength(0);
|
||||
} finally {
|
||||
resetUnexpectedErrorHandler();
|
||||
}
|
||||
});
|
||||
|
||||
it('unregisters on dispose', () => {
|
||||
const { registry, registrar, bindModel } = createRegistrarHarness();
|
||||
bindModel('vision-model', capabilities({ image_in: true, video_in: true }));
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue