fix: add minor improvements for Mira (#649)

This commit is contained in:
_Kerman 2026-06-11 15:50:33 +08:00 committed by GitHub
parent 54302ad612
commit a2c5e1be25
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
16 changed files with 90 additions and 38 deletions

View file

@ -0,0 +1,7 @@
---
"@moonshot-ai/agent-core": patch
"@moonshot-ai/kosong": patch
"@moonshot-ai/kimi-code": patch
---
Add runtime support for dynamic MCP server updates, reference skills, replay timestamps, and Node file uploads.

View file

@ -23,6 +23,8 @@ vi.mock('#/utils/open-url', () => ({ openUrl: vi.fn() }));
type GoalReplayRecord = Extract<AgentReplayRecord, { type: 'goal_updated' }>;
const REPLAY_TIME = 1_700_000_000_000;
function stripAnsi(text: string): string {
return text.replaceAll(/\u001B\[[0-9;]*m/g, '');
}
@ -70,6 +72,7 @@ function message(
} = {},
): AgentReplayRecord {
return {
time: REPLAY_TIME,
type: 'message',
message: {
role,
@ -122,6 +125,7 @@ function goalReplay(
change: GoalReplayRecord['change'],
): GoalReplayRecord {
return {
time: REPLAY_TIME,
type: 'goal_updated',
snapshot,
change,
@ -988,11 +992,12 @@ describe('KimiTUI resume message replay', () => {
it('renders plan permission and approval replay notices', async () => {
const driver = await replayIntoDriver([
{ type: 'plan_updated', enabled: true },
{ type: 'permission_updated', mode: 'auto' },
{ type: 'permission_updated', mode: 'yolo' },
{ type: 'permission_updated', mode: 'manual' },
{ time: REPLAY_TIME, type: 'plan_updated', enabled: true },
{ time: REPLAY_TIME, type: 'permission_updated', mode: 'auto' },
{ time: REPLAY_TIME, type: 'permission_updated', mode: 'yolo' },
{ time: REPLAY_TIME, type: 'permission_updated', mode: 'manual' },
{
time: REPLAY_TIME,
type: 'approval_result',
record: {
turnId: 0,
@ -1006,7 +1011,7 @@ describe('KimiTUI resume message replay', () => {
},
},
},
{ type: 'plan_updated', enabled: false },
{ time: REPLAY_TIME, type: 'plan_updated', enabled: false },
]);
const transcript = driver.state.transcriptContainer.render(120).join('\n');
@ -1025,6 +1030,7 @@ describe('KimiTUI resume message replay', () => {
toolCalls: [toolCall('call_exit_reject', 'ExitPlanMode', {})],
}),
{
time: REPLAY_TIME,
type: 'approval_result',
record: {
turnId: 0,
@ -1042,6 +1048,7 @@ describe('KimiTUI resume message replay', () => {
toolCalls: [toolCall('call_exit_final', 'ExitPlanMode', {})],
}),
{
time: REPLAY_TIME,
type: 'approval_result',
record: {
turnId: 1,
@ -1064,7 +1071,7 @@ describe('KimiTUI resume message replay', () => {
],
{ toolCallId: 'call_exit_final' },
),
{ type: 'plan_updated', enabled: false },
{ time: REPLAY_TIME, type: 'plan_updated', enabled: false },
]);
const transcript = driver.state.transcriptContainer.render(120).join('\n');

View file

@ -5,7 +5,7 @@ import {
migrateWireRecord,
resolveWireMigrations,
type WireMigration,
} from '@moonshot-ai/agent-core/agent/records/migration';
} from '@moonshot-ai/agent-core/agent/records/migration/index';
import type { AgentRecord, WireEntry } from './agent-record-types';

View file

@ -37,13 +37,13 @@
"types": "./src/index.ts",
"default": "./src/index.ts"
},
"./agent/records/migration": {
"types": "./src/agent/records/migration/index.ts",
"default": "./src/agent/records/migration/index.ts"
"./package.json": {
"types": "./package.json",
"default": "./package.json"
},
"./session/store": {
"types": "./src/session/store/index.ts",
"default": "./src/session/store/index.ts"
"./*": {
"types": "./src/*.ts",
"default": "./src/*.ts"
}
},
"scripts": {

View file

@ -14,8 +14,6 @@ import type { PermissionPolicy, PermissionPolicyContext, PermissionPolicyResult
export class SensitiveFileAccessAskPermissionPolicy implements PermissionPolicy {
readonly name = 'sensitive-file-access-ask';
constructor(private readonly agent: Agent) {}
evaluate(context: PermissionPolicyContext): PermissionPolicyResult | undefined {
const access = fileAccesses(context).find((fileAccess) =>
isSensitiveFile(fileAccess.path),

View file

@ -49,7 +49,7 @@ export function createPermissionDecisionPolicies(agent: Agent): PermissionPolicy
// EnterPlanMode, Write/Edit on the plan file, or ExitPlanMode with no actionable plan_review → approve.
new PlanModeToolApprovePermissionPolicy(agent),
// Access touches a sensitive file (.env, SSH key, credentials) → ask.
new SensitiveFileAccessAskPermissionPolicy(agent),
new SensitiveFileAccessAskPermissionPolicy(),
// Access touches .git or a git control-dir path → ask.
new GitControlPathAccessAskPermissionPolicy(agent),
// yolo mode → approve.

View file

@ -1,15 +1,19 @@
import type { Agent } from '..';
import type { AgentReplayRecord } from '../..';
import type { AgentReplayRecord, AgentReplayRecordPayload } from '../..';
import type { ContextMessage } from '../context';
export class ReplayBuilder {
captureLiveRecords = false;
protected readonly records: AgentReplayRecord[] = [];
constructor(public readonly agent: Agent) {}
push(record: AgentReplayRecord): void {
if (this.agent.records.restoring) {
this.records.push(record);
push(record: AgentReplayRecordPayload): void {
if (this.captureLiveRecords || this.agent.records.restoring) {
this.records.push({
...record,
time: this.agent.records.restoring?.time ?? Date.now(),
});
}
}

View file

@ -35,7 +35,6 @@ export class RotatingFileSink implements Sink {
private pending: string[] = [];
private dropped = 0;
private closed = false;
private failed = false;
private lastStderrNotice = 0;
private currentBytes = -1;
private directorySynced = false;
@ -103,7 +102,6 @@ export class RotatingFileSink implements Sink {
this.directorySynced = true;
}
this.failed = false;
return true;
} catch (error) {
this.noteFailure(error);
@ -212,7 +210,6 @@ export class RotatingFileSink implements Sink {
}
private noteFailure(error: unknown): void {
this.failed = true;
const now = Date.now();
if (now - this.lastStderrNotice < STDERR_NOTICE_INTERVAL_MS) return;
this.lastStderrNotice = now;

View file

@ -155,6 +155,38 @@ export class McpConnectionManager {
return initialLoad;
}
async connect(name: string, config: McpServerConfig): Promise<void> {
const previous = this.entries.get(name);
if (previous !== undefined) {
await this.closeClient(previous);
}
const disabled = config.enabled === false;
const entry: InternalEntry = {
name,
config,
attemptId: 0,
status: disabled ? 'disabled' : 'pending',
};
this.entries.set(name, entry);
this.emit(entry);
if (!disabled) {
await this.connectOne(entry, this.beginConnectAttempt(entry));
}
}
async remove(name: string): Promise<boolean> {
const entry = this.entries.get(name);
if (entry === undefined) return false;
await this.closeClient(entry);
entry.status = 'disabled';
entry.tools = undefined;
entry.enabledNames = undefined;
entry.error = undefined;
this.emit(entry);
this.entries.delete(name);
return true;
}
waitForInitialLoad(signal?: AbortSignal): Promise<void> {
signal?.throwIfAborted();
if (signal === undefined) return this.initialLoad;

View file

@ -37,7 +37,7 @@ import {
type OAuthTokenProviderResolver
} from '../session/provider-manager';
import { SessionAPIImpl } from '../session/rpc';
import { normalizeWorkDir, SessionStore } from '../session/store';
import { normalizeWorkDir, SessionStore } from '../session/store/index';
import { noopTelemetryClient, withTelemetryContext, type TelemetryClient } from '../telemetry';
import type { CoreRPCClient } from './client';
import type {

View file

@ -14,7 +14,7 @@ import type { SessionSummary } from '#/rpc/core-api';
import type { UsageStatus } from '#/rpc/events';
import type { SessionMeta } from '#/session';
export type AgentReplayRecord =
export type AgentReplayRecordPayload =
| { type: 'message'; message: ContextMessage }
| {
type: 'goal_updated';
@ -26,6 +26,8 @@ export type AgentReplayRecord =
| { type: 'permission_updated'; mode: PermissionMode }
| { type: 'approval_result'; record: PermissionApprovalResultRecord };
export type AgentReplayRecord = { readonly time: number } & AgentReplayRecordPayload;
export interface ResumedAgentState {
readonly type: AgentType;
readonly config: AgentConfigData;

View file

@ -69,7 +69,7 @@ export function isUserActivatableSkillType(type: string | undefined): boolean {
}
export function isSupportedSkillType(type: string | undefined): boolean {
return isUserActivatableSkillType(type);
return isUserActivatableSkillType(type) || type === 'reference';
}
export function summarizeSkill(skill: SkillDefinition): SkillSummary {

View file

@ -2463,10 +2463,12 @@ describe('Agent-local approve for session', () => {
ctx.dispatch(event);
expect(ctx.agent.replayBuilder.buildResult()).toContainEqual({
type: 'approval_result',
record: event,
});
expect(ctx.agent.replayBuilder.buildResult()).toContainEqual(
expect.objectContaining({
type: 'approval_result',
record: event,
}),
);
expect(ctx.agent.permission.data().rules).toEqual([]);
});
});

View file

@ -313,14 +313,16 @@ describe('Agent resume', () => {
await ctx.agent.resume();
expect(ctx.agent.replayBuilder.buildResult()).toContainEqual({
type: 'message',
message: expect.objectContaining({
role: 'tool',
toolCallId: 'call_failed_bash',
isError: true,
expect(ctx.agent.replayBuilder.buildResult()).toContainEqual(
expect.objectContaining({
type: 'message',
message: expect.objectContaining({
role: 'tool',
toolCallId: 'call_failed_bash',
isError: true,
}),
}),
});
);
});
it('rebuilds goal completion replay cards without adding model-visible context', async () => {

View file

@ -1,5 +1,6 @@
import * as fs from 'node:fs';
import * as path from 'node:path';
import { Blob, File } from 'node:buffer';
import { ChatProviderError } from '#/errors';
import type { VideoURLPart } from '#/message';

View file

@ -33,7 +33,7 @@ import {
SessionStore,
encodeWorkDirKey,
normalizeWorkDir,
} from '@moonshot-ai/agent-core/session/store';
} from '@moonshot-ai/agent-core/session/store/index';
import { Session, type SDKSessionRPC } from '@moonshot-ai/agent-core';
import { LocalKaos } from '@moonshot-ai/kaos';