mirror of
https://github.com/MoonshotAI/kimi-code.git
synced 2026-08-06 23:36:06 +00:00
fix(agent-core-v2): omit max_context_tokens in REST session status when unknown (#2696)
- align the REST status rollup with the WS push: a bound alias that no longer resolves omits max_context_tokens instead of reporting 0 (0 is the engine's UNKNOWN_CAPABILITY marker, not a real limit) - fall back to the default model's limit only when no model is bound, resolved through IModelService like the WS side - mark max_context_tokens optional in the shared session status schema
This commit is contained in:
parent
8c766a6c30
commit
4d39f4fa6f
5 changed files with 114 additions and 19 deletions
|
|
@ -29,12 +29,12 @@ import type { PermissionMode } from '#/agent/permissionPolicy/types';
|
|||
import { IAgentPlanService } from '#/features/plan/plan';
|
||||
import { IAgentProfileService } from '#/agent/profile/profile';
|
||||
import { IAgentSwarmService } from '#/agent/swarm/swarm';
|
||||
import { IConfigService } from '#/app/config/config';
|
||||
import {
|
||||
getLiveSessionById,
|
||||
resumeSessionById,
|
||||
} from '#/app/workspaceLifecycle/sessionLookup';
|
||||
import { IModelCatalog } from '#/kosong/model/catalog';
|
||||
import { IModelService } from '#/kosong/model/model';
|
||||
import { ErrorCodes, Error2 } from '#/errors';
|
||||
import { ensureMainAgent } from '#/session/agentLifecycle/mainAgent';
|
||||
import { IAgentLifecycleService } from '#/session/agentLifecycle/agentLifecycle';
|
||||
|
|
@ -176,14 +176,16 @@ export class SessionLegacyService implements ISessionLegacyService {
|
|||
const swarm = agent.accessor.get(IAgentSwarmService);
|
||||
|
||||
const model = profile.getModel();
|
||||
const caps = profile.getModelCapabilities() as {
|
||||
max_context_tokens?: number;
|
||||
max_input_tokens?: number;
|
||||
};
|
||||
const maxTokens =
|
||||
model === ''
|
||||
? resolveDefaultModelContextTokens(agent)
|
||||
: (caps.max_input_tokens ?? caps.max_context_tokens ?? 0);
|
||||
const capabilities = profile.getModelCapabilities();
|
||||
// An alias that no longer resolves yields UNKNOWN_CAPABILITY whose
|
||||
// max_context_tokens is 0 — the "unknown" marker, not a real limit. Only
|
||||
// an unbound session falls back to the default model's limit; when the
|
||||
// limit stays unknown the field is omitted (never 0), mirroring the WS
|
||||
// status push (`readLegacyStatus`).
|
||||
let maxTokens = capabilities.max_input_tokens ?? capabilities.max_context_tokens;
|
||||
if (maxTokens === 0 && model === '') {
|
||||
maxTokens = resolveDefaultModelContextTokens(agent) ?? 0;
|
||||
}
|
||||
const tokens = tokenCounting.statusSize();
|
||||
const planData = await plan.status();
|
||||
|
||||
|
|
@ -195,7 +197,7 @@ export class SessionLegacyService implements ISessionLegacyService {
|
|||
plan_mode: planData !== null,
|
||||
swarm_mode: swarm.isActive,
|
||||
context_tokens: tokens,
|
||||
max_context_tokens: maxTokens,
|
||||
max_context_tokens: maxTokens > 0 ? maxTokens : undefined,
|
||||
context_usage: maxTokens > 0 ? Math.min(1, tokens / maxTokens) : 0,
|
||||
};
|
||||
}
|
||||
|
|
@ -216,14 +218,18 @@ export class SessionLegacyService implements ISessionLegacyService {
|
|||
}
|
||||
}
|
||||
|
||||
function resolveDefaultModelContextTokens(agent: IAgentScopeHandle): number {
|
||||
const defaultModel = agent.accessor.get(IConfigService).get<string>('defaultModel');
|
||||
if (typeof defaultModel !== 'string' || defaultModel.length === 0) return 0;
|
||||
/**
|
||||
* Context limit of the configured default model, or `undefined` when no
|
||||
* default model is configured or it does not resolve.
|
||||
*/
|
||||
function resolveDefaultModelContextTokens(agent: IAgentScopeHandle): number | undefined {
|
||||
const defaultModel = agent.accessor.get(IModelService).getDefaultModel();
|
||||
if (defaultModel === undefined || defaultModel.length === 0) return undefined;
|
||||
try {
|
||||
const capabilities = agent.accessor.get(IModelCatalog).get(defaultModel).capabilities;
|
||||
return capabilities.max_input_tokens ?? capabilities.max_context_tokens;
|
||||
} catch {
|
||||
return 0;
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -85,7 +85,7 @@ export const sessionStatusResponseSchema = z.object({
|
|||
plan_mode: z.boolean(),
|
||||
swarm_mode: z.boolean(),
|
||||
context_tokens: z.number().int().nonnegative(),
|
||||
max_context_tokens: z.number().int().nonnegative(),
|
||||
max_context_tokens: z.number().int().nonnegative().optional(),
|
||||
context_usage: z.number().min(0).max(1),
|
||||
});
|
||||
export type SessionStatusResponse = z.infer<typeof sessionStatusResponseSchema>;
|
||||
|
|
|
|||
|
|
@ -20,8 +20,9 @@ import { IAgentPermissionModeService } from '#/agent/permissionMode/permissionMo
|
|||
import { IAgentPlanService } from '#/features/plan/plan';
|
||||
import { IAgentProfileService } from '#/agent/profile/profile';
|
||||
import { IAgentSwarmService } from '#/agent/swarm/swarm';
|
||||
import { IConfigService } from '#/app/config/config';
|
||||
import { UNKNOWN_CAPABILITY } from '#/kosong/contract/capability';
|
||||
import { IModelCatalog } from '#/kosong/model/catalog';
|
||||
import { IModelService } from '#/kosong/model/model';
|
||||
import { ISessionLegacyService } from '#/app/sessionLegacy/sessionLegacy';
|
||||
import { SessionLegacyService } from '#/app/sessionLegacy/sessionLegacyService';
|
||||
import { ISessionIndex } from '#/app/sessionIndex/sessionIndex';
|
||||
|
|
@ -147,12 +148,14 @@ describe('Session legacy status (best-effort runtime state)', () => {
|
|||
|
||||
const status = await ix.get(ISessionLegacyService).status('session-test');
|
||||
|
||||
// The ghost alias resolves to UNKNOWN_CAPABILITY whose 0 means "unknown"
|
||||
// — the rollup omits the field rather than reporting 0 (WS parity).
|
||||
expect(status).toMatchObject({
|
||||
busy: false,
|
||||
model: 'removed-model',
|
||||
thinking_level: 'high',
|
||||
max_context_tokens: 0,
|
||||
});
|
||||
expect(status.max_context_tokens).toBeUndefined();
|
||||
});
|
||||
|
||||
it('reports an empty thinking level for a never-bound main agent', async () => {
|
||||
|
|
@ -178,7 +181,9 @@ describe('Session legacy status (best-effort runtime state)', () => {
|
|||
[IAgentPermissionModeService, { mode: 'manual' }],
|
||||
[IAgentPlanService, { status: () => Promise.resolve(null) }],
|
||||
[IAgentSwarmService, { isActive: false }],
|
||||
[IConfigService, { get: () => undefined }],
|
||||
// Unbound: assembleStatus resolves the default model's context cap,
|
||||
// which asks the model service first — no default model here.
|
||||
[IModelService, { getDefaultModel: () => undefined }],
|
||||
[
|
||||
IAgentActivityView,
|
||||
{ state: () => ({ lifecycle: 'ready', background: [] }) },
|
||||
|
|
@ -210,6 +215,75 @@ describe('Session legacy status (best-effort runtime state)', () => {
|
|||
model: undefined,
|
||||
thinking_level: '',
|
||||
});
|
||||
// No bound model and no default model — the limit is unknown and omitted.
|
||||
expect(status.max_context_tokens).toBeUndefined();
|
||||
});
|
||||
|
||||
it('falls back to the default model limit when no model is bound', async () => {
|
||||
// Draft-session shape: no model bound, so the capabilities are unknown;
|
||||
// the rollup mirrors the WS push and reads the default model's limit.
|
||||
const profile = {
|
||||
_serviceBrand: undefined,
|
||||
data: () => ({
|
||||
cwd: '/workspace',
|
||||
modelAlias: undefined,
|
||||
modelCapabilities: UNKNOWN_CAPABILITY,
|
||||
thinkingLevel: 'off',
|
||||
systemPrompt: '',
|
||||
}),
|
||||
getModel: () => '',
|
||||
getModelCapabilities: () => UNKNOWN_CAPABILITY,
|
||||
getEffectiveThinkingLevel: () => 'off',
|
||||
} as unknown as IAgentProfileService;
|
||||
const agent: IAgentScopeHandle = {
|
||||
id: 'main',
|
||||
kind: LifecycleScope.Agent,
|
||||
accessor: accessor([
|
||||
[IAgentProfileService, profile],
|
||||
[IAgentTokenCountingService, { get: () => ({ size: 0, measured: 0, estimated: 0 }), statusSize: () => 0 }],
|
||||
[IAgentPermissionModeService, { mode: 'manual' }],
|
||||
[IAgentPlanService, { status: () => Promise.resolve(null) }],
|
||||
[IAgentSwarmService, { isActive: false }],
|
||||
[IModelService, { getDefaultModel: () => 'default-model' }],
|
||||
[
|
||||
IModelCatalog,
|
||||
{
|
||||
get: (id: string) => {
|
||||
if (id !== 'default-model') throw new Error(`unknown model ${id}`);
|
||||
return { capabilities: { max_context_tokens: 200_000 } };
|
||||
},
|
||||
},
|
||||
],
|
||||
[
|
||||
IAgentActivityView,
|
||||
{ state: () => ({ lifecycle: 'ready', background: [] }) },
|
||||
],
|
||||
]),
|
||||
dispose: () => {},
|
||||
};
|
||||
const agents = {
|
||||
create: () => Promise.resolve(agent),
|
||||
whenReady: () => Promise.resolve(agent),
|
||||
list: () => [agent],
|
||||
} as unknown as IAgentLifecycleService;
|
||||
const session: ISessionScopeHandle = {
|
||||
id: 'session-draft',
|
||||
kind: LifecycleScope.Session,
|
||||
accessor: accessor([
|
||||
[IAgentLifecycleService, agents],
|
||||
[ISessionCronService, { _serviceBrand: undefined }],
|
||||
]),
|
||||
dispose: () => {},
|
||||
};
|
||||
stubSessionChain(ix, session);
|
||||
ix.set(ISessionLegacyService, new SyncDescriptor(SessionLegacyService));
|
||||
|
||||
const status = await ix.get(ISessionLegacyService).status('session-draft');
|
||||
|
||||
expect(status).toMatchObject({
|
||||
model: undefined,
|
||||
max_context_tokens: 200_000,
|
||||
});
|
||||
});
|
||||
|
||||
it('uses the input cap as the status denominator and clamps usage to 1', async () => {
|
||||
|
|
|
|||
|
|
@ -410,6 +410,19 @@ describe('sessionStatusResponseSchema', () => {
|
|||
expect(parsed.model).toBeUndefined();
|
||||
});
|
||||
|
||||
it('accepts an omitted max_context_tokens (unknown context limit)', () => {
|
||||
const parsed = sessionStatusResponseSchema.parse({
|
||||
busy: false,
|
||||
thinking_level: 'off',
|
||||
permission: 'auto',
|
||||
plan_mode: false,
|
||||
swarm_mode: false,
|
||||
context_tokens: 0,
|
||||
context_usage: 0,
|
||||
});
|
||||
expect(parsed.max_context_tokens).toBeUndefined();
|
||||
});
|
||||
|
||||
it('rejects missing busy', () => {
|
||||
expect(
|
||||
sessionStatusResponseSchema.safeParse({
|
||||
|
|
|
|||
|
|
@ -140,7 +140,9 @@ export const sessionStatusResponseSchema = z.object({
|
|||
plan_mode: z.boolean(),
|
||||
swarm_mode: z.boolean(),
|
||||
context_tokens: z.number().int().nonnegative(),
|
||||
max_context_tokens: z.number().int().nonnegative(),
|
||||
/** Omitted when the context limit is unknown — 0 is the engine's "unknown"
|
||||
* marker, never a real limit. */
|
||||
max_context_tokens: z.number().int().nonnegative().optional(),
|
||||
context_usage: z.number().min(0).max(1),
|
||||
});
|
||||
export type SessionStatusResponse = z.infer<typeof sessionStatusResponseSchema>;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue