mirror of
https://github.com/MoonshotAI/kimi-code.git
synced 2026-07-10 01:39:25 +00:00
refactor(agent-core): move HookEngine to sessions (#165)
This commit is contained in:
parent
0a766584cb
commit
74e867a300
30 changed files with 154 additions and 218 deletions
|
|
@ -4,7 +4,6 @@ import type { Agent } from '../..';
|
|||
import type { TelemetryPropertyValue } from '../../telemetry';
|
||||
import {
|
||||
BackgroundProcessManager,
|
||||
type BackgroundProcessManagerOptions,
|
||||
type BackgroundTaskInfo,
|
||||
isBackgroundTaskTerminal,
|
||||
type ReconcileResult,
|
||||
|
|
@ -36,11 +35,11 @@ export class BackgroundManager extends BackgroundProcessManager {
|
|||
private readonly scheduledNotificationKeys = new Set<string>();
|
||||
private readonly deliveredNotificationKeys = new Set<string>();
|
||||
|
||||
constructor(
|
||||
public readonly agent: Agent,
|
||||
options: BackgroundProcessManagerOptions = {},
|
||||
) {
|
||||
super(options);
|
||||
constructor(public readonly agent: Agent) {
|
||||
super({
|
||||
maxRunningTasks: agent.kimiConfig?.background?.maxRunningTasks,
|
||||
sessionDir: agent.homedir,
|
||||
});
|
||||
|
||||
this.onLifecycle((event, info) => {
|
||||
switch (event) {
|
||||
|
|
|
|||
|
|
@ -91,16 +91,6 @@ export interface CronManagerOptions {
|
|||
* means "no automatic timer — caller drives `tick()` manually".
|
||||
*/
|
||||
readonly pollIntervalMs?: number | null;
|
||||
|
||||
/**
|
||||
* Per-session directory used to persist cron tasks across
|
||||
* `kimi resume`. When set, `addTask` / `removeTasks` mirror the
|
||||
* in-memory store to `<sessionDir>/cron/<id>.json` and
|
||||
* `loadFromDisk()` re-populates the store on resume. When omitted
|
||||
* (subagents, tests, ephemeral sessions), the manager stays purely
|
||||
* in-memory and `loadFromDisk()` is a no-op.
|
||||
*/
|
||||
readonly sessionDir?: string;
|
||||
}
|
||||
|
||||
export class CronManager {
|
||||
|
|
@ -158,9 +148,9 @@ export class CronManager {
|
|||
resolveClockSources(process.env['KIMI_CRON_CLOCK']) ??
|
||||
SYSTEM_CLOCKS;
|
||||
this.persistStore =
|
||||
opts.sessionDir === undefined
|
||||
agent.homedir === undefined
|
||||
? undefined
|
||||
: createCronPersistStore(opts.sessionDir);
|
||||
: createCronPersistStore(agent.homedir);
|
||||
|
||||
this.scheduler = createCronScheduler({
|
||||
clocks: this.clocks,
|
||||
|
|
@ -185,6 +175,8 @@ export class CronManager {
|
|||
? null
|
||||
: opts.pollIntervalMs,
|
||||
});
|
||||
|
||||
this.start();
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import { join } from 'pathe';
|
|||
import { ErrorCodes, KimiError, makeErrorPayload } from '#/errors';
|
||||
import { log } from '#/logging/logger';
|
||||
import type { Logger } from '#/logging/types';
|
||||
import type { AgentAPI, AgentEvent, SDKAgentRPC, UsageStatus } from '#/rpc';
|
||||
import type { AgentAPI, AgentEvent, KimiConfig, SDKAgentRPC, UsageStatus } from '#/rpc';
|
||||
import {
|
||||
generate,
|
||||
type ChatProvider,
|
||||
|
|
@ -32,7 +32,7 @@ import { FullCompaction, type CompactionStrategy } from './compaction';
|
|||
import { CronManager } from './cron';
|
||||
import { ConfigState } from './config';
|
||||
import { ContextMemory } from './context';
|
||||
import { HookEngine } from './hooks';
|
||||
import { HookEngine } from '../session/hooks';
|
||||
import { InjectionManager } from './injection/manager';
|
||||
import { PermissionManager, type PermissionManagerOptions } from './permission';
|
||||
import { PlanMode } from './plan';
|
||||
|
|
@ -60,10 +60,10 @@ export type { BuiltinTool, ToolInfo, ToolSource, UserToolRegistration } from './
|
|||
|
||||
export type AgentType = 'main' | 'sub' | 'independent';
|
||||
|
||||
export interface AgentConfig {
|
||||
export interface AgentOptions {
|
||||
readonly runtime: RuntimeConfig;
|
||||
readonly config?: KimiConfig;
|
||||
readonly homedir?: string;
|
||||
readonly skills?: SkillRegistry;
|
||||
readonly rpc?: SDKAgentRPC;
|
||||
readonly persistence?: AgentRecordPersistence;
|
||||
readonly type?: AgentType;
|
||||
|
|
@ -71,16 +71,9 @@ export interface AgentConfig {
|
|||
readonly compactionStrategy?: CompactionStrategy;
|
||||
readonly providerManager?: ProviderManager | undefined;
|
||||
readonly subagentHost?: SessionSubagentHost | undefined;
|
||||
readonly skills?: SkillRegistry;
|
||||
readonly mcp?: McpConnectionManager;
|
||||
readonly hookEngine?: HookEngine;
|
||||
readonly backgroundMaxRunningTasks?: number;
|
||||
readonly backgroundSessionDir?: string;
|
||||
/**
|
||||
* Per-session directory used by `CronManager` to persist scheduled
|
||||
* tasks across `kimi resume`. When omitted the cron stack stays
|
||||
* purely in-memory (subagents, ephemeral sessions). Set in parallel
|
||||
* with {@link backgroundSessionDir} from the session homedir.
|
||||
*/
|
||||
readonly cronSessionDir?: string;
|
||||
readonly permission?: PermissionManagerOptions | undefined;
|
||||
readonly log?: Logger;
|
||||
|
|
@ -90,6 +83,7 @@ export interface AgentConfig {
|
|||
|
||||
export class Agent {
|
||||
readonly runtime: RuntimeConfig;
|
||||
readonly kimiConfig?: KimiConfig;
|
||||
readonly homedir?: string;
|
||||
readonly skills?: SkillManager;
|
||||
readonly pluginSessionStarts: readonly EnabledPluginSessionStart[];
|
||||
|
|
@ -114,38 +108,37 @@ export class Agent {
|
|||
readonly usage: UsageRecorder;
|
||||
readonly tools: ToolManager;
|
||||
readonly background: BackgroundManager;
|
||||
readonly cron: CronManager;
|
||||
readonly cron: CronManager | null;
|
||||
readonly replayBuilder: ReplayBuilder;
|
||||
readonly log: Logger;
|
||||
|
||||
private lastLlmConfigLogSignature?: string;
|
||||
|
||||
constructor(config: AgentConfig) {
|
||||
this.log = config.log ?? log;
|
||||
this.runtime = config.runtime;
|
||||
this.homedir = config.homedir;
|
||||
if (config.skills !== undefined) {
|
||||
this.skills = new SkillManager(this, config.skills);
|
||||
constructor(options: AgentOptions) {
|
||||
this.log = options.log ?? log;
|
||||
this.kimiConfig = options.config;
|
||||
this.runtime = options.runtime;
|
||||
this.homedir = options.homedir;
|
||||
if (options.skills !== undefined) {
|
||||
this.skills = new SkillManager(this, options.skills);
|
||||
}
|
||||
this.pluginSessionStarts = config.pluginSessionStarts ?? [];
|
||||
this.rawGenerate = config.generate ?? generate;
|
||||
this.providerManager = config.providerManager;
|
||||
this.subagentHost = config.subagentHost;
|
||||
this.mcp = config.mcp;
|
||||
this.hooks = config.hookEngine;
|
||||
|
||||
this.type = config.type ?? 'main';
|
||||
|
||||
this.rpc = config.rpc;
|
||||
this.telemetry = config.telemetry ?? noopTelemetryClient;
|
||||
this.blobStore = config.homedir
|
||||
? new BlobStore({ blobsDir: join(config.homedir, 'blobs') })
|
||||
this.pluginSessionStarts = options.pluginSessionStarts ?? [];
|
||||
this.rawGenerate = options.generate ?? generate;
|
||||
this.providerManager = options.providerManager;
|
||||
this.subagentHost = options.subagentHost;
|
||||
this.mcp = options.mcp;
|
||||
this.hooks = options.hookEngine;
|
||||
this.type = options.type ?? 'main';
|
||||
this.rpc = options.rpc;
|
||||
this.telemetry = options.telemetry ?? noopTelemetryClient;
|
||||
this.blobStore = options.homedir
|
||||
? new BlobStore({ blobsDir: join(options.homedir, 'blobs') })
|
||||
: undefined;
|
||||
this.records = new AgentRecords(
|
||||
this,
|
||||
config.persistence ??
|
||||
(config.homedir
|
||||
? new FileSystemAgentRecordPersistence(join(config.homedir, 'wire.jsonl'), {
|
||||
options.persistence ??
|
||||
(options.homedir
|
||||
? new FileSystemAgentRecordPersistence(join(options.homedir, 'wire.jsonl'), {
|
||||
onError: (error) => {
|
||||
this.emitRecordsWriteError(error);
|
||||
},
|
||||
|
|
@ -153,38 +146,17 @@ export class Agent {
|
|||
})
|
||||
: undefined),
|
||||
);
|
||||
this.fullCompaction = new FullCompaction(this, config.compactionStrategy);
|
||||
this.fullCompaction = new FullCompaction(this, options.compactionStrategy);
|
||||
this.context = new ContextMemory(this);
|
||||
this.config = new ConfigState(this);
|
||||
this.turn = new TurnFlow(this);
|
||||
this.injection = new InjectionManager(this);
|
||||
this.permission = new PermissionManager(this, config.permission);
|
||||
this.permission = new PermissionManager(this, options.permission);
|
||||
this.planMode = new PlanMode(this);
|
||||
this.usage = new UsageRecorder(this);
|
||||
this.tools = new ToolManager(this);
|
||||
this.background = new BackgroundManager(this, {
|
||||
maxRunningTasks: config.backgroundMaxRunningTasks,
|
||||
sessionDir: config.backgroundSessionDir,
|
||||
});
|
||||
this.cron = new CronManager(this, {
|
||||
// Subagents stay in-memory: their default profiles don't expose
|
||||
// cron tools, so attaching a sessionDir would only litter the
|
||||
// subagent homedir with an empty `cron/` directory on first
|
||||
// mkdir. Main / non-sub agents persist when the session supplied
|
||||
// a directory.
|
||||
sessionDir: this.type !== 'sub' ? config.cronSessionDir : undefined,
|
||||
});
|
||||
if (this.type !== 'sub') {
|
||||
// Skip auto-tick for subagents: each session can spawn many
|
||||
// subagents, and stacking 1s setInterval timers + SIGUSR1
|
||||
// listeners per subagent serves no purpose — the default subagent
|
||||
// profiles don't expose Cron tools, so the store stays empty.
|
||||
// The scheduler unref()'s its setInterval so the cron timer never
|
||||
// keeps the process alive on its own, and isKilled (reading
|
||||
// KIMI_DISABLE_CRON) short-circuits every tick — no need to
|
||||
// delay start when the killswitch is set.
|
||||
this.cron.start();
|
||||
}
|
||||
this.background = new BackgroundManager(this);
|
||||
this.cron = this.type === 'sub' ? null : new CronManager(this);
|
||||
this.replayBuilder = new ReplayBuilder(this);
|
||||
}
|
||||
|
||||
|
|
@ -282,12 +254,7 @@ export class Agent {
|
|||
const result = await this.records.replay();
|
||||
await this.background.loadFromDisk();
|
||||
await this.background.reconcile();
|
||||
// Rehydrate cron tasks scheduled in the previous CLI invocation.
|
||||
// No-op when this agent was constructed without a `cronSessionDir`
|
||||
// (subagents, ephemeral sessions). The scheduler's `createdAt`-based
|
||||
// baseline then handles any fire times missed during downtime via
|
||||
// `coalescedCount` (and the 7-day stale flag) without further wiring.
|
||||
await this.cron.loadFromDisk();
|
||||
await this.cron?.loadFromDisk();
|
||||
this.turn.finishResume();
|
||||
return result;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -374,9 +374,9 @@ export class ToolManager {
|
|||
new b.TaskListTool(background),
|
||||
new b.TaskOutputTool(background),
|
||||
new b.TaskStopTool(background),
|
||||
this.agent.type !== 'sub' && new b.CronCreateTool(this.agent.cron),
|
||||
this.agent.type !== 'sub' && new b.CronListTool(this.agent.cron),
|
||||
this.agent.type !== 'sub' && new b.CronDeleteTool(this.agent.cron),
|
||||
this.agent.cron && new b.CronCreateTool(this.agent.cron),
|
||||
this.agent.cron && new b.CronListTool(this.agent.cron),
|
||||
this.agent.cron && new b.CronDeleteTool(this.agent.cron),
|
||||
this.agent.skills !== undefined &&
|
||||
this.agent.skills.registry.listInvocableSkills().length > 0 &&
|
||||
new b.SkillTool(this.agent),
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ import type { AgentEvent, TurnEndedEvent } from '../../rpc';
|
|||
import type { TelemetryPropertyValue } from '../../telemetry';
|
||||
import { abortable } from '../../utils/abort';
|
||||
import { USER_PROMPT_ORIGIN, type PromptOrigin } from '../context';
|
||||
import { renderUserPromptHookBlockResult, renderUserPromptHookResult } from '../hooks';
|
||||
import { renderUserPromptHookBlockResult, renderUserPromptHookResult } from '../../session/hooks';
|
||||
import { canonicalTelemetryArgs, isPlainRecord } from './canonical-args';
|
||||
import { ToolCallDeduplicator } from './tool-dedup';
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { HOOK_EVENT_TYPES } from '#/agent/hooks/types';
|
||||
import { HOOK_EVENT_TYPES } from '../session/hooks/types';
|
||||
import { parsePattern } from '#/agent/permission/matches-rule';
|
||||
import { ErrorCodes, KimiError } from '#/errors';
|
||||
import { z } from 'zod';
|
||||
|
|
|
|||
|
|
@ -180,6 +180,7 @@ export class KimiCore implements PromisableMethods<CoreAPI> {
|
|||
const runtime = await this.resolveRuntime(config);
|
||||
const session = new Session({
|
||||
runtime: { ...runtime, kaos: runtime.kaos.withCwd(workDir) },
|
||||
config,
|
||||
id,
|
||||
homedir: summary.sessionDir,
|
||||
kimiHomeDir: this.homeDir,
|
||||
|
|
@ -259,6 +260,7 @@ export class KimiCore implements PromisableMethods<CoreAPI> {
|
|||
const runtime = await this.resolveRuntime(config);
|
||||
const session = new Session({
|
||||
runtime: { ...runtime, kaos: runtime.kaos.withCwd(summary.workDir) },
|
||||
config,
|
||||
id: summary.id,
|
||||
homedir: summary.sessionDir,
|
||||
kimiHomeDir: this.homeDir,
|
||||
|
|
|
|||
|
|
@ -4,11 +4,11 @@ import { join } from 'pathe';
|
|||
import { ErrorCodes, KimiError } from '#/errors';
|
||||
import { getRootLogger, log } from '#/logging/logger';
|
||||
import type { Logger, SessionLogHandle } from '#/logging/types';
|
||||
import type { SDKSessionRPC } from '#/rpc';
|
||||
import type { KimiConfig, SDKSessionRPC } from '#/rpc';
|
||||
import { proxyWithExtraPayload } from '#/rpc/types';
|
||||
|
||||
import { Agent, type AgentConfig, type AgentType } from '../agent';
|
||||
import { HookEngine, type HookDef } from '../agent/hooks';
|
||||
import { Agent, type AgentOptions, type AgentType } from '../agent';
|
||||
import { HookEngine, type HookDef } from './hooks';
|
||||
import type { PermissionManagerOptions, PermissionRule } from '../agent/permission';
|
||||
import { parseBooleanEnv, resolveConfigValue, type BackgroundConfig } from '../config';
|
||||
import { makeErrorPayload } from '../errors';
|
||||
|
|
@ -39,8 +39,9 @@ import {
|
|||
import { noopTelemetryClient, type TelemetryClient } from '../telemetry';
|
||||
import { SessionSubagentHost } from './subagent-host';
|
||||
|
||||
export interface SessionConfig {
|
||||
export interface SessionOptions {
|
||||
readonly runtime: RuntimeConfig;
|
||||
readonly config?: KimiConfig;
|
||||
readonly id?: string | undefined;
|
||||
readonly homedir: string;
|
||||
readonly kimiHomeDir?: string;
|
||||
|
|
@ -105,29 +106,29 @@ export class Session {
|
|||
};
|
||||
private writeMetadataPromise = Promise.resolve();
|
||||
|
||||
constructor(public readonly config: SessionConfig) {
|
||||
constructor(public readonly options: SessionOptions) {
|
||||
// Attach the per-session log sink up front so the constructor's
|
||||
// fire-and-forget `loadSkills` / `loadMcpServers` failures (and
|
||||
// anything else that races) land in the session log, not just global.
|
||||
this.logHandle =
|
||||
config.id === undefined
|
||||
options.id === undefined
|
||||
? undefined
|
||||
: getRootLogger().attachSession({
|
||||
sessionId: config.id,
|
||||
sessionDir: config.homedir,
|
||||
sessionId: options.id,
|
||||
sessionDir: options.homedir,
|
||||
});
|
||||
this.log =
|
||||
this.logHandle?.logger ??
|
||||
(config.id === undefined ? log : log.createChild({ sessionId: config.id }));
|
||||
this.rpc = config.rpc;
|
||||
this.hookEngine = new HookEngine(config.hooks, {
|
||||
cwd: config.runtime.kaos.getcwd(),
|
||||
sessionId: config.id,
|
||||
(options.id === undefined ? log : log.createChild({ sessionId: options.id }));
|
||||
this.rpc = options.rpc;
|
||||
this.hookEngine = new HookEngine(options.hooks, {
|
||||
cwd: options.runtime.kaos.getcwd(),
|
||||
sessionId: options.id,
|
||||
});
|
||||
this.telemetry = config.telemetry ?? noopTelemetryClient;
|
||||
this.skills = new SkillRegistry({ sessionId: config.id });
|
||||
this.telemetry = options.telemetry ?? noopTelemetryClient;
|
||||
this.skills = new SkillRegistry({ sessionId: options.id });
|
||||
this.mcp = new McpConnectionManager({
|
||||
oauthService: new McpOAuthService({ kimiHomeDir: config.kimiHomeDir }),
|
||||
oauthService: new McpOAuthService({ kimiHomeDir: options.kimiHomeDir }),
|
||||
log: this.log,
|
||||
});
|
||||
this.mcp.onStatusChange((entry) => {
|
||||
|
|
@ -181,13 +182,9 @@ export class Session {
|
|||
|
||||
async close(): Promise<void> {
|
||||
try {
|
||||
// Stop cron FIRST. `stopBackgroundTasksOnExit()` can await
|
||||
// long-running background workers (especially with
|
||||
// `background.keepAliveOnExit=false`); while we are waiting, a due
|
||||
// cron tick would otherwise still call `turn.steer()` and start a
|
||||
// fresh turn after shutdown has already begun, racing the
|
||||
// metadata flush below.
|
||||
await this.stopCronOnExit();
|
||||
await Promise.allSettled(
|
||||
Array.from(this.agents.values(), async (agent) => agent.cron?.stop()),
|
||||
);
|
||||
await this.stopBackgroundTasksOnExit();
|
||||
await this.flushMetadata();
|
||||
await this.triggerSessionEnd('exit');
|
||||
|
|
@ -200,20 +197,11 @@ export class Session {
|
|||
}
|
||||
}
|
||||
|
||||
// Stop every agent's cron scheduler on close. No keepAlive notion — cron
|
||||
// intervals reference the agent/session graph and must die with the session.
|
||||
// `allSettled` keeps one agent's failure from blocking the rest.
|
||||
private async stopCronOnExit(): Promise<void> {
|
||||
await Promise.allSettled(
|
||||
Array.from(this.agents.values(), (agent) => agent.cron.stop()),
|
||||
);
|
||||
}
|
||||
|
||||
private async stopBackgroundTasksOnExit(): Promise<void> {
|
||||
const keepAliveOnExit = resolveConfigValue({
|
||||
env: process.env,
|
||||
envKey: BACKGROUND_KEEP_ALIVE_ON_EXIT_ENV,
|
||||
configValue: this.config.background?.keepAliveOnExit,
|
||||
configValue: this.options.background?.keepAliveOnExit,
|
||||
defaultValue: true,
|
||||
parseEnv: parseBooleanEnv,
|
||||
});
|
||||
|
|
@ -226,14 +214,14 @@ export class Session {
|
|||
}
|
||||
|
||||
async createAgent(
|
||||
config: Partial<AgentConfig>,
|
||||
config: Partial<AgentOptions>,
|
||||
profile?: ResolvedAgentProfile,
|
||||
parentAgentId?: string | undefined,
|
||||
): Promise<{ readonly id: string; readonly agent: Agent }> {
|
||||
await this.skillsReady;
|
||||
const type = config.type ?? 'main';
|
||||
const id = type === 'main' ? 'main' : this.nextGeneratedAgentId();
|
||||
const homedir = config.homedir ?? join(this.config.homedir, 'agents', id);
|
||||
const homedir = config.homedir ?? join(this.options.homedir, 'agents', id);
|
||||
const agent = this.instantiateAgent(id, homedir, type, config, parentAgentId ?? null);
|
||||
if (profile) {
|
||||
await this.bootstrapAgentProfile(agent, profile);
|
||||
|
|
@ -301,21 +289,21 @@ export class Session {
|
|||
}
|
||||
|
||||
protected get metadataPath() {
|
||||
return join(this.config.homedir, 'state.json');
|
||||
return join(this.options.homedir, 'state.json');
|
||||
}
|
||||
|
||||
writeMetadata() {
|
||||
const text = JSON.stringify(this.metadata, null, 2);
|
||||
const write = async () => {
|
||||
await this.config.runtime.kaos.mkdir(this.config.homedir, { parents: true, existOk: true });
|
||||
await this.config.runtime.kaos.writeText(this.metadataPath, text);
|
||||
await this.options.runtime.kaos.mkdir(this.options.homedir, { parents: true, existOk: true });
|
||||
await this.options.runtime.kaos.writeText(this.metadataPath, text);
|
||||
};
|
||||
this.writeMetadataPromise = this.writeMetadataPromise.then(write, write);
|
||||
return this.writeMetadataPromise;
|
||||
}
|
||||
|
||||
async readMetadata() {
|
||||
const text = await this.config.runtime.kaos.readText(this.metadataPath);
|
||||
const text = await this.options.runtime.kaos.readText(this.metadataPath);
|
||||
this.metadata = JSON.parse(text);
|
||||
return this.metadata;
|
||||
}
|
||||
|
|
@ -334,21 +322,21 @@ export class Session {
|
|||
private async loadSkills(): Promise<void> {
|
||||
const roots = await resolveSkillRoots({
|
||||
paths: {
|
||||
userHomeDir: this.config.skills?.userHomeDir ?? homedir(),
|
||||
workDir: this.config.runtime.kaos.getcwd(),
|
||||
userHomeDir: this.options.skills?.userHomeDir ?? homedir(),
|
||||
workDir: this.options.runtime.kaos.getcwd(),
|
||||
},
|
||||
explicitDirs: this.config.skills?.explicitDirs,
|
||||
extraDirs: this.config.skills?.extraDirs,
|
||||
pluginSkillRoots: this.config.skills?.pluginSkillRoots,
|
||||
mergeAllAvailableSkills: this.config.skills?.mergeAllAvailableSkills,
|
||||
builtinDir: this.config.skills?.builtinDir,
|
||||
explicitDirs: this.options.skills?.explicitDirs,
|
||||
extraDirs: this.options.skills?.extraDirs,
|
||||
pluginSkillRoots: this.options.skills?.pluginSkillRoots,
|
||||
mergeAllAvailableSkills: this.options.skills?.mergeAllAvailableSkills,
|
||||
builtinDir: this.options.skills?.builtinDir,
|
||||
});
|
||||
await this.skills.loadRoots(roots);
|
||||
registerBuiltinSkills(this.skills);
|
||||
}
|
||||
|
||||
private async loadMcpServers(): Promise<void> {
|
||||
const servers = this.config.mcpConfig?.servers;
|
||||
const servers = this.options.mcpConfig?.servers;
|
||||
if (servers === undefined || Object.keys(servers).length === 0) return;
|
||||
await this.mcp.connectAll(servers);
|
||||
const entries = this.mcp.list().filter((entry) => entry.status !== 'disabled');
|
||||
|
|
@ -399,7 +387,7 @@ export class Session {
|
|||
}
|
||||
|
||||
private backgroundTaskTimeoutMs(): number | undefined {
|
||||
const timeoutS = this.config.background?.agentTaskTimeoutS;
|
||||
const timeoutS = this.options.background?.agentTaskTimeoutS;
|
||||
return timeoutS === undefined ? undefined : timeoutS * 1000;
|
||||
}
|
||||
|
||||
|
|
@ -414,28 +402,26 @@ export class Session {
|
|||
id: string,
|
||||
homedir: string,
|
||||
type: AgentType,
|
||||
config: Partial<AgentConfig> = {},
|
||||
config: Partial<AgentOptions> = {},
|
||||
parentAgentId: string | null = null,
|
||||
): Agent {
|
||||
return new Agent({
|
||||
...config,
|
||||
type,
|
||||
runtime: this.config.runtime,
|
||||
runtime: this.options.runtime,
|
||||
config: this.options.config,
|
||||
homedir,
|
||||
skills: this.skills,
|
||||
rpc: proxyWithExtraPayload(this.rpc, { agentId: id }),
|
||||
providerManager: this.config.providerManager,
|
||||
providerManager: this.options.providerManager,
|
||||
hookEngine: config.hookEngine ?? this.hookEngine,
|
||||
subagentHost:
|
||||
config.subagentHost ?? new SessionSubagentHost(this, id, this.backgroundTaskTimeoutMs()),
|
||||
mcp: this.mcp,
|
||||
backgroundMaxRunningTasks: this.config.background?.maxRunningTasks,
|
||||
backgroundSessionDir: homedir,
|
||||
cronSessionDir: homedir,
|
||||
permission: this.permissionOptions(parentAgentId, config.permission),
|
||||
telemetry: this.telemetry,
|
||||
log: this.log.createChild({ agentId: id }),
|
||||
pluginSessionStarts: type === 'main' ? this.config.pluginSessionStarts : undefined,
|
||||
pluginSessionStarts: type === 'main' ? this.options.pluginSessionStarts : undefined,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -446,7 +432,7 @@ export class Session {
|
|||
if (parentAgentId === null) {
|
||||
return {
|
||||
...input,
|
||||
initialRules: input?.initialRules ?? this.config.permissionRules,
|
||||
initialRules: input?.initialRules ?? this.options.permissionRules,
|
||||
};
|
||||
}
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -12,14 +12,14 @@ import {
|
|||
} from '@moonshot-ai/kosong';
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import type { AgentConfig } from '../../src/agent';
|
||||
import type { AgentOptions } from '../../src/agent';
|
||||
import { DefaultCompactionStrategy, type CompactionStrategy } from '../../src/agent/compaction';
|
||||
import { HookEngine, type HookEngineTriggerArgs } from '../../src/agent/hooks';
|
||||
import { HookEngine, type HookEngineTriggerArgs } from '../../src/session/hooks';
|
||||
import { recordingTelemetry, type TelemetryRecord } from '../fixtures/telemetry';
|
||||
import type { TestAgentContext, TestAgentOptions } from './harness/agent';
|
||||
import { testAgent } from './harness/agent';
|
||||
|
||||
type GenerateFn = NonNullable<AgentConfig['generate']>;
|
||||
type GenerateFn = NonNullable<AgentOptions['generate']>;
|
||||
|
||||
const CATALOGUED_PROVIDER = {
|
||||
type: 'kimi',
|
||||
|
|
|
|||
|
|
@ -26,14 +26,14 @@ describe('Agent + Cron integration (P1.7)', () => {
|
|||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await ctx.agent.cron.stop();
|
||||
await ctx.agent.cron?.stop();
|
||||
vi.unstubAllEnvs();
|
||||
});
|
||||
|
||||
it('exposes agent.cron with its session store on construction', () => {
|
||||
expect(ctx.agent.cron).toBeDefined();
|
||||
expect(ctx.agent.cron.store).toBeDefined();
|
||||
expect(ctx.agent.cron.store.list()).toEqual([]);
|
||||
expect(ctx.agent.cron!.store).toBeDefined();
|
||||
expect(ctx.agent.cron!.store.list()).toEqual([]);
|
||||
});
|
||||
|
||||
it('registers CronCreate / CronList / CronDelete in the tool manager', () => {
|
||||
|
|
@ -58,7 +58,7 @@ describe('Agent + Cron integration (P1.7)', () => {
|
|||
// killswitch lives in `resolveExecution`, so a direct call is the
|
||||
// precise unit being asserted, and it stays robust if the loop /
|
||||
// dispatch surface changes around it (P1.8 onwards).
|
||||
const tool = new CronCreateTool(ctx.agent.cron);
|
||||
const tool = new CronCreateTool(ctx.agent.cron!);
|
||||
const args: CronCreateInput = {
|
||||
cron: '*/5 * * * *',
|
||||
prompt: 'x',
|
||||
|
|
@ -76,6 +76,6 @@ describe('Agent + Cron integration (P1.7)', () => {
|
|||
);
|
||||
|
||||
// And no task slipped into the store.
|
||||
expect(ctx.agent.cron.store.list()).toEqual([]);
|
||||
expect(ctx.agent.cron!.store.list()).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -50,7 +50,7 @@ describe('Cron — session E2E (P1.9)', () => {
|
|||
// `agent.cron.stop()`, but doing it here as well keeps the test
|
||||
// self-contained against future harness changes and ensures the
|
||||
// SIGUSR1 handler (if any) is unbound before the next test.
|
||||
await ctx.agent.cron.stop();
|
||||
await ctx.agent.cron?.stop();
|
||||
vi.unstubAllEnvs();
|
||||
});
|
||||
|
||||
|
|
@ -60,7 +60,7 @@ describe('Cron — session E2E (P1.9)', () => {
|
|||
// test-only escape hatch — Agent.cron is `readonly` precisely
|
||||
// because production code must never overwrite it; tests are the
|
||||
// only legitimate exception.
|
||||
await ctx.agent.cron.stop();
|
||||
await ctx.agent.cron!.stop();
|
||||
const harness = createClocks(LOCAL_ANCHOR_MS);
|
||||
(ctx.agent as unknown as { cron: CronManager }).cron = new CronManager(
|
||||
ctx.agent,
|
||||
|
|
@ -71,7 +71,7 @@ describe('Cron — session E2E (P1.9)', () => {
|
|||
pollIntervalMs: null,
|
||||
},
|
||||
);
|
||||
ctx.agent.cron.start();
|
||||
ctx.agent.cron!.start();
|
||||
|
||||
// Spy on agent.turn.steer. We wrap rather than replace so the real
|
||||
// steer logic still runs (and the `turn.steer` record is written /
|
||||
|
|
@ -96,7 +96,7 @@ describe('Cron — session E2E (P1.9)', () => {
|
|||
// bypass `emitScheduled` telemetry and skip the byte-length /
|
||||
// expression checks; that would not be the production code path
|
||||
// this commit is meant to smoke.
|
||||
const createTool = new CronCreateTool(ctx.agent.cron);
|
||||
const createTool = new CronCreateTool(ctx.agent.cron!);
|
||||
const execution = createTool.resolveExecution({
|
||||
cron: '*/5 * * * *',
|
||||
prompt: 'cron-fired prompt',
|
||||
|
|
@ -113,13 +113,13 @@ describe('Cron — session E2E (P1.9)', () => {
|
|||
signal: new AbortController().signal,
|
||||
});
|
||||
expect(createResult.isError ?? false).toBe(false);
|
||||
expect(ctx.agent.cron.store.list().length).toBe(1);
|
||||
expect(ctx.agent.cron!.store.list().length).toBe(1);
|
||||
|
||||
// Advance 15 minutes — exactly three ideal */5 fires across the gap
|
||||
// (12:05, 12:10, 12:15). See the file header for the calibration
|
||||
// derivation.
|
||||
harness.advance(15 * 60_000);
|
||||
ctx.agent.cron.tick();
|
||||
ctx.agent.cron!.tick();
|
||||
|
||||
// ── Steer was called exactly once ─────────────────────────────────
|
||||
expect(steerCalls.length).toBe(1);
|
||||
|
|
@ -149,9 +149,9 @@ describe('Cron — session E2E (P1.9)', () => {
|
|||
// Optional second case from the P1.9 plan: prove the three-tool
|
||||
// surface composes correctly end-to-end on the real manager. No
|
||||
// clock manipulation needed — list/delete are time-invariant.
|
||||
const createTool = new CronCreateTool(ctx.agent.cron);
|
||||
const listTool = new CronListTool(ctx.agent.cron);
|
||||
const deleteTool = new CronDeleteTool(ctx.agent.cron);
|
||||
const createTool = new CronCreateTool(ctx.agent.cron!);
|
||||
const listTool = new CronListTool(ctx.agent.cron!);
|
||||
const deleteTool = new CronDeleteTool(ctx.agent.cron!);
|
||||
const ctxArgs = {
|
||||
turnId: 'p19-tools',
|
||||
toolCallId: 'p19-tools-call',
|
||||
|
|
|
|||
|
|
@ -38,6 +38,8 @@ export interface AgentStubOptions {
|
|||
* encodes "input was buffered into the steer queue" (turn in flight).
|
||||
*/
|
||||
readonly steerReturns?: number | null;
|
||||
/** Optional homedir so CronManager persistence can derive its path. */
|
||||
readonly homedir?: string;
|
||||
}
|
||||
|
||||
export interface AgentStub {
|
||||
|
|
@ -70,7 +72,7 @@ export function createAgentStub(opts: AgentStubOptions = {}): AgentStub {
|
|||
telemetryCalls.push({ event, props });
|
||||
},
|
||||
};
|
||||
const agent = { turn, telemetry } as unknown as Agent;
|
||||
const agent = { turn, telemetry, homedir: opts.homedir } as unknown as Agent;
|
||||
return {
|
||||
agent,
|
||||
steerCalls,
|
||||
|
|
|
|||
|
|
@ -185,9 +185,9 @@ describe('CronManager — P1.8 manual tick + SIGUSR1', () => {
|
|||
if (process.platform === 'win32') return;
|
||||
|
||||
const stub = createAgentStub();
|
||||
const manager = new CronManager(stub.agent, { pollIntervalMs: null });
|
||||
const before = process.listenerCount('SIGUSR1');
|
||||
manager.start();
|
||||
// Constructor auto-starts, which binds SIGUSR1 under KIMI_CRON_MANUAL_TICK=1.
|
||||
const manager = new CronManager(stub.agent, { pollIntervalMs: null });
|
||||
expect(process.listenerCount('SIGUSR1')).toBe(before + 1);
|
||||
await manager.stop();
|
||||
expect(process.listenerCount('SIGUSR1')).toBe(before);
|
||||
|
|
@ -197,10 +197,11 @@ describe('CronManager — P1.8 manual tick + SIGUSR1', () => {
|
|||
if (process.platform === 'win32') return;
|
||||
|
||||
const stub = createAgentStub();
|
||||
const manager = new CronManager(stub.agent, { pollIntervalMs: null });
|
||||
const before = process.listenerCount('SIGUSR1');
|
||||
// Constructor already calls start() once; an explicit second
|
||||
// call must not stack a handler.
|
||||
const manager = new CronManager(stub.agent, { pollIntervalMs: null });
|
||||
try {
|
||||
manager.start();
|
||||
manager.start();
|
||||
expect(process.listenerCount('SIGUSR1')).toBe(before + 1);
|
||||
} finally {
|
||||
|
|
|
|||
|
|
@ -50,12 +50,11 @@ async function readDiskIds(): Promise<readonly string[]> {
|
|||
|
||||
describe('CronManager — persistence and resume', () => {
|
||||
it('addTask writes a JSON record to <sessionDir>/cron/<id>.json', async () => {
|
||||
const { agent } = createAgentStub();
|
||||
const { agent } = createAgentStub({ homedir: sessionDir });
|
||||
const harness = createClocks();
|
||||
const manager = new CronManager(agent, {
|
||||
clocks: harness.clocks,
|
||||
pollIntervalMs: null,
|
||||
sessionDir,
|
||||
});
|
||||
|
||||
const task = manager.addTask({
|
||||
|
|
@ -82,12 +81,11 @@ describe('CronManager — persistence and resume', () => {
|
|||
});
|
||||
|
||||
it('removeTasks deletes the JSON record', async () => {
|
||||
const { agent } = createAgentStub();
|
||||
const { agent } = createAgentStub({ homedir: sessionDir });
|
||||
const harness = createClocks();
|
||||
const manager = new CronManager(agent, {
|
||||
clocks: harness.clocks,
|
||||
pollIntervalMs: null,
|
||||
sessionDir,
|
||||
});
|
||||
|
||||
const task = manager.addTask({ cron: '*/5 * * * *', prompt: 'a' });
|
||||
|
|
@ -103,12 +101,11 @@ describe('CronManager — persistence and resume', () => {
|
|||
|
||||
it('loadFromDisk re-adopts tasks with original id and createdAt', async () => {
|
||||
// First "session": schedule two recurring tasks.
|
||||
const stubA = createAgentStub();
|
||||
const stubA = createAgentStub({ homedir: sessionDir });
|
||||
const clockA = createClocks();
|
||||
const managerA = new CronManager(stubA.agent, {
|
||||
clocks: clockA.clocks,
|
||||
pollIntervalMs: null,
|
||||
sessionDir,
|
||||
});
|
||||
const t1 = managerA.addTask({ cron: '*/5 * * * *', prompt: 'a' });
|
||||
const t2 = managerA.addTask({
|
||||
|
|
@ -120,12 +117,11 @@ describe('CronManager — persistence and resume', () => {
|
|||
await managerA.stop();
|
||||
|
||||
// Second "session": fresh manager, same sessionDir.
|
||||
const stubB = createAgentStub();
|
||||
const stubB = createAgentStub({ homedir: sessionDir });
|
||||
const clockB = createClocks(clockA.now() + 60_000);
|
||||
const managerB = new CronManager(stubB.agent, {
|
||||
clocks: clockB.clocks,
|
||||
pollIntervalMs: null,
|
||||
sessionDir,
|
||||
});
|
||||
expect(managerB.store.list()).toEqual([]);
|
||||
await managerB.loadFromDisk();
|
||||
|
|
@ -146,12 +142,11 @@ describe('CronManager — persistence and resume', () => {
|
|||
|
||||
it('recurring task missed during downtime fires once with coalescedCount > 1', async () => {
|
||||
// Session A: create a `*/5 * * * *` task.
|
||||
const stubA = createAgentStub();
|
||||
const stubA = createAgentStub({ homedir: sessionDir });
|
||||
const clockA = createClocks();
|
||||
const managerA = new CronManager(stubA.agent, {
|
||||
clocks: clockA.clocks,
|
||||
pollIntervalMs: null,
|
||||
sessionDir,
|
||||
});
|
||||
managerA.addTask({ cron: '*/5 * * * *', prompt: 'check' });
|
||||
await managerA.flushPersist();
|
||||
|
|
@ -160,12 +155,11 @@ describe('CronManager — persistence and resume', () => {
|
|||
// Session B: 23 minutes later (≈ 4 ideal fires missed: t+5, +10,
|
||||
// +15, +20). With createdAt as the baseline the scheduler must
|
||||
// collapse them into one fire.
|
||||
const stubB = createAgentStub();
|
||||
const stubB = createAgentStub({ homedir: sessionDir });
|
||||
const clockB = createClocks(clockA.now() + 23 * 60_000);
|
||||
const managerB = new CronManager(stubB.agent, {
|
||||
clocks: clockB.clocks,
|
||||
pollIntervalMs: null,
|
||||
sessionDir,
|
||||
});
|
||||
await managerB.loadFromDisk();
|
||||
managerB.tick();
|
||||
|
|
@ -183,12 +177,11 @@ describe('CronManager — persistence and resume', () => {
|
|||
it('one-shot scheduled in the past fires once on resume and the file is removed', async () => {
|
||||
// Session A: schedule a one-shot 5 minutes ahead of clockA's now,
|
||||
// then quit before it fires.
|
||||
const stubA = createAgentStub();
|
||||
const stubA = createAgentStub({ homedir: sessionDir });
|
||||
const clockA = createClocks(WALL_ANCHOR);
|
||||
const managerA = new CronManager(stubA.agent, {
|
||||
clocks: clockA.clocks,
|
||||
pollIntervalMs: null,
|
||||
sessionDir,
|
||||
});
|
||||
// Compute a cron expression that lands 5 minutes in the future
|
||||
// using the harness's anchor (Nov 14 2023 22:13:20 UTC). The next
|
||||
|
|
@ -204,12 +197,11 @@ describe('CronManager — persistence and resume', () => {
|
|||
|
||||
// Session B: 10 minutes after the anchor — the one-shot's ideal
|
||||
// fire (anchor + 100s ≈ 22:15:00) is in the past.
|
||||
const stubB = createAgentStub();
|
||||
const stubB = createAgentStub({ homedir: sessionDir });
|
||||
const clockB = createClocks(clockA.now() + 10 * 60_000);
|
||||
const managerB = new CronManager(stubB.agent, {
|
||||
clocks: clockB.clocks,
|
||||
pollIntervalMs: null,
|
||||
sessionDir,
|
||||
});
|
||||
await managerB.loadFromDisk();
|
||||
managerB.tick();
|
||||
|
|
@ -237,12 +229,11 @@ describe('CronManager — persistence and resume', () => {
|
|||
// ideal fire, tick once. The scheduler's onAdvanceCursor callback
|
||||
// must stamp `lastFiredAt` on the persisted record so session B
|
||||
// doesn't re-coalesce that already-delivered occurrence.
|
||||
const stubA = createAgentStub();
|
||||
const stubA = createAgentStub({ homedir: sessionDir });
|
||||
const clockA = createClocks(WALL_ANCHOR);
|
||||
const managerA = new CronManager(stubA.agent, {
|
||||
clocks: clockA.clocks,
|
||||
pollIntervalMs: null,
|
||||
sessionDir,
|
||||
});
|
||||
const task = managerA.addTask({ cron: '*/5 * * * *', prompt: 'check' });
|
||||
await managerA.flushPersist();
|
||||
|
|
@ -271,12 +262,11 @@ describe('CronManager — persistence and resume', () => {
|
|||
// the one that was already delivered in session A). With the
|
||||
// persistence, session A's fire is skipped on resume so the
|
||||
// count is strictly lower.
|
||||
const stubB = createAgentStub();
|
||||
const stubB = createAgentStub({ homedir: sessionDir });
|
||||
const clockB = createClocks(WALL_ANCHOR + 23 * 60_000);
|
||||
const managerB = new CronManager(stubB.agent, {
|
||||
clocks: clockB.clocks,
|
||||
pollIntervalMs: null,
|
||||
sessionDir,
|
||||
});
|
||||
await managerB.loadFromDisk();
|
||||
managerB.tick();
|
||||
|
|
@ -297,12 +287,11 @@ describe('CronManager — persistence and resume', () => {
|
|||
// (clock skew or a bench env mistake) the scheduler must not skip
|
||||
// legitimately-due fires. The sanity gate ignores the bogus value
|
||||
// and falls back to `createdAt`, matching pre-persistence behaviour.
|
||||
const stubA = createAgentStub();
|
||||
const stubA = createAgentStub({ homedir: sessionDir });
|
||||
const clockA = createClocks();
|
||||
const managerA = new CronManager(stubA.agent, {
|
||||
clocks: clockA.clocks,
|
||||
pollIntervalMs: null,
|
||||
sessionDir,
|
||||
});
|
||||
const task = managerA.addTask({ cron: '*/5 * * * *', prompt: 'check' });
|
||||
await managerA.flushPersist();
|
||||
|
|
@ -320,12 +309,11 @@ describe('CronManager — persistence and resume', () => {
|
|||
|
||||
// Session B: 23 minutes later. Even though lastFiredAt is in the
|
||||
// future, the scheduler must still fire (sanity gate ignores it).
|
||||
const stubB = createAgentStub();
|
||||
const stubB = createAgentStub({ homedir: sessionDir });
|
||||
const clockB = createClocks(clockA.now() + 23 * 60_000);
|
||||
const managerB = new CronManager(stubB.agent, {
|
||||
clocks: clockB.clocks,
|
||||
pollIntervalMs: null,
|
||||
sessionDir,
|
||||
});
|
||||
await managerB.loadFromDisk();
|
||||
managerB.tick();
|
||||
|
|
@ -341,7 +329,7 @@ describe('CronManager — persistence and resume', () => {
|
|||
it('no sessionDir = pure in-memory: no FS side effects, loadFromDisk is a no-op', async () => {
|
||||
const { agent } = createAgentStub();
|
||||
const harness = createClocks();
|
||||
// Construct without sessionDir.
|
||||
// Construct without homedir.
|
||||
const manager = new CronManager(agent, {
|
||||
clocks: harness.clocks,
|
||||
pollIntervalMs: null,
|
||||
|
|
|
|||
|
|
@ -5,9 +5,9 @@
|
|||
* never serve any purpose — default subagent profiles don't expose the
|
||||
* Cron tools to the LLM. This test pins both halves of the fix:
|
||||
*
|
||||
* 1. `agent.cron` is still constructed (consumers reach into
|
||||
* `agent.cron.store` and the field is `readonly` on the type).
|
||||
* 2. `cron.start()` is skipped for `type: 'sub'` so the SIGUSR1
|
||||
* 1. `agent.cron` is `null` for `type: 'sub'` so no timers or
|
||||
* listeners leak for ephemeral agents.
|
||||
* 2. `cron.start()` is never called for subagents, so the SIGUSR1
|
||||
* listener count stays put.
|
||||
* 3. The three Cron tools (`CronCreate` / `CronList` / `CronDelete`)
|
||||
* are NOT registered in the subagent's tool manager.
|
||||
|
|
@ -39,9 +39,8 @@ describe('Agent + Cron — subagent suppression', () => {
|
|||
const before = process.listenerCount('SIGUSR1');
|
||||
const ctx = testAgent({ type: 'sub' });
|
||||
|
||||
// Manager is still wired so `agent.cron.store` access stays safe.
|
||||
expect(ctx.agent.cron).toBeDefined();
|
||||
expect(ctx.agent.cron.store).toBeDefined();
|
||||
// Subagents do not get a CronManager instance at all.
|
||||
expect(ctx.agent.cron).toBeNull();
|
||||
|
||||
// start() was not called — no SIGUSR1 binding accrued.
|
||||
expect(process.listenerCount('SIGUSR1')).toBe(before);
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import { expect, onTestFinished, vi } from 'vitest';
|
|||
|
||||
import {
|
||||
Agent,
|
||||
type AgentConfig,
|
||||
type AgentOptions,
|
||||
type AgentRecord,
|
||||
type AgentRecordPersistence,
|
||||
} from '../../../src/agent';
|
||||
|
|
@ -65,7 +65,7 @@ type RpcLogEntry = RpcSnapshotEntry & {
|
|||
};
|
||||
|
||||
type PromiseAgentAPI = PromisifyMethods<AgentAPI>;
|
||||
type GenerateFn = NonNullable<AgentConfig['generate']>;
|
||||
type GenerateFn = NonNullable<AgentOptions['generate']>;
|
||||
|
||||
type TestToolResult = ExecutableToolResult & {
|
||||
readonly content?: unknown;
|
||||
|
|
@ -93,14 +93,14 @@ export interface TestAgentOptions {
|
|||
readonly runtime?: RuntimeConfig | undefined;
|
||||
readonly compactionStrategy?: CompactionStrategy | undefined;
|
||||
readonly generate?: GenerateFn | undefined;
|
||||
readonly hookEngine?: AgentConfig['hookEngine'];
|
||||
readonly type?: AgentConfig['type'];
|
||||
readonly permission?: AgentConfig['permission'];
|
||||
readonly hookEngine?: AgentOptions['hookEngine'];
|
||||
readonly type?: AgentOptions['type'];
|
||||
readonly permission?: AgentOptions['permission'];
|
||||
readonly providerManager?: ProviderManager;
|
||||
readonly initialConfig?: KimiConfig;
|
||||
readonly providerManagerOverrides?: Omit<ConstructorParameters<typeof ProviderManager>[0], 'config'>;
|
||||
readonly sessionId?: string;
|
||||
readonly subagentHost?: AgentConfig['subagentHost'];
|
||||
readonly subagentHost?: AgentOptions['subagentHost'];
|
||||
readonly onEvent?: ((event: AgentRecord) => AgentRecord | undefined) | undefined;
|
||||
readonly persistence?: AgentRecordPersistence | undefined;
|
||||
readonly telemetry?: TelemetryClient | undefined;
|
||||
|
|
@ -198,7 +198,7 @@ export class AgentTestContext {
|
|||
// we register cleanup transparently without forcing every test to
|
||||
// remember an afterEach.
|
||||
onTestFinished(async () => {
|
||||
await this.agent.cron.stop();
|
||||
await this.agent.cron?.stop();
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import {
|
|||
type StreamedMessagePart,
|
||||
} from '@moonshot-ai/kosong';
|
||||
|
||||
import type { AgentConfig } from '../../../src/agent';
|
||||
import type { AgentOptions } from '../../../src/agent';
|
||||
import { estimateTokensForMessages } from '../../../src/utils/tokens';
|
||||
import {
|
||||
generateInputSnapshot,
|
||||
|
|
@ -15,7 +15,7 @@ import {
|
|||
type GenerateCall,
|
||||
} from './snapshots';
|
||||
|
||||
type GenerateFn = NonNullable<AgentConfig['generate']>;
|
||||
type GenerateFn = NonNullable<AgentOptions['generate']>;
|
||||
|
||||
interface ScriptedResponse {
|
||||
readonly parts: readonly StreamedMessagePart[];
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import type { ToolCall } from '@moonshot-ai/kosong';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { HookEngine } from '../../src/agent/hooks';
|
||||
import { HookEngine } from '../../src/session/hooks';
|
||||
import type { SessionSubagentHost } from '../../src/session/subagent-host';
|
||||
import { createFakeKaos } from '../tools/fixtures/fake-kaos';
|
||||
import { createCommandKaos, testAgent } from './harness/agent';
|
||||
|
|
|
|||
|
|
@ -15,8 +15,8 @@ import {
|
|||
} from '@moonshot-ai/kosong';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { HookEngine } from '../../src/agent/hooks';
|
||||
import type { AgentConfig } from '../../src/agent';
|
||||
import { HookEngine } from '../../src/session/hooks';
|
||||
import type { AgentOptions } from '../../src/agent';
|
||||
import type { Logger, LogPayload } from '../../src/logging';
|
||||
import {
|
||||
estimateTokens,
|
||||
|
|
@ -28,7 +28,7 @@ import { createFakeKaos } from '../tools/fixtures/fake-kaos';
|
|||
import { createCommandKaos, testAgent, type TestAgentOptions } from './harness/agent';
|
||||
import { executeTool } from '../tools/fixtures/execute-tool';
|
||||
|
||||
type GenerateFn = NonNullable<AgentConfig['generate']>;
|
||||
type GenerateFn = NonNullable<AgentOptions['generate']>;
|
||||
|
||||
interface CapturedLogEntry {
|
||||
readonly level: 'error' | 'warn' | 'info' | 'debug';
|
||||
|
|
|
|||
|
|
@ -73,9 +73,9 @@ custom_headers = { "X-Test" = "1" }
|
|||
storage: 'file',
|
||||
key: 'oauth/custom-kimi-code',
|
||||
});
|
||||
expect(session?.config.runtime.webSearcher).toBeDefined();
|
||||
expect(session?.options.runtime.webSearcher).toBeDefined();
|
||||
|
||||
await session!.config.runtime.webSearcher!.search('kimi');
|
||||
await session!.options.runtime.webSearcher!.search('kimi');
|
||||
|
||||
expect(getAccessToken).toHaveBeenCalledWith();
|
||||
const init = fetchImpl.mock.calls[0]?.[1] as RequestInit;
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import type { ContentPart } from '@moonshot-ai/kosong';
|
|||
|
||||
// Dynamic-import contract: locks the public shape of the future HookEngine
|
||||
// without forcing TS module resolution to find a file that doesn't exist yet.
|
||||
const ENGINE_MODULE = '../../src/agent/hooks/engine' as string;
|
||||
const ENGINE_MODULE = '../../src/session/hooks/engine' as string;
|
||||
|
||||
type HookDef = {
|
||||
event: string;
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import { describe, expect, it } from 'vitest';
|
|||
// Dynamic-import contract: locks the Agent <-> HookEngine integration shape
|
||||
// (engine ctor, trigger surface, summary, wire callbacks, event helpers,
|
||||
// config round-trip) before the implementation lands.
|
||||
const ENGINE_MODULE = '../../src/agent/hooks/engine' as string;
|
||||
const ENGINE_MODULE = '../../src/session/hooks/engine' as string;
|
||||
const CONFIG_MODULE = '../../src/config' as string;
|
||||
|
||||
type HookDef = {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
const RUNNER_MODULE = '../../src/agent/hooks/runner' as string;
|
||||
const RUNNER_MODULE = '../../src/session/hooks/runner' as string;
|
||||
|
||||
interface HookResult {
|
||||
action: 'allow' | 'block';
|
||||
|
|
|
|||
|
|
@ -488,7 +488,7 @@ describe('McpConnectionManager', () => {
|
|||
transport: 'stdio',
|
||||
command: process.execPath,
|
||||
args: [crashAfterConnectFixture],
|
||||
env: { KIMI_TEST_MCP_EXIT_AFTER_MS: '50', KIMI_TEST_MCP_STDERR: 'fatal: out of memory' },
|
||||
env: { KIMI_TEST_MCP_EXIT_AFTER_MS: '500', KIMI_TEST_MCP_STDERR: 'fatal: out of memory' },
|
||||
startupTimeoutMs: 4_000,
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ describe('Session.close stops cron', () => {
|
|||
skills: { explicitDirs: [join(workDir, 'missing-skills')] },
|
||||
});
|
||||
const main = await session.createMain();
|
||||
const stopSpy = vi.spyOn(main.cron, 'stop');
|
||||
const stopSpy = vi.spyOn(main.cron!, 'stop');
|
||||
|
||||
await session.close();
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue