diff --git a/.changeset/split-runtime-kaos.md b/.changeset/split-runtime-kaos.md new file mode 100644 index 0000000000..59d369ab2d --- /dev/null +++ b/.changeset/split-runtime-kaos.md @@ -0,0 +1,6 @@ +--- +"@moonshot-ai/agent-core": patch +"@moonshot-ai/kimi-code": patch +--- + +Split `RuntimeConfig` into `Kaos` and `ToolServices` and update all references accordingly. diff --git a/packages/agent-core/src/agent/config/index.ts b/packages/agent-core/src/agent/config/index.ts index 83d8c20c23..45aca21367 100644 --- a/packages/agent-core/src/agent/config/index.ts +++ b/packages/agent-core/src/agent/config/index.ts @@ -23,7 +23,7 @@ export class ConfigState { private _systemPrompt: string = ''; constructor(protected readonly agent: Agent) { - this._cwd = agent.runtime.kaos.getcwd(); + this._cwd = agent.kaos.getcwd(); this._modelAlias = agent.modelProvider?.defaultModel; } @@ -40,7 +40,7 @@ export class ConfigState { }); if (changed.cwd) { this._cwd = changed.cwd; - void this.agent.runtime.kaos.chdir(changed.cwd); + void this.agent.kaos.chdir(changed.cwd); } if (changed.modelAlias) { this._modelAlias = changed.modelAlias; diff --git a/packages/agent-core/src/agent/index.ts b/packages/agent-core/src/agent/index.ts index f9b0d15f6e..6151c7bad2 100644 --- a/packages/agent-core/src/agent/index.ts +++ b/packages/agent-core/src/agent/index.ts @@ -17,7 +17,7 @@ import type { EnabledPluginSessionStart } from '#/plugin'; import type { McpConnectionManager } from '../mcp'; import type { PreparedSystemPromptContext, ResolvedAgentProfile } from '../profile'; import type { ModelProvider } from '../session/provider-manager'; -import type { RuntimeConfig } from '../runtime-types'; +import type { ToolServices } from '../runtime-types'; import type { SessionSubagentHost } from '../session/subagent-host'; import type { SkillRegistry } from '../skill'; import { noopTelemetryClient, type TelemetryClient } from '../telemetry'; @@ -54,6 +54,7 @@ import { } from './turn/kosong-llm'; import { UsageRecorder } from './usage'; import { resolveCompletionBudget } from '../utils/completion-budget'; +import type { Kaos } from '@moonshot-ai/kaos'; export type { AgentRecord, AgentRecordPersistence } from './records'; export type { BuiltinTool, ToolInfo, ToolSource, UserToolRegistration } from './tool'; @@ -61,13 +62,14 @@ export type { BuiltinTool, ToolInfo, ToolSource, UserToolRegistration } from './ export type AgentType = 'main' | 'sub' | 'independent'; export interface AgentOptions { - readonly runtime: RuntimeConfig; + readonly kaos: Kaos; readonly config?: KimiConfig; readonly homedir?: string; readonly rpc?: Partial; readonly persistence?: AgentRecordPersistence; readonly type?: AgentType; readonly generate?: typeof generate; + readonly toolServices?: ToolServices; readonly compactionStrategy?: CompactionStrategy; readonly modelProvider?: ModelProvider | undefined; readonly subagentHost?: SessionSubagentHost | undefined; @@ -82,10 +84,11 @@ export interface AgentOptions { export class Agent { readonly type: AgentType; - readonly runtime: RuntimeConfig; + readonly kaos: Kaos; readonly kimiConfig?: KimiConfig; readonly homedir?: string; readonly rpc?: Partial; + readonly toolServices?: ToolServices; readonly pluginSessionStarts: readonly EnabledPluginSessionStart[]; readonly rawGenerate: typeof generate; readonly modelProvider?: ModelProvider; @@ -115,10 +118,11 @@ export class Agent { constructor(options: AgentOptions) { this.type = options.type ?? 'main'; - this.runtime = options.runtime; + this.kaos = options.kaos; this.kimiConfig = options.config; this.homedir = options.homedir; this.rpc = options.rpc; + this.toolServices = options.toolServices; this.pluginSessionStarts = options.pluginSessionStarts ?? []; this.rawGenerate = options.generate ?? generate; this.modelProvider = options.modelProvider; @@ -238,7 +242,7 @@ export class Agent { useProfile(profile: ResolvedAgentProfile, context?: PreparedSystemPromptContext): void { const systemPrompt = profile.systemPrompt({ - osEnv: this.runtime.kaos.osEnv, + osEnv: this.kaos.osEnv, cwd: this.config.cwd, skills: this.skills?.registry, cwdListing: context?.cwdListing, diff --git a/packages/agent-core/src/agent/permission/policies/file-access-ask.ts b/packages/agent-core/src/agent/permission/policies/file-access-ask.ts index f014e73d71..b6eebef56d 100644 --- a/packages/agent-core/src/agent/permission/policies/file-access-ask.ts +++ b/packages/agent-core/src/agent/permission/policies/file-access-ask.ts @@ -36,7 +36,7 @@ export class GitControlPathAccessAskPermissionPolicy implements PermissionPolicy async evaluate(context: PermissionPolicyContext): Promise { const cwd = this.agent.config.cwd; if (cwd.length === 0) return; - const pathClass = this.agent.runtime.kaos.pathClass(); + const pathClass = this.agent.kaos.pathClass(); const accesses = fileAccesses(context); if (accesses.length === 0) return; @@ -50,7 +50,7 @@ export class GitControlPathAccessAskPermissionPolicy implements PermissionPolicy }; } - const marker = await findGitWorkTreeMarker(this.agent.runtime.kaos, cwd); + const marker = await findGitWorkTreeMarker(this.agent.kaos, cwd); if (marker === null) return; const access = accesses.find((fileAccess) => { return isGitControlPath(fileAccess.path, marker, pathClass); @@ -71,7 +71,7 @@ export class CwdOutsideFileWriteAskPermissionPolicy implements PermissionPolicy evaluate(context: PermissionPolicyContext): PermissionPolicyResult | undefined { const cwd = this.agent.config.cwd; if (cwd.length === 0) return; - const pathClass = this.agent.runtime.kaos.pathClass(); + const pathClass = this.agent.kaos.pathClass(); const access = writeFileAccesses(context).find((fileAccess) => { return !isWithinDirectory(fileAccess.path, cwd, pathClass); }); diff --git a/packages/agent-core/src/agent/permission/policies/git-cwd-write-approve.ts b/packages/agent-core/src/agent/permission/policies/git-cwd-write-approve.ts index ae76fb9130..14c72f351a 100644 --- a/packages/agent-core/src/agent/permission/policies/git-cwd-write-approve.ts +++ b/packages/agent-core/src/agent/permission/policies/git-cwd-write-approve.ts @@ -12,7 +12,7 @@ export class GitCwdWriteApprovePermissionPolicy implements PermissionPolicy { async evaluate(context: PermissionPolicyContext): Promise { const toolName = context.toolCall.name; if (toolName !== 'Write' && toolName !== 'Edit') return; - if (this.agent.runtime.kaos.pathClass() !== 'posix') return; + if (this.agent.kaos.pathClass() !== 'posix') return; const cwd = this.agent.config.cwd; if (cwd.length === 0) return; @@ -23,7 +23,7 @@ export class GitCwdWriteApprovePermissionPolicy implements PermissionPolicy { return; } - const marker = await findGitWorkTreeMarker(this.agent.runtime.kaos, cwd); + const marker = await findGitWorkTreeMarker(this.agent.kaos, cwd); if (marker === null) return; return { diff --git a/packages/agent-core/src/agent/plan/index.ts b/packages/agent-core/src/agent/plan/index.ts index 6051f1c708..fdaafdbb1a 100644 --- a/packages/agent-core/src/agent/plan/index.ts +++ b/packages/agent-core/src/agent/plan/index.ts @@ -107,7 +107,7 @@ export class PlanMode { if (!this._planId || !this._planFilePath) return null; let content = ''; try { - content = await this.agent.runtime.kaos.readText(this._planFilePath); + content = await this.agent.kaos.readText(this._planFilePath); } catch (error) { if (!isMissingFileError(error)) throw error; } @@ -120,11 +120,11 @@ export class PlanMode { private async writeEmptyPlanFile(path: string): Promise { await this.ensurePlanDirectory(path); - await this.agent.runtime.kaos.writeText(path, ''); + await this.agent.kaos.writeText(path, ''); } private async ensurePlanDirectory(path: string): Promise { - await this.agent.runtime.kaos.mkdir(dirname(path), { + await this.agent.kaos.mkdir(dirname(path), { parents: true, existOk: true, }); diff --git a/packages/agent-core/src/agent/tool/index.ts b/packages/agent-core/src/agent/tool/index.ts index 77c7dde3f2..550cfeba8c 100644 --- a/packages/agent-core/src/agent/tool/index.ts +++ b/packages/agent-core/src/agent/tool/index.ts @@ -340,9 +340,10 @@ export class ToolManager { return { ...this.store }; } - initializeBuiltinTools(): void { + initializeBuiltinTools() { const { - runtime: { kaos, urlFetcher, webSearcher }, + kaos, + toolServices, config: { cwd, provider, modelCapabilities }, background, } = this.agent; @@ -365,7 +366,7 @@ export class ToolManager { new b.EditTool(kaos, workspace), new b.GrepTool(kaos, workspace), new b.GlobTool(kaos, workspace), - new b.BashTool(kaos, cwd, kaos.osEnv, background, { + new b.BashTool(kaos, cwd, background, { allowBackground, }), (modelCapabilities.image_in || modelCapabilities.video_in) && @@ -392,8 +393,8 @@ export class ToolManager { log: this.agent.log, }, ), - webSearcher && new b.WebSearchTool(webSearcher), - urlFetcher && new b.FetchURLTool(urlFetcher), + toolServices?.webSearcher && new b.WebSearchTool(toolServices.webSearcher), + toolServices?.urlFetcher && new b.FetchURLTool(toolServices.urlFetcher), ] .filter((tool) => !!tool) .map((tool) => [tool.name, tool] as const), diff --git a/packages/agent-core/src/index.ts b/packages/agent-core/src/index.ts index 7e78d3103c..e13c5f64a9 100644 --- a/packages/agent-core/src/index.ts +++ b/packages/agent-core/src/index.ts @@ -39,7 +39,7 @@ export type { BackgroundTaskKind, BackgroundTaskStatus, } from './tools/background/manager'; -export type { RuntimeConfig } from './runtime-types'; +export type { ToolServices } from './runtime-types'; export { SingleModelProvider } from './session/provider-manager'; export type { BearerTokenProvider, diff --git a/packages/agent-core/src/rpc/core-impl.ts b/packages/agent-core/src/rpc/core-impl.ts index 719fd7e81f..c0aa30dd74 100644 --- a/packages/agent-core/src/rpc/core-impl.ts +++ b/packages/agent-core/src/rpc/core-impl.ts @@ -9,7 +9,6 @@ import { MoonshotFetchURLProvider } from '#/tools/providers/moonshot-fetch-url'; import { MoonshotWebSearchProvider } from '#/tools/providers/moonshot-web-search'; import type { PromisableMethods } from '#/utils/types'; import { getCoreVersion } from '#/version'; -import { KaosShellNotFoundError, LocalKaos } from '@moonshot-ai/kaos'; import { resolveThinkingLevel } from '../agent/config/thinking'; import { ensureKimiHome, @@ -23,7 +22,7 @@ import { } from '../config'; import type { Logger } from '../logging/types'; import { resolveSessionMcpConfig, type SessionMcpConfig } from '../mcp'; -import type { RuntimeConfig } from '../runtime-types'; +import type { ToolServices } from '../runtime-types'; import { Session, type SessionMeta, type SessionSkillConfig } from '../session'; import { exportSessionDirectory } from '../session/export'; import { @@ -84,6 +83,7 @@ import type { import type { ResumedAgentState, ResumeSessionResult } from './resumed'; import type { SDKRPC } from './sdk-api'; import { proxyWithExtraPayload } from './types'; +import { KaosShellNotFoundError, LocalKaos, type Kaos } from '@moonshot-ai/kaos'; const KIMI_CODE_PROVIDER_NAME = 'managed:kimi-code'; @@ -96,7 +96,7 @@ type UpdateSessionMetadataRequest = SessionScopedPayload | undefined; readonly resolveOAuthTokenProvider?: OAuthTokenProviderResolver | undefined; readonly skillDirs?: readonly string[]; @@ -110,7 +110,8 @@ export class KimiCore implements PromisableMethods { readonly sessions = new Map(); readonly telemetry: TelemetryClient; - private runtime: RuntimeConfig | undefined; + private kaos: Promise; + private runtime: ToolServices | undefined; private config: KimiConfig; private readonly userHomeDir: string; private readonly kimiRequestHeaders: Record | undefined; @@ -131,6 +132,12 @@ export class KimiCore implements PromisableMethods { homeDir: this.homeDir, configPath: options.configPath, }); + this.kaos = LocalKaos.create().catch((error: unknown) => { + if (error instanceof KaosShellNotFoundError) { + throw new KimiError(ErrorCodes.SHELL_GIT_BASH_NOT_FOUND, error.message); + } + throw error; + }); this.runtime = options.runtime; this.kimiRequestHeaders = options.kimiRequestHeaders; this.resolveOAuthTokenProvider = options.resolveOAuthTokenProvider; @@ -179,7 +186,8 @@ export class KimiCore implements PromisableMethods { // ctor block throws, `session.close()` releases the sink (and mcp). const runtime = await this.resolveRuntime(config); const session = new Session({ - runtime: { ...runtime, kaos: runtime.kaos.withCwd(workDir) }, + kaos: (await this.kaos).withCwd(workDir), + toolServices: runtime, config, id, homedir: summary.sessionDir, @@ -259,7 +267,8 @@ export class KimiCore implements PromisableMethods { const mcpConfig = this.mergePluginMcpConfig(baseMcpConfig); const runtime = await this.resolveRuntime(config); const session = new Session({ - runtime: { ...runtime, kaos: runtime.kaos.withCwd(summary.workDir) }, + kaos: (await this.kaos).withCwd(summary.workDir), + toolServices: runtime, config, id: summary.id, homedir: summary.sessionDir, @@ -632,7 +641,7 @@ export class KimiCore implements PromisableMethods { ); } - private async resolveRuntime(config: KimiConfig): Promise { + private async resolveRuntime(config: KimiConfig): Promise { if (this.runtime !== undefined) return this.runtime; const runtime = await createRuntimeConfig({ config, @@ -729,20 +738,12 @@ async function createRuntimeConfig(input: { readonly config: KimiConfig; readonly kimiRequestHeaders?: Record | undefined; readonly resolveOAuthTokenProvider?: OAuthTokenProviderResolver | undefined; -}): Promise { +}): Promise { const localFetcher = new LocalFetchURLProvider(); const searchService = input.config.services?.moonshotSearch; const fetchService = input.config.services?.moonshotFetch; - const kaos = await LocalKaos.create().catch((error: unknown) => { - if (error instanceof KaosShellNotFoundError) { - throw new KimiError(ErrorCodes.SHELL_GIT_BASH_NOT_FOUND, error.message); - } - throw error; - }); - return { - kaos, urlFetcher: fetchService?.baseUrl === undefined ? localFetcher diff --git a/packages/agent-core/src/runtime-types.ts b/packages/agent-core/src/runtime-types.ts index 960fff7efc..7732feae18 100644 --- a/packages/agent-core/src/runtime-types.ts +++ b/packages/agent-core/src/runtime-types.ts @@ -1,9 +1,6 @@ -import type { Kaos } from '@moonshot-ai/kaos'; - import type { UrlFetcher, WebSearchProvider } from './tools/builtin'; -export interface RuntimeConfig { - readonly kaos: Kaos; +export interface ToolServices { readonly urlFetcher?: UrlFetcher | undefined; readonly webSearcher?: WebSearchProvider | undefined; } diff --git a/packages/agent-core/src/session/index.ts b/packages/agent-core/src/session/index.ts index 222872f271..7192cc6140 100644 --- a/packages/agent-core/src/session/index.ts +++ b/packages/agent-core/src/session/index.ts @@ -1,5 +1,6 @@ import { homedir } from 'node:os'; import { join } from 'pathe'; +import type { Kaos } from '@moonshot-ai/kaos'; import { ErrorCodes, KimiError } from '#/errors'; import { getRootLogger, log } from '#/logging/logger'; @@ -27,7 +28,7 @@ import { type ResolvedAgentProfile, } from '../profile'; import type { ProviderManager } from './provider-manager'; -import type { RuntimeConfig } from '../runtime-types'; +import type { ToolServices } from '../runtime-types'; import { registerBuiltinSkills, resolveSkillRoots, @@ -40,12 +41,13 @@ import { noopTelemetryClient, type TelemetryClient } from '../telemetry'; import { SessionSubagentHost } from './subagent-host'; export interface SessionOptions { - readonly runtime: RuntimeConfig; + readonly kaos: Kaos; readonly config?: KimiConfig; readonly id?: string | undefined; readonly homedir: string; readonly kimiHomeDir?: string; readonly rpc: SDKSessionRPC; + readonly toolServices?: ToolServices; readonly initializeMainAgent?: boolean | undefined; readonly providerManager?: ProviderManager | undefined; readonly background?: BackgroundConfig | undefined; @@ -122,7 +124,7 @@ export class Session { (options.id === undefined ? log : log.createChild({ sessionId: options.id })); this.rpc = options.rpc; this.hookEngine = new HookEngine(options.hooks, { - cwd: options.runtime.kaos.getcwd(), + cwd: options.kaos.getcwd(), sessionId: options.id, }); this.telemetry = options.telemetry ?? noopTelemetryClient; @@ -247,7 +249,7 @@ export class Session { agent: Agent, profile: ResolvedAgentProfile, ): Promise { - const context = await prepareSystemPromptContext(agent.runtime.kaos); + const context = await prepareSystemPromptContext(agent.kaos); agent.useProfile(profile, context); } @@ -266,7 +268,7 @@ export class Session { }); await handle.completion; - const agentsMd = await loadAgentsMd(mainAgent.runtime.kaos); + const agentsMd = await loadAgentsMd(mainAgent.kaos); mainAgent.context.appendSystemReminder(initCompletionReminder(agentsMd), { kind: 'injection', variant: 'init', @@ -295,15 +297,15 @@ export class Session { writeMetadata() { const text = JSON.stringify(this.metadata, null, 2); const write = async () => { - await this.options.runtime.kaos.mkdir(this.options.homedir, { parents: true, existOk: true }); - await this.options.runtime.kaos.writeText(this.metadataPath, text); + await this.options.kaos.mkdir(this.options.homedir, { parents: true, existOk: true }); + await this.options.kaos.writeText(this.metadataPath, text); }; this.writeMetadataPromise = this.writeMetadataPromise.then(write, write); return this.writeMetadataPromise; } async readMetadata() { - const text = await this.options.runtime.kaos.readText(this.metadataPath); + const text = await this.options.kaos.readText(this.metadataPath); this.metadata = JSON.parse(text); return this.metadata; } @@ -323,7 +325,7 @@ export class Session { const roots = await resolveSkillRoots({ paths: { userHomeDir: this.options.skills?.userHomeDir ?? homedir(), - workDir: this.options.runtime.kaos.getcwd(), + workDir: this.options.kaos.getcwd(), }, explicitDirs: this.options.skills?.explicitDirs, extraDirs: this.options.skills?.extraDirs, @@ -405,10 +407,13 @@ export class Session { config: Partial = {}, parentAgentId: string | null = null, ): Agent { + const parentAgent = parentAgentId !== null ? this.agents.get(parentAgentId) : undefined; + const cwd = parentAgent?.config.cwd ?? this.options.kaos.getcwd(); return new Agent({ ...config, type, - runtime: this.options.runtime, + kaos: this.options.kaos.withCwd(cwd), + toolServices: this.options.toolServices, config: this.options.config, homedir, skills: this.skills, diff --git a/packages/agent-core/src/session/subagent-host.ts b/packages/agent-core/src/session/subagent-host.ts index 97c425ad2a..49861e9d2e 100644 --- a/packages/agent-core/src/session/subagent-host.ts +++ b/packages/agent-core/src/session/subagent-host.ts @@ -228,7 +228,7 @@ export class SessionSubagentHost { // in the repository before searching. let childPrompt = options.prompt; if (profileName === 'explore') { - const gitContext = await collectGitContext(child.runtime.kaos, child.config.cwd); + const gitContext = await collectGitContext(child.kaos, child.config.cwd); if (gitContext) childPrompt = `${gitContext}\n\n${childPrompt}`; } const origin: PromptOrigin = options.origin ?? { kind: 'system_trigger', name: 'subagent' }; @@ -283,7 +283,7 @@ export class SessionSubagentHost { thinkingLevel: parent.config.thinkingLevel, }); - const context = await prepareSystemPromptContext(child.runtime.kaos); + const context = await prepareSystemPromptContext(child.kaos); child.useProfile(profile, context); } diff --git a/packages/agent-core/src/tools/builtin/shell/bash.ts b/packages/agent-core/src/tools/builtin/shell/bash.ts index 2320b2e41b..fa9a2a4078 100644 --- a/packages/agent-core/src/tools/builtin/shell/bash.ts +++ b/packages/agent-core/src/tools/builtin/shell/bash.ts @@ -26,7 +26,7 @@ import type { Readable } from 'node:stream'; import { StringDecoder } from 'node:string_decoder'; -import type { Environment, Kaos, KaosProcess } from '@moonshot-ai/kaos'; +import type { Kaos, KaosProcess } from '@moonshot-ai/kaos'; import { z } from 'zod'; import type { BuiltinTool } from '../../../agent/tool'; @@ -155,15 +155,14 @@ export class BashTool implements BuiltinTool { constructor( private readonly kaos: Kaos, private readonly cwd: string, - private readonly environment: Environment, private readonly backgroundManager?: BackgroundProcessManager, options?: { allowBackground?: boolean | undefined; }, ) { - this.isWindowsBash = this.environment.osKind === 'Windows'; + this.isWindowsBash = this.kaos.osEnv.osKind === 'Windows'; this.allowBackground = options?.allowBackground ?? this.backgroundManager !== undefined; - const rendered = renderBashDescription(this.environment.shellName); + const rendered = renderBashDescription(this.kaos.osEnv.shellName); this.description = this.allowBackground ? rendered : withoutBackgroundDescription(rendered); } @@ -189,7 +188,7 @@ export class BashTool implements BuiltinTool { private spawn(effectiveCwd: string, command: string): Promise { const shellCwd = this.isWindowsBash ? windowsPathToPosixPath(effectiveCwd) : effectiveCwd; const shellArgs = [ - this.environment.shellPath, + this.kaos.osEnv.shellPath, '-c', `cd ${shellQuote(shellCwd)} && ${command}`, ]; @@ -201,7 +200,7 @@ export class BashTool implements BuiltinTool { // to be inherited; honour an explicit ambient value when the user has // set one. GIT_TERMINAL_PROMPT: process.env['GIT_TERMINAL_PROMPT'] ?? '0', - SHELL: this.environment.shellPath, + SHELL: this.kaos.osEnv.shellPath, }; // Merge ambient env + noninteractive knobs so tools like git / node @@ -403,8 +402,8 @@ export class BashTool implements BuiltinTool { taskId = backgroundManager.register(proc, command, args.description.trim(), { reservation, shellInfo: { - shellName: this.environment.shellName, - shellPath: this.environment.shellPath, + shellName: this.kaos.osEnv.shellName, + shellPath: this.kaos.osEnv.shellPath, cwd: args.cwd ?? this.cwd, }, }); diff --git a/packages/agent-core/test/agent/harness/agent.ts b/packages/agent-core/test/agent/harness/agent.ts index 481a2de7eb..afe5e247c3 100644 --- a/packages/agent-core/test/agent/harness/agent.ts +++ b/packages/agent-core/test/agent/harness/agent.ts @@ -24,7 +24,7 @@ import type { Logger } from '../../../src/logging'; import { ProviderManager } from '../../../src/session/provider-manager'; import type { QuestionResult, RPCCallOptions, SDKAgentRPC } from '../../../src/rpc'; import type { AgentAPI } from '../../../src/rpc/core-api'; -import type { RuntimeConfig } from '../../../src/runtime-types'; +import type { ToolServices } from '../../../src/runtime-types'; import type { TelemetryClient } from '../../../src/telemetry'; import type { PromisifyMethods } from '../../../src/utils/types'; import { createFakeKaos } from '../../tools/fixtures/fake-kaos'; @@ -90,7 +90,7 @@ interface ResumeStateSnapshot { export interface TestAgentOptions { readonly kaos?: Kaos | undefined; - readonly runtime?: RuntimeConfig | undefined; + readonly runtime?: ToolServices | undefined; readonly compactionStrategy?: CompactionStrategy | undefined; readonly generate?: GenerateFn | undefined; readonly hookEngine?: AgentOptions['hookEngine']; @@ -169,14 +169,14 @@ export class AgentTestContext { ...options.providerManagerOverrides, }); - const runtime = options.runtime ?? { - kaos: options.kaos ?? testKaos, - }; + const kaos = options.kaos ?? testKaos; + const toolServices = options.runtime; const persistence = this.wrapPersistence( options.persistence ?? new InMemoryAgentRecordPersistence(), ); this.agent = new Agent({ - runtime, + kaos, + toolServices, config: this.kimiConfig, rpc: this.createRpcProxy(), persistence, @@ -726,10 +726,10 @@ export class AgentTestContext { async expectResumeMatches(): Promise { const resumed = testAgent({ + kaos: createResumeNoSideEffectKaos(this.agent.config.cwd), runtime: { - kaos: createResumeNoSideEffectKaos(this.agent.config.cwd), - urlFetcher: this.agent.runtime.urlFetcher, - webSearcher: this.agent.runtime.webSearcher, + urlFetcher: this.agent.toolServices?.urlFetcher, + webSearcher: this.agent.toolServices?.webSearcher, }, providerManager: this.options.providerManager, initialConfig: this.kimiConfig, diff --git a/packages/agent-core/test/agent/permission.test.ts b/packages/agent-core/test/agent/permission.test.ts index d6bd924407..88e63ad9b5 100644 --- a/packages/agent-core/test/agent/permission.test.ts +++ b/packages/agent-core/test/agent/permission.test.ts @@ -3576,7 +3576,7 @@ function makePermissionManager( const agent = { type: options.agentType ?? 'main', config: { cwd: options.cwd ?? '/workspace' }, - runtime: { kaos: options.kaos ?? createFakeKaos() }, + kaos: options.kaos ?? createFakeKaos(), emitStatusUpdated: vi.fn(), records: { logRecord: record }, replayBuilder: { push: vi.fn() }, @@ -3623,7 +3623,7 @@ function makePlanPermissionManager(input: { const agent = { type: 'main', config: { cwd: '/workspace' }, - runtime: { kaos: createFakeKaos() }, + kaos: createFakeKaos(), emitStatusUpdated: vi.fn(), records: { logRecord: record }, replayBuilder: { push: vi.fn() }, diff --git a/packages/agent-core/test/agent/skill-tool-manager.test.ts b/packages/agent-core/test/agent/skill-tool-manager.test.ts index 4eff1dc68d..d047457ec2 100644 --- a/packages/agent-core/test/agent/skill-tool-manager.test.ts +++ b/packages/agent-core/test/agent/skill-tool-manager.test.ts @@ -45,9 +45,7 @@ function makeAgent( toolCall: vi.fn(), } as unknown as SDKAgentRPC; const agent = new Agent({ - runtime: { - kaos: testKaos, - }, + kaos: testKaos, rpc, skills, persistence, @@ -189,7 +187,7 @@ describe('ToolManager SkillTool registration', () => { const session = new Session({ id: 'test-skill-tool', - runtime: runtime(workDir), + kaos: testKaos.withCwd(workDir), homedir: homeDir, rpc: sessionRpc(), providerManager: testProviderManager(), diff --git a/packages/agent-core/test/harness/runtime.test.ts b/packages/agent-core/test/harness/runtime.test.ts index 6c21f0adc9..a79600c3a9 100644 --- a/packages/agent-core/test/harness/runtime.test.ts +++ b/packages/agent-core/test/harness/runtime.test.ts @@ -73,9 +73,9 @@ custom_headers = { "X-Test" = "1" } storage: 'file', key: 'oauth/custom-kimi-code', }); - expect(session?.options.runtime.webSearcher).toBeDefined(); + expect(session?.options.toolServices?.webSearcher).toBeDefined(); - await session!.options.runtime.webSearcher!.search('kimi'); + await session!.options.toolServices?.webSearcher!.search('kimi'); expect(getAccessToken).toHaveBeenCalledWith(); const init = fetchImpl.mock.calls[0]?.[1] as RequestInit; diff --git a/packages/agent-core/test/mcp/connection-manager.test.ts b/packages/agent-core/test/mcp/connection-manager.test.ts index ae808d9353..21c9673905 100644 --- a/packages/agent-core/test/mcp/connection-manager.test.ts +++ b/packages/agent-core/test/mcp/connection-manager.test.ts @@ -663,7 +663,7 @@ describe('Session MCP startup', () => { const session = new Session({ id: 'test-mcp-oauth', - runtime: { kaos: testKaos.withCwd(tmp) }, + kaos: testKaos.withCwd(tmp), homedir: join(tmp, 'session'), kimiHomeDir: kimiHome, rpc: sessionRpc(), @@ -704,7 +704,7 @@ describe('Session MCP startup', () => { const tmp = await mkdtemp(join(tmpdir(), 'kimi-session-mcp-startup-')); const session = new Session({ id: 'test-mcp-slow', - runtime: { kaos: testKaos.withCwd(tmp) }, + kaos: testKaos.withCwd(tmp), homedir: join(tmp, 'session'), rpc: sessionRpc(), mcpConfig: { @@ -744,7 +744,7 @@ describe('Session MCP startup', () => { scripted.mockNextResponse({ type: 'text', text: 'ready' }); const session = new Session({ id: 'test-mcp-turn-ended', - runtime: { kaos: testKaos.withCwd(tmp) }, + kaos: testKaos.withCwd(tmp), homedir: join(tmp, 'session'), rpc: sessionRpc({ events, @@ -811,7 +811,7 @@ describe('Session MCP startup', () => { const events: SessionRpcEvent[] = []; const session = new Session({ id: 'test-mcp-mixed', - runtime: { kaos: testKaos.withCwd(tmp) }, + kaos: testKaos.withCwd(tmp), homedir: join(tmp, 'session'), rpc: sessionRpc({ events }), mcpConfig: { diff --git a/packages/agent-core/test/session/cron-stop-on-close.test.ts b/packages/agent-core/test/session/cron-stop-on-close.test.ts index f1026a5898..9947a86ec1 100644 --- a/packages/agent-core/test/session/cron-stop-on-close.test.ts +++ b/packages/agent-core/test/session/cron-stop-on-close.test.ts @@ -29,7 +29,7 @@ describe('Session.close stops cron', () => { it('stops each agent cron scheduler on close', async () => { const { sessionDir, workDir } = await sessionFixture(); const session = new Session({ - runtime: { kaos: testKaos.withCwd(workDir) }, + kaos: testKaos.withCwd(workDir), id: 'session-cron-stop', homedir: sessionDir, rpc: createSessionRpc(), @@ -56,7 +56,7 @@ describe('Session.close stops cron', () => { const before = process.listenerCount('SIGUSR1'); const { sessionDir, workDir } = await sessionFixture(); const session = new Session({ - runtime: { kaos: testKaos.withCwd(workDir) }, + kaos: testKaos.withCwd(workDir), id: 'session-cron-stop-sigusr1', homedir: sessionDir, rpc: createSessionRpc(), diff --git a/packages/agent-core/test/session/init.test.ts b/packages/agent-core/test/session/init.test.ts index 8ab93b9057..3a5015405e 100644 --- a/packages/agent-core/test/session/init.test.ts +++ b/packages/agent-core/test/session/init.test.ts @@ -44,7 +44,7 @@ describe('Session.init', () => { const scripted = createScriptedGenerate(); const session = new Session({ id: 'test-init', - runtime: { kaos: testKaos.withCwd(workDir) }, + kaos: testKaos.withCwd(workDir), homedir: sessionDir, rpc: createSessionRpc(events), skills: { explicitDirs: [join(workDir, 'missing-skills')] }, @@ -120,7 +120,7 @@ describe('Session.init', () => { const sessionDir = await makeTempDir(); const records: TelemetryRecord[] = []; const session = new Session({ - runtime: { kaos: testKaos.withCwd(workDir) }, + kaos: testKaos.withCwd(workDir), homedir: sessionDir, rpc: createSessionRpc([]), providerManager: testProviderManager(), diff --git a/packages/agent-core/test/session/lifecycle-hooks.test.ts b/packages/agent-core/test/session/lifecycle-hooks.test.ts index 6a06560500..cc50f75301 100644 --- a/packages/agent-core/test/session/lifecycle-hooks.test.ts +++ b/packages/agent-core/test/session/lifecycle-hooks.test.ts @@ -26,7 +26,7 @@ describe('Session lifecycle hooks', () => { it('fires SessionStart on startup and SessionEnd on close', async () => { const { command, logPath, sessionDir, workDir } = await hookFixture(); const session = new Session({ - runtime: { kaos: testKaos.withCwd(workDir) }, + kaos: testKaos.withCwd(workDir), id: 'session-123', homedir: sessionDir, rpc: createSessionRpc(), @@ -71,7 +71,7 @@ describe('Session lifecycle hooks', () => { 'utf-8', ); const session = new Session({ - runtime: { kaos: testKaos.withCwd(workDir) }, + kaos: testKaos.withCwd(workDir), id: 'session-456', homedir: sessionDir, rpc: createSessionRpc(), @@ -94,7 +94,7 @@ describe('Session lifecycle hooks', () => { it('does not let failing SessionStart or SessionEnd hook commands interrupt startup or close', async () => { const { sessionDir, workDir } = await hookFixture(); const session = new Session({ - runtime: { kaos: testKaos.withCwd(workDir) }, + kaos: testKaos.withCwd(workDir), id: 'session-reject', homedir: sessionDir, rpc: createSessionRpc(), @@ -112,7 +112,7 @@ describe('Session lifecycle hooks', () => { it('stops background tasks on close when keepAliveOnExit is false', async () => { const { sessionDir, workDir } = await hookFixture(); const session = new Session({ - runtime: { kaos: testKaos.withCwd(workDir) }, + kaos: testKaos.withCwd(workDir), id: 'session-bg-cleanup', homedir: sessionDir, rpc: createSessionRpc(), @@ -133,7 +133,7 @@ describe('Session lifecycle hooks', () => { vi.stubEnv('KIMI_CODE_BACKGROUND_KEEP_ALIVE_ON_EXIT', '0'); const { sessionDir, workDir } = await hookFixture(); const session = new Session({ - runtime: { kaos: testKaos.withCwd(workDir) }, + kaos: testKaos.withCwd(workDir), id: 'session-bg-env-cleanup', homedir: sessionDir, rpc: createSessionRpc(), diff --git a/packages/agent-core/test/session/subagent-host.test.ts b/packages/agent-core/test/session/subagent-host.test.ts index f1eefa2f19..d08b6634c0 100644 --- a/packages/agent-core/test/session/subagent-host.test.ts +++ b/packages/agent-core/test/session/subagent-host.test.ts @@ -736,7 +736,7 @@ describe('Session resume permission parent chain', () => { await writeWire(childDir, []); const session = new Session({ - runtime: { kaos: testKaos.withCwd(workDir) }, + kaos: testKaos.withCwd(workDir), homedir: sessionDir, rpc: createSessionRpc(), initializeMainAgent: false, @@ -787,9 +787,7 @@ describe('Session.createAgent', () => { }); const session = new Session({ id: 'test-subagent-remote-context', - runtime: { - kaos, - }, + kaos, homedir: '/tmp/kimi-session', rpc: createSessionRpc(), initializeMainAgent: false, @@ -854,7 +852,7 @@ describe('Session.createAgent', () => { }); const session = new Session({ id: 'test-subagent-agents-md', - runtime: { kaos: kaos.withCwd(workDir) }, + kaos: kaos.withCwd(workDir), homedir: '/tmp/kimi-session', rpc: createSessionRpc(), initializeMainAgent: false, @@ -878,15 +876,56 @@ describe('Session.createAgent', () => { expect(created.agent.config.systemPrompt).toContain('leaf instructions'); }); + it('inherits the parent agent cwd when creating a subagent', async () => { + const sessionWorkDir = '/session/work'; + const parentWorkDir = '/parent/work'; + + const kaos = createFakeKaos({ + mkdir: vi.fn().mockResolvedValue(undefined), + writeText: vi.fn().mockResolvedValue(0), + stat: vi.fn(async (path: string) => { + if ([sessionWorkDir, parentWorkDir].includes(path)) { + return stat('dir'); + } + throw new Error(`ENOENT ${path}`); + }), + // oxlint-disable-next-line require-yield + iterdir: async function* () { + return; + }, + getcwd: () => sessionWorkDir, + }); + + const session = new Session({ + id: 'test-subagent-parent-cwd', + kaos, + homedir: '/tmp/kimi-session', + rpc: createSessionRpc(), + initializeMainAgent: false, + }); + + // Create a parent agent — it should start at the session workDir. + const parent = await session.createAgent({ type: 'main' }, contextProfile()); + expect(parent.agent.config.systemPrompt).toContain(`cwd=${sessionWorkDir}`); + + // Move the parent agent to a different cwd (e.g. after a config.update replay). + parent.agent.config.update({ cwd: parentWorkDir }); + + // Create a subagent from the moved parent. + const child = await session.createAgent({ type: 'sub' }, contextProfile(), parent.id); + + // The subagent should inherit the parent's current cwd, not the session default. + expect(child.agent.config.systemPrompt).toContain(`cwd=${parentWorkDir}`); + expect(child.agent.config.systemPrompt).not.toContain(`cwd=${sessionWorkDir}`); + }); + it('allocates the next unused generated agent id', async () => { const session = new Session({ id: 'test-subagent-agent-id', - runtime: { - kaos: createFakeKaos({ - mkdir: vi.fn().mockResolvedValue(undefined), - writeText: vi.fn().mockResolvedValue(0), - }), - }, + kaos: createFakeKaos({ + mkdir: vi.fn().mockResolvedValue(undefined), + writeText: vi.fn().mockResolvedValue(0), + }), homedir: '/tmp/kimi-session', rpc: createSessionRpc(), initializeMainAgent: false, @@ -909,12 +948,10 @@ describe('Session.createAgent', () => { it('shares the session McpConnectionManager with sub and main agents', async () => { const session = new Session({ - runtime: { - kaos: createFakeKaos({ - mkdir: vi.fn().mockResolvedValue(undefined), - writeText: vi.fn().mockResolvedValue(0), - }), - }, + kaos: createFakeKaos({ + mkdir: vi.fn().mockResolvedValue(undefined), + writeText: vi.fn().mockResolvedValue(0), + }), homedir: '/tmp/kimi-session', rpc: createSessionRpc(), initializeMainAgent: false, diff --git a/packages/agent-core/test/tools/bash-env.test.ts b/packages/agent-core/test/tools/bash-env.test.ts index b0c137e895..e88df9ee94 100644 --- a/packages/agent-core/test/tools/bash-env.test.ts +++ b/packages/agent-core/test/tools/bash-env.test.ts @@ -31,7 +31,7 @@ const signal = new AbortController().signal; async function captureSpawnEnv(): Promise> { const execWithEnv = vi.fn().mockResolvedValue(fakeProcess()); - const tool = new BashTool(createFakeKaos({ execWithEnv }), '/workspace', posixEnv); + const tool = new BashTool(createFakeKaos({ execWithEnv, osEnv: posixEnv }), '/workspace'); await executeTool(tool, { turnId: '0', toolCallId: 'tc_env', diff --git a/packages/agent-core/test/tools/bash.test.ts b/packages/agent-core/test/tools/bash.test.ts index 02c50a60da..d4f5ecce5c 100644 --- a/packages/agent-core/test/tools/bash.test.ts +++ b/packages/agent-core/test/tools/bash.test.ts @@ -132,7 +132,7 @@ function context(args: BashInput, signal = new AbortController().signal) { describe('BashTool', () => { it('exposes current metadata and schema', () => { - const tool = new BashTool(createFakeKaos(), '/workspace', posixEnv); + const tool = new BashTool(createFakeKaos({ osEnv: posixEnv }), '/workspace'); expect(tool.name).toBe('Bash'); expect(tool.parameters).toMatchObject({ @@ -181,7 +181,7 @@ describe('BashTool', () => { }); it('describes the cwd, command, run_in_background, description, and disable_timeout parameters', () => { - const tool = new BashTool(createFakeKaos(), '/workspace', posixEnv); + const tool = new BashTool(createFakeKaos({ osEnv: posixEnv }), '/workspace'); const properties = (tool.parameters as { properties: Record }) .properties; @@ -199,7 +199,7 @@ describe('BashTool', () => { }); it('exposes a default timeout in the JSON Schema', () => { - const tool = new BashTool(createFakeKaos(), '/workspace', posixEnv); + const tool = new BashTool(createFakeKaos({ osEnv: posixEnv }), '/workspace'); const properties = (tool.parameters as { properties: Record }) .properties; @@ -220,9 +220,8 @@ describe('BashTool', () => { }, }); const tool = new BashTool( - createFakeKaos({ execWithEnv: vi.fn().mockResolvedValue(proc) }), + createFakeKaos({ execWithEnv: vi.fn().mockResolvedValue(proc), osEnv: posixEnv }), '/workspace', - posixEnv, ); const running = executeTool(tool, context({ command: 'sleep 3', timeout: 2 })); @@ -241,9 +240,8 @@ describe('BashTool', () => { it('renders the available commands section and the /tasks hint', () => { const tool = new BashTool( - createFakeKaos(), + createFakeKaos({ osEnv: posixEnv }), '/workspace', - posixEnv, new BackgroundProcessManager(), ); @@ -254,7 +252,7 @@ describe('BashTool', () => { it('runs through execWithEnv, injects cwd, noninteractive env, and closes stdin', async () => { const proc = processWithOutput({ stdout: 'ok\n' }); const execWithEnv = vi.fn().mockResolvedValue(proc); - const tool = new BashTool(createFakeKaos({ execWithEnv }), '/workspace', posixEnv); + const tool = new BashTool(createFakeKaos({ execWithEnv, osEnv: posixEnv }), '/workspace'); const result = await executeTool(tool, context({ command: 'printf ok', timeout: 60 })); @@ -275,7 +273,7 @@ describe('BashTool', () => { it('uses args.cwd when provided', async () => { const execWithEnv = vi.fn().mockResolvedValue(processWithOutput({ stdout: 'sub\n' })); - const tool = new BashTool(createFakeKaos({ execWithEnv }), '/workspace', posixEnv); + const tool = new BashTool(createFakeKaos({ execWithEnv, osEnv: posixEnv }), '/workspace'); await executeTool(tool, context({ command: 'pwd', cwd: '/tmp/project', timeout: 60 })); @@ -286,9 +284,8 @@ describe('BashTool', () => { const proc = processWithOutput({ stdout: 'ok\n' }); const execWithEnv = vi.fn().mockResolvedValue(proc); const tool = new BashTool( - createFakeKaos({ execWithEnv }), + createFakeKaos({ execWithEnv, osEnv: windowsBashEnv }), 'C:\\Users\\me\\project', - windowsBashEnv, ); const result = await executeTool(tool, context({ command: 'echo ok 2>nul', timeout: 60 })); @@ -314,9 +311,9 @@ describe('BashTool', () => { execWithEnv: vi .fn() .mockResolvedValue(processWithOutput({ stderr: 'boom\n', exitCode: 2 })), + osEnv: posixEnv, }), '/workspace', - posixEnv, ); const result = await executeTool(tool, context({ command: 'exit 2', timeout: 60 })); @@ -336,9 +333,9 @@ describe('BashTool', () => { execWithEnv: vi .fn() .mockResolvedValue(processWithOutput({ stdout: 'out\n', stderr: 'warn\n' })), + osEnv: posixEnv, }), '/workspace', - posixEnv, ); const result = await executeTool(tool, context({ command: 'mixed', timeout: 60 })); @@ -360,9 +357,9 @@ describe('BashTool', () => { exitCode: 2, }), ), + osEnv: posixEnv, }), '/workspace', - posixEnv, ); const result = await executeTool(tool, context({ command: 'mixed fail', timeout: 60 })); @@ -387,9 +384,9 @@ describe('BashTool', () => { const tool = new BashTool( createFakeKaos({ execWithEnv: vi.fn().mockResolvedValue(proc), + osEnv: posixEnv, }), '/workspace', - posixEnv, ); const resultPromise = executeTool(tool, context({ command: 'mixed', timeout: 60 })); @@ -410,7 +407,7 @@ describe('BashTool', () => { const controller = new AbortController(); controller.abort(); const execWithEnv = vi.fn(); - const tool = new BashTool(createFakeKaos({ execWithEnv }), '/workspace', posixEnv); + const tool = new BashTool(createFakeKaos({ execWithEnv, osEnv: posixEnv }), '/workspace'); const result = await executeTool(tool, context({ command: 'echo nope' }, controller.signal)); @@ -431,7 +428,7 @@ describe('BashTool', () => { }); const execWithEnv = vi.fn().mockResolvedValue(proc); const controller = new AbortController(); - const tool = new BashTool(createFakeKaos({ execWithEnv }), '/workspace', posixEnv); + const tool = new BashTool(createFakeKaos({ execWithEnv, osEnv: posixEnv }), '/workspace'); const running = executeTool(tool, context({ command: 'sleep 10' }, controller.signal)); await vi.waitFor(() => { @@ -448,7 +445,7 @@ describe('BashTool', () => { it('requires a background manager and description for background commands', async () => { const proc = processWithOutput(); const execWithEnv = vi.fn().mockResolvedValue(proc); - const withoutManager = new BashTool(createFakeKaos({ execWithEnv }), '/workspace', posixEnv); + const withoutManager = new BashTool(createFakeKaos({ execWithEnv, osEnv: posixEnv }), '/workspace'); const unavailable = await executeTool(withoutManager, context({ command: 'sleep 10', run_in_background: true, description: 'watch' }), @@ -459,9 +456,8 @@ describe('BashTool', () => { const manager = new BackgroundProcessManager(); const withManager = new BashTool( - createFakeKaos({ execWithEnv }), + createFakeKaos({ execWithEnv, osEnv: posixEnv }), '/workspace', - posixEnv, manager, ); const missingDescription = await executeTool(withManager, @@ -477,7 +473,7 @@ describe('BashTool', () => { const proc = processWithOutput(); const execWithEnv = vi.fn().mockResolvedValue(proc); const manager = new BackgroundProcessManager(); - const tool = new BashTool(createFakeKaos({ execWithEnv }), '/workspace', posixEnv, manager); + const tool = new BashTool(createFakeKaos({ execWithEnv, osEnv: posixEnv }), '/workspace', manager); const result = await executeTool(tool, context({ command: 'sleep 10', run_in_background: true, description: 'long running task' }), @@ -492,7 +488,7 @@ describe('BashTool', () => { const manager = new BackgroundProcessManager({ maxRunningTasks: 1 }); manager.register(processWithOutput(), 'sleep 10', 'existing task'); const execWithEnv = vi.fn().mockResolvedValue(processWithOutput()); - const tool = new BashTool(createFakeKaos({ execWithEnv }), '/workspace', posixEnv, manager); + const tool = new BashTool(createFakeKaos({ execWithEnv, osEnv: posixEnv }), '/workspace', manager); const result = await executeTool(tool, context({ command: 'sleep 10', run_in_background: true, description: 'second task' }), @@ -515,7 +511,7 @@ describe('BashTool', () => { }), ) .mockResolvedValueOnce(processWithOutput()); - const tool = new BashTool(createFakeKaos({ execWithEnv }), '/workspace', posixEnv, manager); + const tool = new BashTool(createFakeKaos({ execWithEnv, osEnv: posixEnv }), '/workspace', manager); const first = executeTool(tool, context({ command: 'sleep 10', run_in_background: true, description: 'first task' }), @@ -547,9 +543,8 @@ describe('BashTool', () => { ) .mockResolvedValueOnce(processWithOutput()); const tool = new BashTool( - createFakeKaos({ execWithEnv }), + createFakeKaos({ execWithEnv, osEnv: windowsBashEnv }), 'C:\\Users\\me\\project', - windowsBashEnv, manager, ); @@ -593,7 +588,7 @@ describe('BashTool', () => { const { proc, finishWait, markExited } = processWithVisibleExitBeforeWait(0); const execWithEnv = vi.fn().mockResolvedValue(proc); const manager = new BackgroundProcessManager(); - const tool = new BashTool(createFakeKaos({ execWithEnv }), '/workspace', posixEnv, manager); + const tool = new BashTool(createFakeKaos({ execWithEnv, osEnv: posixEnv }), '/workspace', manager); const result = await executeTool(tool, context({ @@ -629,7 +624,7 @@ describe('BashTool', () => { const proc = processThatNeverExits(); const execWithEnv = vi.fn().mockResolvedValue(proc); const manager = new BackgroundProcessManager(); - const tool = new BashTool(createFakeKaos({ execWithEnv }), '/workspace', posixEnv, manager); + const tool = new BashTool(createFakeKaos({ execWithEnv, osEnv: posixEnv }), '/workspace', manager); const result = await executeTool(tool, context({ @@ -654,7 +649,7 @@ describe('BashTool', () => { const proc = processThatNeverExits(); const execWithEnv = vi.fn().mockResolvedValue(proc); const manager = new BackgroundProcessManager(); - const tool = new BashTool(createFakeKaos({ execWithEnv }), '/workspace', posixEnv, manager); + const tool = new BashTool(createFakeKaos({ execWithEnv, osEnv: posixEnv }), '/workspace', manager); const result = await executeTool(tool, context({ @@ -679,9 +674,9 @@ describe('BashTool', () => { const tool = new BashTool( createFakeKaos({ execWithEnv: vi.fn().mockResolvedValue(processWithOutput({ stdout: huge })), + osEnv: posixEnv, }), '/workspace', - posixEnv, ); const result = await executeTool(tool, context({ command: 'yes', timeout: 60 })); @@ -696,9 +691,9 @@ describe('BashTool', () => { const tool = new BashTool( createFakeKaos({ execWithEnv: vi.fn().mockResolvedValue(processWithOutput({ stdout: huge })), + osEnv: posixEnv, }), '/workspace', - posixEnv, ); const result = await executeTool(tool, context({ command: 'yes', timeout: 60 })); @@ -715,9 +710,9 @@ describe('BashTool', () => { execWithEnv: vi .fn() .mockResolvedValue(processWithOutput({ stdout: huge, exitCode: 1 })), + osEnv: posixEnv, }), '/workspace', - posixEnv, ); const result = await executeTool(tool, context({ command: 'fail-and-flood', timeout: 60 })); @@ -743,9 +738,8 @@ describe('BashTool', () => { }, }); const tool = new BashTool( - createFakeKaos({ execWithEnv: vi.fn().mockResolvedValue(proc) }), + createFakeKaos({ execWithEnv: vi.fn().mockResolvedValue(proc), osEnv: posixEnv }), '/workspace', - posixEnv, ); const running = executeTool(tool, context({ command: 'sleep 2', timeout: 1 })); @@ -771,7 +765,7 @@ describe('BashTool', () => { delete process.env['GIT_SSH_COMMAND']; try { const execWithEnv = vi.fn().mockResolvedValue(processWithOutput({ stdout: 'ok\n' })); - const tool = new BashTool(createFakeKaos({ execWithEnv }), '/workspace', posixEnv); + const tool = new BashTool(createFakeKaos({ execWithEnv, osEnv: posixEnv }), '/workspace'); await executeTool(tool, context({ command: 'true', timeout: 60 })); @@ -786,7 +780,7 @@ describe('BashTool', () => { const proc = processWithOutput(); const execWithEnv = vi.fn().mockResolvedValue(proc); const manager = new BackgroundProcessManager(); - const tool = new BashTool(createFakeKaos({ execWithEnv }), '/workspace', posixEnv, manager); + const tool = new BashTool(createFakeKaos({ execWithEnv, osEnv: posixEnv }), '/workspace', manager); const result = await executeTool( tool, @@ -805,7 +799,7 @@ describe('BashTool', () => { it('rejects background command without description (description-required guard)', async () => { const manager = new BackgroundProcessManager(); const execWithEnv = vi.fn().mockResolvedValue(processWithOutput()); - const tool = new BashTool(createFakeKaos({ execWithEnv }), '/workspace', posixEnv, manager); + const tool = new BashTool(createFakeKaos({ execWithEnv, osEnv: posixEnv }), '/workspace', manager); const result = await executeTool( tool, @@ -820,9 +814,8 @@ describe('BashTool', () => { it('rewrites nul-redirect on Windows so the spawned argv has /dev/null', async () => { const execWithEnv = vi.fn().mockResolvedValue(processWithOutput({ stdout: '' })); const tool = new BashTool( - createFakeKaos({ execWithEnv }), + createFakeKaos({ execWithEnv, osEnv: windowsBashEnv }), 'C:\\Users\\me\\project', - windowsBashEnv, ); await executeTool(tool, context({ command: 'ls 2>nul', timeout: 60 })); @@ -833,7 +826,7 @@ describe('BashTool', () => { it('passes nul-redirect through unchanged on Linux so the argv keeps the literal file target', async () => { const execWithEnv = vi.fn().mockResolvedValue(processWithOutput({ stdout: '' })); - const tool = new BashTool(createFakeKaos({ execWithEnv }), '/workspace', posixEnv); + const tool = new BashTool(createFakeKaos({ execWithEnv, osEnv: posixEnv }), '/workspace'); await executeTool(tool, context({ command: 'ls 2>nul', timeout: 60 })); @@ -843,9 +836,8 @@ describe('BashTool', () => { it('exposes a shell description that documents /bin/bash, TaskOutput/TaskStop, safety and efficiency sections, and background semantics', () => { const tool = new BashTool( - createFakeKaos(), + createFakeKaos({ osEnv: posixEnv }), '/workspace', - posixEnv, new BackgroundProcessManager(), ); @@ -868,16 +860,15 @@ describe('BashTool prompt / runtime consistency', () => { // the background-enabled prompt, which is the only variant that documents // any Task* tool. const enabledTool = new BashTool( - createFakeKaos({ execWithEnv }), + createFakeKaos({ execWithEnv, osEnv: posixEnv }), '/workspace', - posixEnv, new BackgroundProcessManager(), ); const promptToolNames = new Set( [...enabledTool.description.matchAll(/`(Task[A-Za-z]+)`/g)].map((match) => match[1]), ); - const tool = new BashTool(createFakeKaos({ execWithEnv }), '/workspace', posixEnv); + const tool = new BashTool(createFakeKaos({ execWithEnv, osEnv: posixEnv }), '/workspace'); const result = await executeTool(tool, context({ command: 'sleep 10', run_in_background: true, description: 'watch' }), ); @@ -898,7 +889,7 @@ describe('BashTool prompt / runtime consistency', () => { }); it('does not claim failure exit codes appear in a system tag', () => { - const tool = new BashTool(createFakeKaos({}), '/workspace', posixEnv); + const tool = new BashTool(createFakeKaos({ osEnv: posixEnv }), '/workspace'); // The implementation reports failures as plain text inside the output // (`Command failed with exit code: N`), never via a system tag. diff --git a/packages/agent-core/test/tools/builtin-current.test.ts b/packages/agent-core/test/tools/builtin-current.test.ts index fda3782d44..9f2c50ff8f 100644 --- a/packages/agent-core/test/tools/builtin-current.test.ts +++ b/packages/agent-core/test/tools/builtin-current.test.ts @@ -185,15 +185,17 @@ describe('current builtin file and shell tools', () => { it('Bash exposes parameters and returns foreground stdout', async () => { const tool = new BashTool( - createFakeKaos({ execWithEnv: vi.fn().mockResolvedValue(processWithOutput('ok\n')) }), + createFakeKaos({ + execWithEnv: vi.fn().mockResolvedValue(processWithOutput('ok\n')), + osEnv: { + osKind: 'Linux', + osArch: 'arm64', + osVersion: 'test', + shellPath: '/bin/bash', + shellName: 'bash', + }, + }), '/workspace', - { - osKind: 'Linux', - osArch: 'arm64', - osVersion: 'test', - shellPath: '/bin/bash', - shellName: 'bash', - }, ); expect(BashInputSchema.safeParse({ command: 'printf ok' }).success).toBe(true); diff --git a/packages/agent-core/test/tools/plan-mode-hard-block.test.ts b/packages/agent-core/test/tools/plan-mode-hard-block.test.ts index 90fb2ca13e..d0598b129e 100644 --- a/packages/agent-core/test/tools/plan-mode-hard-block.test.ts +++ b/packages/agent-core/test/tools/plan-mode-hard-block.test.ts @@ -20,10 +20,8 @@ async function activePlanAgent(): Promise<{ agent: Agent; planMode: PlanMode }> emitStatusUpdated: vi.fn(), records: { logRecord: vi.fn() }, replayBuilder: { push: vi.fn() }, - runtime: { - kaos: { - mkdir: vi.fn().mockResolvedValue(undefined), - }, + kaos: { + mkdir: vi.fn().mockResolvedValue(undefined), }, } as unknown as Agent; const planMode = new PlanMode(agent); diff --git a/packages/agent-core/test/tools/planning/exit-plan-mode-telemetry.test.ts b/packages/agent-core/test/tools/planning/exit-plan-mode-telemetry.test.ts index 87e8662610..84c57c9a2b 100644 --- a/packages/agent-core/test/tools/planning/exit-plan-mode-telemetry.test.ts +++ b/packages/agent-core/test/tools/planning/exit-plan-mode-telemetry.test.ts @@ -49,7 +49,7 @@ function makeAgent(input: { permission: { mode: input.mode }, type: 'main', config: { cwd: '/workspace' }, - runtime: { kaos: createFakeKaos() }, + kaos: createFakeKaos(), emitStatusUpdated: vi.fn(), records: { logRecord: vi.fn() }, replayBuilder: { push: vi.fn() }, diff --git a/packages/agent-core/test/tools/shell-cancel.test.ts b/packages/agent-core/test/tools/shell-cancel.test.ts index 5a57c28bd0..2dae11804b 100644 --- a/packages/agent-core/test/tools/shell-cancel.test.ts +++ b/packages/agent-core/test/tools/shell-cancel.test.ts @@ -35,7 +35,7 @@ describe('BashTool cancellation contract', () => { }; const execWithEnv = vi.fn().mockResolvedValue(proc); const controller = new AbortController(); - const tool = new BashTool(createFakeKaos({ execWithEnv }), '/workspace', posixEnv); + const tool = new BashTool(createFakeKaos({ execWithEnv, osEnv: posixEnv }), '/workspace'); const running = executeTool(tool, { turnId: '0', diff --git a/packages/agent-core/test/tools/shell-quoting.test.ts b/packages/agent-core/test/tools/shell-quoting.test.ts index 17cc20aa6c..f214d7f9b8 100644 --- a/packages/agent-core/test/tools/shell-quoting.test.ts +++ b/packages/agent-core/test/tools/shell-quoting.test.ts @@ -43,7 +43,7 @@ function captureCommandRewrite( ): Promise<{ rewritten: string; argv: readonly string[] }> { const execWithEnv = vi.fn().mockResolvedValue(fakeProcess()); const cwd = env.osKind === 'Windows' ? 'C:\\work' : '/work'; - const tool = new BashTool(createFakeKaos({ execWithEnv }), cwd, env); + const tool = new BashTool(createFakeKaos({ execWithEnv, osEnv: env }), cwd); return executeTool(tool, { turnId: '0', diff --git a/packages/migration-legacy/test/resume.integration.test.ts b/packages/migration-legacy/test/resume.integration.test.ts index 00a44c554e..90581e0d4e 100644 --- a/packages/migration-legacy/test/resume.integration.test.ts +++ b/packages/migration-legacy/test/resume.integration.test.ts @@ -126,7 +126,7 @@ describe('migrated session loads in real kimi-core', () => { // If `agents.main.homedir` were the project workdir (the bug), the agent // would replay an absent file and the history would be empty. const session = new Session({ - runtime: { kaos: (await LocalKaos.create()).withCwd(WORK_DIR) }, + kaos: (await LocalKaos.create()).withCwd(WORK_DIR), id: 'ses_tiny-resume', homedir: targetDir, rpc: createSessionRpc(),