mirror of
https://github.com/MoonshotAI/kimi-code.git
synced 2026-08-21 06:35:50 +00:00
refactor(agent-core-v2): add kaos domain for execution environment
- Add `kaos` domain (`IKaos` per session + `IKaosFactory`) wrapping `@moonshot-ai/kaos`, so business code imports `#/kaos` instead of the package directly. - Back `agentFs` and `process` with `IKaos`; drop the per-domain `IFileSystemBackend` / `IProcessBackend` interfaces and their local/ssh stubs. - Migrate `read-media`, `path-access`, `background/process-task`, and `bootstrap` off direct kaos-package imports. - Seed `IKaos` per session in `SessionLifecycleService` and expose `agentFs` / `fs` on the server-v2 action map. - Create the server-v2 main agent on demand (`ensureMainAgent`) instead of failing requests when it is missing.
This commit is contained in:
parent
d4f9933b92
commit
9076f55643
37 changed files with 613 additions and 369 deletions
|
|
@ -38,6 +38,10 @@ const DOMAIN_LAYER = new Map([
|
|||
// `errors` is a top-level facade (src/errors.ts) that aggregates every
|
||||
// domain's error codes; any domain may import it, so it sits at L0.
|
||||
['errors', 0],
|
||||
// `kaos` is the execution-environment substrate (cwd/env/osEnv/backend);
|
||||
// it wraps the `@moonshot-ai/kaos` package and depends on no business
|
||||
// domain, so it sits at L0 where any domain may import it.
|
||||
['kaos', 0],
|
||||
// L1 — abstraction bridges & low-level capabilities
|
||||
['log', 1],
|
||||
['telemetry', 1],
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@
|
|||
|
||||
import * as pathe from 'pathe';
|
||||
|
||||
import type { Kaos } from '@moonshot-ai/kaos';
|
||||
import type { IKaos } from '#/kaos';
|
||||
|
||||
import type { WorkspaceConfig } from '../support/workspace';
|
||||
import { isSensitiveFile } from './sensitive';
|
||||
|
|
@ -178,7 +178,7 @@ export interface ResolvePathAccessOptions {
|
|||
}
|
||||
|
||||
export interface ResolvePathAccessPathOptions {
|
||||
readonly kaos: Pick<Kaos, 'pathClass' | 'gethome'>;
|
||||
readonly kaos: Pick<IKaos, 'pathClass' | 'gethome'>;
|
||||
readonly workspace: WorkspaceConfig;
|
||||
readonly operation: PathAccessOperation;
|
||||
readonly policy?: WorkspaceAccessPolicy;
|
||||
|
|
|
|||
|
|
@ -1,11 +1,9 @@
|
|||
/**
|
||||
* `agentFs` domain (L1) — the Agent's filesystem and its pluggable backend.
|
||||
* `agentFs` domain (L1) — the Agent's filesystem.
|
||||
*
|
||||
* Defines the `IAgentFileSystem` that business code injects to read and write
|
||||
* files inside the Agent's execution environment, plus the internal
|
||||
* `IFileSystemBackend` provider that hides the local/ssh/container split.
|
||||
* Session-scoped. Business code depends on `IAgentFileSystem` only; the
|
||||
* backend is wired through the scope registry.
|
||||
* files inside the Agent's execution environment. Session-scoped and backed by
|
||||
* the session `IKaos`; business code depends on `IAgentFileSystem` only.
|
||||
*/
|
||||
|
||||
import { createDecorator, type ServiceIdentifier } from '#/_base/di/instantiation';
|
||||
|
|
@ -23,7 +21,7 @@ export interface IAgentFileSystem {
|
|||
|
||||
readText(path: string): Promise<string>;
|
||||
writeText(path: string, data: string): Promise<void>;
|
||||
readBytes(path: string): Promise<Uint8Array>;
|
||||
readBytes(path: string, n?: number): Promise<Uint8Array>;
|
||||
writeBytes(path: string, data: Uint8Array): Promise<void>;
|
||||
stat(path: string): Promise<AgentFileStat>;
|
||||
readdir(path: string): Promise<readonly string[]>;
|
||||
|
|
@ -34,19 +32,3 @@ export interface IAgentFileSystem {
|
|||
|
||||
export const IAgentFileSystem: ServiceIdentifier<IAgentFileSystem> =
|
||||
createDecorator<IAgentFileSystem>('agentFileSystem');
|
||||
|
||||
export interface IFileSystemBackend {
|
||||
readonly _serviceBrand: undefined;
|
||||
|
||||
readText(absPath: string): Promise<string>;
|
||||
writeText(absPath: string, data: string): Promise<void>;
|
||||
readBytes(absPath: string): Promise<Uint8Array>;
|
||||
writeBytes(absPath: string, data: Uint8Array): Promise<void>;
|
||||
stat(absPath: string): Promise<AgentFileStat>;
|
||||
readdir(absPath: string): Promise<readonly string[]>;
|
||||
glob(absDir: string, pattern: string): Promise<readonly string[]>;
|
||||
mkdir(absPath: string): Promise<void>;
|
||||
}
|
||||
|
||||
export const IFileSystemBackend: ServiceIdentifier<IFileSystemBackend> =
|
||||
createDecorator<IFileSystemBackend>('fileSystemBackend');
|
||||
|
|
|
|||
|
|
@ -1,70 +1,85 @@
|
|||
/**
|
||||
* `agentFs` domain (L1) — `IAgentFileSystem` implementation.
|
||||
*
|
||||
* Resolves paths against the session workspace and delegates IO to the
|
||||
* injected `IFileSystemBackend`; reads the work directory through
|
||||
* `workspaceContext`. Bound at Session scope.
|
||||
* Focused file-IO surface over the session execution environment (`IKaos.backend`).
|
||||
* Relative-path resolution (in the target path style) and symlink-safe glob are
|
||||
* handled by the kaos backend; this service exposes a kaos-free, filesystem-shaped
|
||||
* interface to business code and derives sub-views via `withCwd`. Bound at Session
|
||||
* scope.
|
||||
*/
|
||||
|
||||
import { isAbsolute, resolve } from 'node:path';
|
||||
|
||||
import { InstantiationType } from '#/_base/di/extensions';
|
||||
import { NotImplementedError } from '#/_base/errors';
|
||||
import { LifecycleScope, registerScopedService } from '#/_base/di/scope';
|
||||
import { IWorkspaceContext } from '#/workspaceContext';
|
||||
import { IKaos, type StatResult } from '#/kaos';
|
||||
|
||||
import { type AgentFileStat, IAgentFileSystem, IFileSystemBackend } from './agentFs';
|
||||
import { type AgentFileStat, IAgentFileSystem } from './agentFs';
|
||||
|
||||
const S_IFMT = 0o170000;
|
||||
const S_IFREG = 0o100000;
|
||||
const S_IFDIR = 0o040000;
|
||||
|
||||
function statKind(s: StatResult): Pick<AgentFileStat, 'isFile' | 'isDirectory'> {
|
||||
const kind = s.stMode & S_IFMT;
|
||||
return { isFile: kind === S_IFREG, isDirectory: kind === S_IFDIR };
|
||||
}
|
||||
|
||||
function basename(p: string): string {
|
||||
const parts = p.split(/[\\/]/);
|
||||
return parts[parts.length - 1] ?? p;
|
||||
}
|
||||
|
||||
export class AgentFileSystem implements IAgentFileSystem {
|
||||
declare readonly _serviceBrand: undefined;
|
||||
|
||||
constructor(
|
||||
@IFileSystemBackend private readonly backend: IFileSystemBackend,
|
||||
@IWorkspaceContext private readonly workspace: IWorkspaceContext,
|
||||
) {}
|
||||
constructor(@IKaos private readonly kaos: IKaos) {}
|
||||
|
||||
get cwd(): string {
|
||||
return this.workspace.workDir;
|
||||
}
|
||||
|
||||
private abs(path: string): string {
|
||||
return isAbsolute(path) ? path : resolve(this.cwd, path);
|
||||
return this.kaos.cwd;
|
||||
}
|
||||
|
||||
readText(path: string): Promise<string> {
|
||||
return this.backend.readText(this.abs(path));
|
||||
return this.kaos.backend.readText(path);
|
||||
}
|
||||
|
||||
writeText(path: string, data: string): Promise<void> {
|
||||
return this.backend.writeText(this.abs(path), data);
|
||||
return this.kaos.backend.writeText(path, data).then(() => undefined);
|
||||
}
|
||||
|
||||
readBytes(path: string): Promise<Uint8Array> {
|
||||
return this.backend.readBytes(this.abs(path));
|
||||
readBytes(path: string, n?: number): Promise<Uint8Array> {
|
||||
return this.kaos.backend.readBytes(path, n);
|
||||
}
|
||||
|
||||
writeBytes(path: string, data: Uint8Array): Promise<void> {
|
||||
return this.backend.writeBytes(this.abs(path), data);
|
||||
return this.kaos.backend.writeBytes(path, Buffer.from(data)).then(() => undefined);
|
||||
}
|
||||
|
||||
stat(path: string): Promise<AgentFileStat> {
|
||||
return this.backend.stat(this.abs(path));
|
||||
async stat(path: string): Promise<AgentFileStat> {
|
||||
const s = await this.kaos.backend.stat(path);
|
||||
return { ...statKind(s), size: s.stSize };
|
||||
}
|
||||
|
||||
readdir(path: string): Promise<readonly string[]> {
|
||||
return this.backend.readdir(this.abs(path));
|
||||
async readdir(path: string): Promise<readonly string[]> {
|
||||
const names: string[] = [];
|
||||
for await (const entry of this.kaos.backend.iterdir(path)) {
|
||||
names.push(basename(entry));
|
||||
}
|
||||
return names;
|
||||
}
|
||||
|
||||
glob(pattern: string): Promise<readonly string[]> {
|
||||
return this.backend.glob(this.cwd, pattern);
|
||||
async glob(pattern: string): Promise<readonly string[]> {
|
||||
const out: string[] = [];
|
||||
for await (const match of this.kaos.backend.glob(this.kaos.cwd, pattern)) {
|
||||
out.push(match);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
mkdir(path: string): Promise<void> {
|
||||
return this.backend.mkdir(this.abs(path));
|
||||
return this.kaos.backend.mkdir(path, { parents: true, existOk: true });
|
||||
}
|
||||
|
||||
withCwd(_cwd: string): IAgentFileSystem {
|
||||
throw new NotImplementedError('agentFs.withCwd');
|
||||
withCwd(cwd: string): IAgentFileSystem {
|
||||
return new AgentFileSystem(this.kaos.withCwd(cwd));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,10 +1,9 @@
|
|||
/**
|
||||
* `agentFs` domain barrel — re-exports the agent-filesystem contract
|
||||
* (`agentFs`) and its scoped service (`agentFsService`), the wire-shaped fs
|
||||
* service (`fs`, `fsService`), the fs error codes (`errors`), and the backend
|
||||
* implementations (`localFileSystemBackend`, `sshFileSystemBackend`).
|
||||
* Importing this barrel registers the `IAgentFileSystem` / `IFsService`
|
||||
* bindings and the default local `IFileSystemBackend` into the scope registry.
|
||||
* service (`fs`, `fsService`), and the fs error codes (`errors`). Importing
|
||||
* this barrel registers the `IAgentFileSystem` and `IFsService` bindings into
|
||||
* the scope registry.
|
||||
*/
|
||||
|
||||
export * from './agentFs';
|
||||
|
|
@ -12,5 +11,3 @@ export * from './agentFsService';
|
|||
export * from './errors';
|
||||
export * from './fs';
|
||||
export * from './fsService';
|
||||
export * from './localFileSystemBackend';
|
||||
export * from './sshFileSystemBackend';
|
||||
|
|
|
|||
|
|
@ -1,63 +0,0 @@
|
|||
/**
|
||||
* `agentFs` domain (L1) — local `IFileSystemBackend` implementation.
|
||||
*
|
||||
* Backs the Agent filesystem with the real local disk by delegating to the
|
||||
* program-side `hostFs` primitives. Registered as the default
|
||||
* `IFileSystemBackend` at Session scope; remote backends override it via the
|
||||
* scope registry.
|
||||
*/
|
||||
|
||||
import { InstantiationType } from '#/_base/di/extensions';
|
||||
import { NotImplementedError } from '#/_base/errors';
|
||||
import { LifecycleScope, registerScopedService } from '#/_base/di/scope';
|
||||
import { IHostFileSystem } from '#/hostFs';
|
||||
|
||||
import { type AgentFileStat, IFileSystemBackend } from './agentFs';
|
||||
|
||||
export class LocalFileSystemBackend implements IFileSystemBackend {
|
||||
declare readonly _serviceBrand: undefined;
|
||||
|
||||
constructor(@IHostFileSystem private readonly hostFs: IHostFileSystem) {}
|
||||
|
||||
readText(absPath: string): Promise<string> {
|
||||
return this.hostFs.readText(absPath);
|
||||
}
|
||||
|
||||
writeText(absPath: string, data: string): Promise<void> {
|
||||
return this.hostFs.writeText(absPath, data);
|
||||
}
|
||||
|
||||
readBytes(absPath: string): Promise<Uint8Array> {
|
||||
return this.hostFs.readBytes(absPath);
|
||||
}
|
||||
|
||||
writeBytes(absPath: string, data: Uint8Array): Promise<void> {
|
||||
return this.hostFs.writeBytes(absPath, data);
|
||||
}
|
||||
|
||||
async stat(absPath: string): Promise<AgentFileStat> {
|
||||
const s = await this.hostFs.stat(absPath);
|
||||
return { isFile: s.isFile, isDirectory: s.isDirectory, size: s.size };
|
||||
}
|
||||
|
||||
async readdir(absPath: string): Promise<readonly string[]> {
|
||||
const entries = await this.hostFs.readdir(absPath);
|
||||
return entries.map((e) => e.name);
|
||||
}
|
||||
|
||||
glob(_absDir: string, _pattern: string): Promise<readonly string[]> {
|
||||
throw new NotImplementedError('localFileSystemBackend.glob');
|
||||
}
|
||||
|
||||
mkdir(absPath: string): Promise<void> {
|
||||
return this.hostFs.mkdir(absPath, { recursive: true });
|
||||
}
|
||||
}
|
||||
|
||||
registerScopedService(
|
||||
LifecycleScope.Session,
|
||||
IFileSystemBackend,
|
||||
LocalFileSystemBackend,
|
||||
InstantiationType.Delayed,
|
||||
'agentFs',
|
||||
);
|
||||
|
|
@ -1,47 +0,0 @@
|
|||
/**
|
||||
* `agentFs` domain (L1) — ssh `IFileSystemBackend` stub.
|
||||
*
|
||||
* Placeholder for the remote backend; not registered into the scope registry
|
||||
* yet. A composition root that needs ssh supplies it through
|
||||
* `ScopeOptions.extra` to override the local backend.
|
||||
*/
|
||||
|
||||
import { NotImplementedError } from '#/_base/errors';
|
||||
|
||||
import { type AgentFileStat, IFileSystemBackend } from './agentFs';
|
||||
|
||||
export class SshFileSystemBackend implements IFileSystemBackend {
|
||||
declare readonly _serviceBrand: undefined;
|
||||
|
||||
readText(_absPath: string): Promise<string> {
|
||||
throw new NotImplementedError('sshFileSystemBackend');
|
||||
}
|
||||
|
||||
writeText(_absPath: string, _data: string): Promise<void> {
|
||||
throw new NotImplementedError('sshFileSystemBackend');
|
||||
}
|
||||
|
||||
readBytes(_absPath: string): Promise<Uint8Array> {
|
||||
throw new NotImplementedError('sshFileSystemBackend');
|
||||
}
|
||||
|
||||
writeBytes(_absPath: string, _data: Uint8Array): Promise<void> {
|
||||
throw new NotImplementedError('sshFileSystemBackend');
|
||||
}
|
||||
|
||||
stat(_absPath: string): Promise<AgentFileStat> {
|
||||
throw new NotImplementedError('sshFileSystemBackend');
|
||||
}
|
||||
|
||||
readdir(_absPath: string): Promise<readonly string[]> {
|
||||
throw new NotImplementedError('sshFileSystemBackend');
|
||||
}
|
||||
|
||||
glob(_absDir: string, _pattern: string): Promise<readonly string[]> {
|
||||
throw new NotImplementedError('sshFileSystemBackend');
|
||||
}
|
||||
|
||||
mkdir(_absPath: string): Promise<void> {
|
||||
throw new NotImplementedError('sshFileSystemBackend');
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
import type { KaosProcess } from '@moonshot-ai/kaos';
|
||||
import type { Readable } from 'node:stream';
|
||||
|
||||
import type { IProcess } from '#/process';
|
||||
|
||||
import type {
|
||||
BackgroundTask,
|
||||
BackgroundTaskInfoBase,
|
||||
|
|
@ -30,7 +31,7 @@ export class ProcessBackgroundTask implements BackgroundTask {
|
|||
private exitCode: number | null = null;
|
||||
|
||||
constructor(
|
||||
readonly proc: KaosProcess,
|
||||
readonly proc: IProcess,
|
||||
readonly command: string,
|
||||
readonly description: string,
|
||||
private readonly onOutput?: ProcessBackgroundTaskOutputCallback,
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ import { homedir } from 'node:os';
|
|||
|
||||
import { join } from 'pathe';
|
||||
|
||||
import type { Environment } from '@moonshot-ai/kaos';
|
||||
import type { Environment } from '#/kaos';
|
||||
|
||||
import { SyncDescriptor } from '#/_base/di/descriptors';
|
||||
import { createDecorator, type ServiceIdentifier } from '#/_base/di/instantiation';
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@
|
|||
|
||||
import { join } from 'pathe';
|
||||
|
||||
import { type Environment, detectEnvironmentFromNode } from '@moonshot-ai/kaos';
|
||||
import { type Environment, detectEnvironmentFromNode } from '#/kaos';
|
||||
|
||||
import { InstantiationType } from '#/_base/di/extensions';
|
||||
import { LifecycleScope, registerScopedService } from '#/_base/di/scope';
|
||||
|
|
|
|||
|
|
@ -52,6 +52,7 @@ export * from './gateway/index';
|
|||
export * from './workspaceContext/index';
|
||||
export * from './workspaceRegistry/index';
|
||||
export * from './hostFolderBrowser/index';
|
||||
export * from './kaos/index';
|
||||
export * from './agentFs/index';
|
||||
export * from './process/index';
|
||||
export * from './terminal/index';
|
||||
|
|
|
|||
8
packages/agent-core-v2/src/kaos/index.ts
Normal file
8
packages/agent-core-v2/src/kaos/index.ts
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
/**
|
||||
* `kaos` domain barrel — re-exports the execution-environment contracts
|
||||
* (`kaos`) and the `IKaosFactory` binding (`kaosFactoryService`). Importing
|
||||
* this barrel registers the `IKaosFactory` binding into the scope registry.
|
||||
*/
|
||||
|
||||
export * from './kaos';
|
||||
export * from './kaosFactoryService';
|
||||
61
packages/agent-core-v2/src/kaos/kaos.ts
Normal file
61
packages/agent-core-v2/src/kaos/kaos.ts
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
/**
|
||||
* `kaos` domain (L1) — execution-environment contracts.
|
||||
*
|
||||
* Defines `IKaos`, the Agent's execution environment (cwd, env layers, the
|
||||
* OS/shell probe, and the backend handle the fs/process domains delegate to),
|
||||
* plus `IKaosFactory`, the Core factory that builds an `IKaos` for a session
|
||||
* (local today; ssh/container behind the same factory later).
|
||||
*
|
||||
* Temporary: this domain wraps the `@moonshot-ai/kaos` package and re-exports
|
||||
* a few of its data types so business code imports them from `#/kaos` instead
|
||||
* of the package. `IKaos` is seeded into each Session scope by the composition
|
||||
* root; `IKaosFactory` is bound at Core scope.
|
||||
*/
|
||||
|
||||
import type { Environment, Kaos } from '@moonshot-ai/kaos';
|
||||
|
||||
import { createDecorator, type ServiceIdentifier } from '#/_base/di/instantiation';
|
||||
|
||||
export type { Environment, KaosProcess, StatResult } from '@moonshot-ai/kaos';
|
||||
export { detectEnvironmentFromNode } from '@moonshot-ai/kaos';
|
||||
|
||||
export type PathClass = 'posix' | 'win32';
|
||||
|
||||
export interface IKaos {
|
||||
readonly _serviceBrand: undefined;
|
||||
|
||||
/** Human-readable backend name (e.g. `"local"`, `"ssh:host"`). */
|
||||
readonly name: string;
|
||||
/** Current working directory of this execution environment. */
|
||||
readonly cwd: string;
|
||||
/** OS / shell probe of the execution environment. */
|
||||
readonly osEnv: Environment;
|
||||
/**
|
||||
* The backend fs/process domains delegate to. Temporary — owned by this
|
||||
* environment; business code should reach for `IAgentFileSystem` /
|
||||
* `IProcessRunner` instead of touching this directly.
|
||||
*/
|
||||
readonly backend: Kaos;
|
||||
|
||||
pathClass(): PathClass;
|
||||
normpath(path: string): string;
|
||||
gethome(): string;
|
||||
getcwd(): string;
|
||||
|
||||
/** Derive a new environment rooted at `cwd` (shares backend + osEnv). */
|
||||
withCwd(cwd: string): IKaos;
|
||||
/** Derive a new environment that overlays `env` onto spawned processes. */
|
||||
withEnv(env: Record<string, string>): IKaos;
|
||||
}
|
||||
|
||||
export const IKaos: ServiceIdentifier<IKaos> = createDecorator<IKaos>('kaos');
|
||||
|
||||
export interface IKaosFactory {
|
||||
readonly _serviceBrand: undefined;
|
||||
|
||||
/** Build a local execution environment rooted at `cwd`. */
|
||||
createLocal(cwd: string): Promise<IKaos>;
|
||||
}
|
||||
|
||||
export const IKaosFactory: ServiceIdentifier<IKaosFactory> =
|
||||
createDecorator<IKaosFactory>('kaosFactory');
|
||||
31
packages/agent-core-v2/src/kaos/kaosFactoryService.ts
Normal file
31
packages/agent-core-v2/src/kaos/kaosFactoryService.ts
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
/**
|
||||
* `kaos` domain (L1) — `IKaosFactory` implementation.
|
||||
*
|
||||
* Builds an `IKaos` for a session. Today only local (`LocalKaos`); ssh/container
|
||||
* are added behind the same factory later. Bound at Core scope.
|
||||
*/
|
||||
|
||||
import { LocalKaos } from '@moonshot-ai/kaos';
|
||||
|
||||
import { InstantiationType } from '#/_base/di/extensions';
|
||||
import { LifecycleScope, registerScopedService } from '#/_base/di/scope';
|
||||
|
||||
import { type IKaos, IKaosFactory } from './kaos';
|
||||
import { KaosService } from './kaosService';
|
||||
|
||||
export class KaosFactory implements IKaosFactory {
|
||||
declare readonly _serviceBrand: undefined;
|
||||
|
||||
async createLocal(cwd: string): Promise<IKaos> {
|
||||
const base = await LocalKaos.create();
|
||||
return new KaosService(base.withCwd(cwd));
|
||||
}
|
||||
}
|
||||
|
||||
registerScopedService(
|
||||
LifecycleScope.Core,
|
||||
IKaosFactory,
|
||||
KaosFactory,
|
||||
InstantiationType.Delayed,
|
||||
'kaos',
|
||||
);
|
||||
54
packages/agent-core-v2/src/kaos/kaosService.ts
Normal file
54
packages/agent-core-v2/src/kaos/kaosService.ts
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
/**
|
||||
* `kaos` domain (L1) — `IKaos` implementation.
|
||||
*
|
||||
* Thin wrapper around a `@moonshot-ai/kaos` `Kaos` backend, exposing cwd, the
|
||||
* OS/shell probe, path primitives, and context derivation (`withCwd`/`withEnv`).
|
||||
* Not registered directly — built by `IKaosFactory` and seeded into a Session
|
||||
* scope by the composition root.
|
||||
*/
|
||||
|
||||
import type { Kaos } from '@moonshot-ai/kaos';
|
||||
|
||||
import type { Environment, IKaos, PathClass } from './kaos';
|
||||
|
||||
export class KaosService implements IKaos {
|
||||
declare readonly _serviceBrand: undefined;
|
||||
|
||||
constructor(readonly backend: Kaos) {}
|
||||
|
||||
get name(): string {
|
||||
return this.backend.name;
|
||||
}
|
||||
|
||||
get cwd(): string {
|
||||
return this.backend.getcwd();
|
||||
}
|
||||
|
||||
get osEnv(): Environment {
|
||||
return this.backend.osEnv;
|
||||
}
|
||||
|
||||
pathClass(): PathClass {
|
||||
return this.backend.pathClass();
|
||||
}
|
||||
|
||||
normpath(path: string): string {
|
||||
return this.backend.normpath(path);
|
||||
}
|
||||
|
||||
gethome(): string {
|
||||
return this.backend.gethome();
|
||||
}
|
||||
|
||||
getcwd(): string {
|
||||
return this.backend.getcwd();
|
||||
}
|
||||
|
||||
withCwd(cwd: string): IKaos {
|
||||
return new KaosService(this.backend.withCwd(cwd));
|
||||
}
|
||||
|
||||
withEnv(env: Record<string, string>): IKaos {
|
||||
return new KaosService(this.backend.withEnv(env));
|
||||
}
|
||||
}
|
||||
|
|
@ -13,16 +13,18 @@
|
|||
* left to the composition root, which owns the auth resolver.
|
||||
*/
|
||||
|
||||
import type { Kaos } from '@moonshot-ai/kaos';
|
||||
import type { ChatProvider, ModelCapability } from '@moonshot-ai/kosong';
|
||||
|
||||
import { toDisposable, type IDisposable } from '#/_base/di';
|
||||
import type { WorkspaceConfig } from '#/_base/tools/support/workspace';
|
||||
import type { IAgentFileSystem } from '#/agentFs';
|
||||
import type { IKaos } from '#/kaos';
|
||||
import type { IToolRegistry } from '#/toolRegistry';
|
||||
import { ReadMediaFileTool, type VideoUploader } from './tools/read-media';
|
||||
|
||||
export interface RegisterMediaToolsDeps {
|
||||
readonly kaos: Kaos;
|
||||
readonly fs: IAgentFileSystem;
|
||||
readonly kaos: IKaos;
|
||||
readonly workspace: WorkspaceConfig;
|
||||
readonly capabilities: ModelCapability;
|
||||
readonly videoUploader?: VideoUploader;
|
||||
|
|
@ -44,7 +46,13 @@ export function registerMediaTools(
|
|||
return toDisposable(() => {});
|
||||
}
|
||||
return toolRegistry.register(
|
||||
new ReadMediaFileTool(deps.kaos, deps.workspace, deps.capabilities, deps.videoUploader),
|
||||
new ReadMediaFileTool(
|
||||
deps.fs,
|
||||
deps.kaos,
|
||||
deps.workspace,
|
||||
deps.capabilities,
|
||||
deps.videoUploader,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -19,7 +19,6 @@
|
|||
* only registered when the active model supports image or video input.
|
||||
*/
|
||||
|
||||
import type { Kaos } from '@moonshot-ai/kaos';
|
||||
import type {
|
||||
ContentPart,
|
||||
ModelCapability,
|
||||
|
|
@ -28,6 +27,8 @@ import type {
|
|||
} from '@moonshot-ai/kosong';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { IAgentFileSystem } from '#/agentFs';
|
||||
import { IKaos } from '#/kaos';
|
||||
import { ToolAccesses } from '#/tool';
|
||||
import type { BuiltinTool, ExecutableToolResult, ToolExecution } from '#/tool';
|
||||
import { resolvePathAccessPath } from '#/_base/tools/policies/path-access';
|
||||
|
|
@ -63,7 +64,7 @@ export const ReadMediaFileInputSchema = z.object({
|
|||
),
|
||||
});
|
||||
|
||||
export type ReadMediaFileInput = z.Infer<typeof ReadMediaFileInputSchema>;
|
||||
export type ReadMediaFileInput = z.infer<typeof ReadMediaFileInputSchema>;
|
||||
|
||||
// ── Tool description (capability-driven) ─────────────────────────────
|
||||
|
||||
|
|
@ -135,7 +136,8 @@ export class ReadMediaFileTool implements BuiltinTool<ReadMediaFileInput> {
|
|||
readonly description: string;
|
||||
readonly parameters: Record<string, unknown> = toInputJsonSchema(ReadMediaFileInputSchema);
|
||||
constructor(
|
||||
private readonly kaos: Kaos,
|
||||
private readonly fs: IAgentFileSystem,
|
||||
private readonly kaos: IKaos,
|
||||
private readonly workspace: WorkspaceConfig,
|
||||
private readonly capabilities: ModelCapability,
|
||||
private readonly videoUploader?: VideoUploader | undefined,
|
||||
|
|
@ -181,7 +183,7 @@ export class ReadMediaFileTool implements BuiltinTool<ReadMediaFileInput> {
|
|||
try {
|
||||
// For media input, the bytes are authoritative; the extension is only
|
||||
// a fallback for formats that cannot be sniffed from the header.
|
||||
const header = await this.kaos.readBytes(safePath, MEDIA_SNIFF_BYTES);
|
||||
const header = await this.fs.readBytes(safePath, MEDIA_SNIFF_BYTES);
|
||||
const fileType = detectFileType(safePath, header, 'media');
|
||||
|
||||
if (fileType.kind === 'text') {
|
||||
|
|
@ -216,20 +218,20 @@ export class ReadMediaFileTool implements BuiltinTool<ReadMediaFileInput> {
|
|||
};
|
||||
}
|
||||
|
||||
const stat = await this.kaos.stat(safePath);
|
||||
if (stat.stSize === 0) {
|
||||
const stat = await this.fs.stat(safePath);
|
||||
if (stat.size === 0) {
|
||||
return { isError: true, output: `"${args.path}" is empty.` };
|
||||
}
|
||||
if (stat.stSize > MAX_MEDIA_BYTES) {
|
||||
if (stat.size > MAX_MEDIA_BYTES) {
|
||||
return {
|
||||
isError: true,
|
||||
output:
|
||||
`"${args.path}" is ${String(stat.stSize)} bytes, which exceeds the ` +
|
||||
`"${args.path}" is ${String(stat.size)} bytes, which exceeds the ` +
|
||||
`maximum ${String(MAX_MEDIA_MEGABYTES)}MB for media files.`,
|
||||
};
|
||||
}
|
||||
|
||||
const data = await this.kaos.readBytes(safePath);
|
||||
const data = Buffer.from(await this.fs.readBytes(safePath));
|
||||
const base64 = data.toString('base64');
|
||||
let mediaPart: ContentPart;
|
||||
if (fileType.kind === 'image') {
|
||||
|
|
@ -259,7 +261,7 @@ export class ReadMediaFileTool implements BuiltinTool<ReadMediaFileInput> {
|
|||
const systemText = buildSystemSummary({
|
||||
kind: fileType.kind,
|
||||
mimeType: fileType.mimeType,
|
||||
byteSize: stat.stSize,
|
||||
byteSize: stat.size,
|
||||
dimensions,
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -1,12 +1,8 @@
|
|||
/**
|
||||
* `process` domain barrel — re-exports the process contract (`process`), its
|
||||
* scoped service (`processRunnerService`), and the backend implementations
|
||||
* (`localProcessBackend`, `sshProcessBackend`). Importing this barrel
|
||||
* registers the `IProcessRunner` and default local `IProcessBackend` bindings
|
||||
* into the scope registry.
|
||||
* `process` domain barrel — re-exports the process contract (`process`) and
|
||||
* its scoped service (`processRunnerService`). Importing this barrel registers
|
||||
* the `IProcessRunner` binding into the scope registry.
|
||||
*/
|
||||
|
||||
export * from './process';
|
||||
export * from './processRunnerService';
|
||||
export * from './localProcessBackend';
|
||||
export * from './sshProcessBackend';
|
||||
|
|
|
|||
|
|
@ -1,72 +0,0 @@
|
|||
/**
|
||||
* `process` domain (L1) — local `IProcessBackend` implementation.
|
||||
*
|
||||
* Spawns real child processes on the host through `node:child_process`.
|
||||
* Registered as the default `IProcessBackend` at Session scope; remote
|
||||
* backends override it via the scope registry.
|
||||
*/
|
||||
|
||||
import { type ChildProcess, spawn } from 'node:child_process';
|
||||
import type { Readable, Writable } from 'node:stream';
|
||||
|
||||
import { InstantiationType } from '#/_base/di/extensions';
|
||||
import { LifecycleScope, registerScopedService } from '#/_base/di/scope';
|
||||
|
||||
import { type IProcess, IProcessBackend } from './process';
|
||||
|
||||
class LocalProcess implements IProcess {
|
||||
readonly stdin: Writable;
|
||||
readonly stdout: Readable;
|
||||
readonly stderr: Readable;
|
||||
readonly pid: number;
|
||||
|
||||
constructor(private readonly child: ChildProcess) {
|
||||
if (child.stdin === null || child.stdout === null || child.stderr === null) {
|
||||
throw new Error('LocalProcess: child must be spawned with piped stdio.');
|
||||
}
|
||||
this.stdin = child.stdin;
|
||||
this.stdout = child.stdout;
|
||||
this.stderr = child.stderr;
|
||||
this.pid = child.pid ?? -1;
|
||||
}
|
||||
|
||||
wait(): Promise<number> {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.child.once('exit', (code) => resolve(code ?? -1));
|
||||
this.child.once('error', reject);
|
||||
});
|
||||
}
|
||||
|
||||
kill(signal?: NodeJS.Signals): Promise<void> {
|
||||
this.child.kill(signal);
|
||||
return Promise.resolve();
|
||||
}
|
||||
}
|
||||
|
||||
export class LocalProcessBackend implements IProcessBackend {
|
||||
declare readonly _serviceBrand: undefined;
|
||||
|
||||
spawn(
|
||||
args: readonly string[],
|
||||
options: { readonly cwd: string; readonly env?: Record<string, string> },
|
||||
): Promise<IProcess> {
|
||||
const [command, ...rest] = args;
|
||||
if (command === undefined) {
|
||||
return Promise.reject(new Error('LocalProcessBackend.spawn: command is required.'));
|
||||
}
|
||||
const child = spawn(command, rest, {
|
||||
cwd: options.cwd,
|
||||
env: options.env === undefined ? process.env : { ...process.env, ...options.env },
|
||||
stdio: ['pipe', 'pipe', 'pipe'],
|
||||
});
|
||||
return Promise.resolve(new LocalProcess(child));
|
||||
}
|
||||
}
|
||||
|
||||
registerScopedService(
|
||||
LifecycleScope.Session,
|
||||
IProcessBackend,
|
||||
LocalProcessBackend,
|
||||
InstantiationType.Delayed,
|
||||
'process',
|
||||
);
|
||||
|
|
@ -1,11 +1,10 @@
|
|||
/**
|
||||
* `process` domain (L1) — the Agent's process runner and its pluggable backend.
|
||||
* `process` domain (L1) — the Agent's process runner.
|
||||
*
|
||||
* Defines the `IProcessRunner` that business code injects to spawn processes
|
||||
* inside the Agent's execution environment, the `IProcess` handle it returns,
|
||||
* and the internal `IProcessBackend` provider that hides the
|
||||
* local/ssh/container split. Session-scoped. Business code depends on
|
||||
* `IProcessRunner` only; the backend is wired through the scope registry.
|
||||
* inside the Agent's execution environment, plus the `IProcess` handle it
|
||||
* returns. Session-scoped and backed by the session `IKaos`; business code
|
||||
* depends on `IProcessRunner` only.
|
||||
*/
|
||||
|
||||
import type { Readable, Writable } from 'node:stream';
|
||||
|
|
@ -17,8 +16,10 @@ export interface IProcess {
|
|||
readonly stdout: Readable;
|
||||
readonly stderr: Readable;
|
||||
readonly pid: number;
|
||||
readonly exitCode: number | null;
|
||||
wait(): Promise<number>;
|
||||
kill(signal?: NodeJS.Signals): Promise<void>;
|
||||
dispose(): Promise<void> | void;
|
||||
}
|
||||
|
||||
export interface ProcessExecOptions {
|
||||
|
|
@ -34,15 +35,3 @@ export interface IProcessRunner {
|
|||
|
||||
export const IProcessRunner: ServiceIdentifier<IProcessRunner> =
|
||||
createDecorator<IProcessRunner>('processRunner');
|
||||
|
||||
export interface IProcessBackend {
|
||||
readonly _serviceBrand: undefined;
|
||||
|
||||
spawn(
|
||||
args: readonly string[],
|
||||
options: { readonly cwd: string; readonly env?: Record<string, string> },
|
||||
): Promise<IProcess>;
|
||||
}
|
||||
|
||||
export const IProcessBackend: ServiceIdentifier<IProcessBackend> =
|
||||
createDecorator<IProcessBackend>('processBackend');
|
||||
|
|
|
|||
|
|
@ -1,34 +1,29 @@
|
|||
/**
|
||||
* `process` domain (L1) — `IProcessRunner` implementation.
|
||||
*
|
||||
* Resolves the working directory through `workspaceContext` and delegates
|
||||
* spawning to the injected `IProcessBackend`. Bound at Session scope.
|
||||
* Spawns processes through the session execution environment (`IKaos.backend`),
|
||||
* defaulting cwd/env to the execution environment and honoring per-call
|
||||
* overrides via `withCwd` / `execWithEnv`. Bound at Session scope.
|
||||
*/
|
||||
|
||||
import { InstantiationType } from '#/_base/di/extensions';
|
||||
import { LifecycleScope, registerScopedService } from '#/_base/di/scope';
|
||||
import { IWorkspaceContext } from '#/workspaceContext';
|
||||
import { IKaos } from '#/kaos';
|
||||
|
||||
import {
|
||||
type IProcess,
|
||||
IProcessBackend,
|
||||
IProcessRunner,
|
||||
type ProcessExecOptions,
|
||||
} from './process';
|
||||
import { type IProcess, IProcessRunner, type ProcessExecOptions } from './process';
|
||||
|
||||
export class ProcessRunner implements IProcessRunner {
|
||||
declare readonly _serviceBrand: undefined;
|
||||
|
||||
constructor(
|
||||
@IProcessBackend private readonly backend: IProcessBackend,
|
||||
@IWorkspaceContext private readonly workspace: IWorkspaceContext,
|
||||
) {}
|
||||
constructor(@IKaos private readonly kaos: IKaos) {}
|
||||
|
||||
exec(args: readonly string[], options?: ProcessExecOptions): Promise<IProcess> {
|
||||
return this.backend.spawn(args, {
|
||||
cwd: options?.cwd ?? this.workspace.workDir,
|
||||
env: options?.env,
|
||||
});
|
||||
const k = options?.cwd !== undefined ? this.kaos.withCwd(options.cwd) : this.kaos;
|
||||
const env =
|
||||
options?.env !== undefined
|
||||
? ({ ...process.env, ...options.env } as Record<string, string>)
|
||||
: undefined;
|
||||
return k.backend.execWithEnv([...args], env);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,22 +0,0 @@
|
|||
/**
|
||||
* `process` domain (L1) — ssh `IProcessBackend` stub.
|
||||
*
|
||||
* Placeholder for the remote backend; not registered into the scope registry
|
||||
* yet. A composition root that needs ssh supplies it through
|
||||
* `ScopeOptions.extra` to override the local backend.
|
||||
*/
|
||||
|
||||
import { NotImplementedError } from '#/_base/errors';
|
||||
|
||||
import { type IProcess, IProcessBackend } from './process';
|
||||
|
||||
export class SshProcessBackend implements IProcessBackend {
|
||||
declare readonly _serviceBrand: undefined;
|
||||
|
||||
spawn(
|
||||
_args: readonly string[],
|
||||
_options: { readonly cwd: string; readonly env?: Record<string, string> },
|
||||
): Promise<IProcess> {
|
||||
throw new NotImplementedError('sshProcessBackend');
|
||||
}
|
||||
}
|
||||
|
|
@ -21,6 +21,7 @@ import {
|
|||
import { encodeWorkDirKey } from '#/_base/utils/workdir-slug';
|
||||
import { IBootstrapService } from '#/bootstrap';
|
||||
import { NotImplementedError } from '#/errors';
|
||||
import { IKaos, IKaosFactory } from '#/kaos';
|
||||
import { sessionLogSeed } from '#/log';
|
||||
import { ISessionService } from '#/session';
|
||||
import { type ISessionContext, sessionContextSeed } from '#/session-context';
|
||||
|
|
@ -39,6 +40,7 @@ export class SessionLifecycleService implements ISessionLifecycleService {
|
|||
constructor(
|
||||
@IInstantiationService private readonly instantiation: IInstantiationService,
|
||||
@IBootstrapService private readonly bootstrap: IBootstrapService,
|
||||
@IKaosFactory private readonly kaosFactory: IKaosFactory,
|
||||
) {}
|
||||
|
||||
async create(opts: CreateSessionOptions): Promise<IScopeHandle> {
|
||||
|
|
@ -52,11 +54,18 @@ export class SessionLifecycleService implements ISessionLifecycleService {
|
|||
sessionDir,
|
||||
metaScope,
|
||||
};
|
||||
const kaos = await this.kaosFactory.createLocal(opts.workDir);
|
||||
const handle = createScopedChildHandle(
|
||||
this.instantiation,
|
||||
LifecycleScope.Session,
|
||||
opts.sessionId,
|
||||
{ extra: [...sessionContextSeed(ctx), ...sessionLogSeed(opts.sessionId, sessionDir)] },
|
||||
{
|
||||
extra: [
|
||||
...sessionContextSeed(ctx),
|
||||
...sessionLogSeed(opts.sessionId, sessionDir),
|
||||
[IKaos, kaos] as const,
|
||||
],
|
||||
},
|
||||
);
|
||||
this.sessions.set(opts.sessionId, handle);
|
||||
await handle.accessor.get(ISessionMetadata).ready;
|
||||
|
|
|
|||
90
packages/agent-core-v2/test/agentFs/agentFsService.test.ts
Normal file
90
packages/agent-core-v2/test/agentFs/agentFsService.test.ts
Normal file
|
|
@ -0,0 +1,90 @@
|
|||
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
||||
|
||||
import { mkdtemp, rm } from 'node:fs/promises';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
|
||||
import { InstantiationType } from '#/_base/di/extensions';
|
||||
import {
|
||||
LifecycleScope,
|
||||
_clearScopedRegistryForTests,
|
||||
registerScopedService,
|
||||
} from '#/_base/di/scope';
|
||||
import { createScopedTestHost, stubPair } from '#/_base/di/test';
|
||||
import { AgentFileSystem, IAgentFileSystem } from '#/agentFs';
|
||||
import { IKaos, IKaosFactory, KaosFactory } from '#/kaos';
|
||||
|
||||
describe('AgentFileSystem (backed by IKaos)', () => {
|
||||
let dir: string;
|
||||
|
||||
beforeEach(async () => {
|
||||
_clearScopedRegistryForTests();
|
||||
registerScopedService(
|
||||
LifecycleScope.Core,
|
||||
IKaosFactory,
|
||||
KaosFactory,
|
||||
InstantiationType.Delayed,
|
||||
'kaos',
|
||||
);
|
||||
registerScopedService(
|
||||
LifecycleScope.Session,
|
||||
IAgentFileSystem,
|
||||
AgentFileSystem,
|
||||
InstantiationType.Delayed,
|
||||
'agentFs',
|
||||
);
|
||||
dir = await mkdtemp(join(tmpdir(), 'agentfs-'));
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await rm(dir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
async function makeFs(): Promise<IAgentFileSystem> {
|
||||
const host = createScopedTestHost();
|
||||
const factory = host.core.accessor.get(IKaosFactory);
|
||||
const kaos = await factory.createLocal(dir);
|
||||
const session = host.child(LifecycleScope.Session, 's', [stubPair(IKaos, kaos)]);
|
||||
return session.accessor.get(IAgentFileSystem);
|
||||
}
|
||||
|
||||
it('writes and reads text relative to cwd', async () => {
|
||||
const fs = await makeFs();
|
||||
await fs.writeText('a.txt', 'hello');
|
||||
expect(await fs.readText('a.txt')).toBe('hello');
|
||||
});
|
||||
|
||||
it('stat reports file kind and byte size', async () => {
|
||||
const fs = await makeFs();
|
||||
await fs.writeText('b.txt', 'abc');
|
||||
const st = await fs.stat('b.txt');
|
||||
expect(st.isFile).toBe(true);
|
||||
expect(st.isDirectory).toBe(false);
|
||||
expect(st.size).toBe(3);
|
||||
});
|
||||
|
||||
it('stat reports directories', async () => {
|
||||
const fs = await makeFs();
|
||||
await fs.mkdir('sub');
|
||||
const st = await fs.stat('sub');
|
||||
expect(st.isDirectory).toBe(true);
|
||||
expect(st.isFile).toBe(false);
|
||||
});
|
||||
|
||||
it('readdir returns entry names', async () => {
|
||||
const fs = await makeFs();
|
||||
await fs.writeText('x.txt', '');
|
||||
await fs.mkdir('sub');
|
||||
const names = [...(await fs.readdir('.'))].sort();
|
||||
expect(names).toEqual(['sub', 'x.txt']);
|
||||
});
|
||||
|
||||
it('withCwd derives a sub-view rooted at the new cwd', async () => {
|
||||
const fs = await makeFs();
|
||||
await fs.mkdir('sub');
|
||||
await fs.writeText('sub/c.txt', 'deep');
|
||||
const sub = fs.withCwd(join(dir, 'sub'));
|
||||
expect(sub.cwd).toBe(join(dir, 'sub'));
|
||||
expect(await sub.readText('c.txt')).toBe('deep');
|
||||
});
|
||||
});
|
||||
61
packages/agent-core-v2/test/kaos/kaosFactory.test.ts
Normal file
61
packages/agent-core-v2/test/kaos/kaosFactory.test.ts
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
import { beforeEach, describe, expect, it } from 'vitest';
|
||||
|
||||
import { InstantiationType } from '#/_base/di/extensions';
|
||||
import {
|
||||
LifecycleScope,
|
||||
_clearScopedRegistryForTests,
|
||||
registerScopedService,
|
||||
} from '#/_base/di/scope';
|
||||
import { createScopedTestHost } from '#/_base/di/test';
|
||||
import { IKaosFactory, KaosFactory } from '#/kaos';
|
||||
|
||||
describe('KaosFactory', () => {
|
||||
beforeEach(() => {
|
||||
_clearScopedRegistryForTests();
|
||||
registerScopedService(
|
||||
LifecycleScope.Core,
|
||||
IKaosFactory,
|
||||
KaosFactory,
|
||||
InstantiationType.Delayed,
|
||||
'kaos',
|
||||
);
|
||||
});
|
||||
|
||||
it('createLocal builds an IKaos rooted at the given cwd', async () => {
|
||||
const host = createScopedTestHost();
|
||||
const factory = host.core.accessor.get(IKaosFactory);
|
||||
const k = await factory.createLocal(process.cwd());
|
||||
|
||||
expect(k.name).toBe('local');
|
||||
expect(k.getcwd()).toBe(process.cwd());
|
||||
expect(k.cwd).toBe(process.cwd());
|
||||
expect(['posix', 'win32']).toContain(k.pathClass());
|
||||
expect(typeof k.osEnv.osKind).toBe('string');
|
||||
expect(typeof k.osEnv.shellPath).toBe('string');
|
||||
|
||||
host.dispose();
|
||||
});
|
||||
|
||||
it('withCwd derives an independent env without mutating the parent', async () => {
|
||||
const host = createScopedTestHost();
|
||||
const factory = host.core.accessor.get(IKaosFactory);
|
||||
const k = await factory.createLocal('/tmp');
|
||||
|
||||
const child = k.withCwd('/var');
|
||||
expect(child.getcwd()).toBe('/var');
|
||||
expect(k.getcwd()).toBe('/tmp');
|
||||
|
||||
host.dispose();
|
||||
});
|
||||
|
||||
it('backend delegates fs operations to the kaos backend', async () => {
|
||||
const host = createScopedTestHost();
|
||||
const factory = host.core.accessor.get(IKaosFactory);
|
||||
const k = await factory.createLocal(process.cwd());
|
||||
|
||||
const st = await k.backend.stat(process.cwd());
|
||||
expect(typeof st.stSize).toBe('number');
|
||||
|
||||
host.dispose();
|
||||
});
|
||||
});
|
||||
|
|
@ -1,15 +1,15 @@
|
|||
/**
|
||||
* ReadMediaFileTool tests for the v2 output/capability contract.
|
||||
*
|
||||
* Self-contained: builds a minimal fake `Kaos` inline (the v2 shared kaos
|
||||
* fixtures do not exist yet) so the tool can be exercised without the
|
||||
* missing composition root.
|
||||
* Self-contained: builds minimal fake `IAgentFileSystem` and `IKaos` inline
|
||||
* so the tool can be exercised without the missing composition root.
|
||||
*/
|
||||
|
||||
import type { Kaos } from '@moonshot-ai/kaos';
|
||||
import type { ContentPart, ModelCapability } from '@moonshot-ai/kosong';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import type { IAgentFileSystem } from '../../src/agentFs';
|
||||
import type { IKaos } from '../../src/kaos';
|
||||
import {
|
||||
ReadMediaFileInputSchema,
|
||||
ReadMediaFileTool,
|
||||
|
|
@ -65,17 +65,27 @@ interface FakeFile {
|
|||
readonly size?: number;
|
||||
}
|
||||
|
||||
function createTestKaos(files: Record<string, FakeFile>): Kaos {
|
||||
function createTestFs(files: Record<string, FakeFile>): IAgentFileSystem {
|
||||
const lookup = (path: string): FakeFile | undefined => files[path];
|
||||
return {
|
||||
readBytes: vi.fn(async (path: string, _length?: number) => lookup(path)?.data ?? Buffer.alloc(0)),
|
||||
cwd: '/workspace',
|
||||
readBytes: vi.fn(async (path: string, _n?: number) => lookup(path)?.data ?? Buffer.alloc(0)),
|
||||
stat: vi.fn(async (path: string) => {
|
||||
const file = lookup(path);
|
||||
return { stSize: file?.size ?? file?.data.length ?? 0 };
|
||||
return {
|
||||
isFile: true,
|
||||
isDirectory: false,
|
||||
size: file?.size ?? file?.data.length ?? 0,
|
||||
};
|
||||
}),
|
||||
} as unknown as IAgentFileSystem;
|
||||
}
|
||||
|
||||
function createTestKaos(): IKaos {
|
||||
return {
|
||||
pathClass: () => 'posix',
|
||||
gethome: () => '/home',
|
||||
} as unknown as Kaos;
|
||||
} as unknown as IKaos;
|
||||
}
|
||||
|
||||
function makeTool(
|
||||
|
|
@ -83,7 +93,7 @@ function makeTool(
|
|||
caps: ModelCapability = capabilities(),
|
||||
videoUploader?: VideoUploader,
|
||||
): ReadMediaFileTool {
|
||||
return new ReadMediaFileTool(createTestKaos(files), WORKSPACE, caps, videoUploader);
|
||||
return new ReadMediaFileTool(createTestFs(files), createTestKaos(), WORKSPACE, caps, videoUploader);
|
||||
}
|
||||
|
||||
async function execute(
|
||||
|
|
@ -250,11 +260,13 @@ describe('ReadMediaFileTool', () => {
|
|||
});
|
||||
|
||||
describe('registerMediaTools', () => {
|
||||
const kaos = createTestKaos({});
|
||||
const fs = createTestFs({});
|
||||
const kaos = createTestKaos();
|
||||
|
||||
it('registers ReadMediaFile when the model supports image input', () => {
|
||||
const registry = new ToolRegistryService();
|
||||
const disposable = registerMediaTools(registry, {
|
||||
fs,
|
||||
kaos,
|
||||
workspace: WORKSPACE,
|
||||
capabilities: capabilities({ image_in: true, video_in: false }),
|
||||
|
|
@ -267,6 +279,7 @@ describe('registerMediaTools', () => {
|
|||
it('registers ReadMediaFile when the model supports video input', () => {
|
||||
const registry = new ToolRegistryService();
|
||||
registerMediaTools(registry, {
|
||||
fs,
|
||||
kaos,
|
||||
workspace: WORKSPACE,
|
||||
capabilities: capabilities({ image_in: false, video_in: true }),
|
||||
|
|
@ -277,6 +290,7 @@ describe('registerMediaTools', () => {
|
|||
it('does not register anything when the model lacks media capability', () => {
|
||||
const registry = new ToolRegistryService();
|
||||
const disposable = registerMediaTools(registry, {
|
||||
fs,
|
||||
kaos,
|
||||
workspace: WORKSPACE,
|
||||
capabilities: capabilities({ image_in: false, video_in: false }),
|
||||
|
|
|
|||
|
|
@ -0,0 +1,79 @@
|
|||
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
||||
|
||||
import { mkdtemp, rm } from 'node:fs/promises';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
import { Readable } from 'node:stream';
|
||||
|
||||
import { InstantiationType } from '#/_base/di/extensions';
|
||||
import {
|
||||
LifecycleScope,
|
||||
_clearScopedRegistryForTests,
|
||||
registerScopedService,
|
||||
} from '#/_base/di/scope';
|
||||
import { createScopedTestHost, stubPair } from '#/_base/di/test';
|
||||
import { IKaos, IKaosFactory, KaosFactory } from '#/kaos';
|
||||
import { IProcessRunner, ProcessRunner } from '#/process';
|
||||
|
||||
async function collect(stream: Readable): Promise<string> {
|
||||
const chunks: Buffer[] = [];
|
||||
for await (const chunk of stream) {
|
||||
chunks.push(chunk as Buffer);
|
||||
}
|
||||
return Buffer.concat(chunks).toString('utf8');
|
||||
}
|
||||
|
||||
describe('ProcessRunner (backed by IKaos)', () => {
|
||||
let dir: string;
|
||||
|
||||
beforeEach(async () => {
|
||||
_clearScopedRegistryForTests();
|
||||
registerScopedService(
|
||||
LifecycleScope.Core,
|
||||
IKaosFactory,
|
||||
KaosFactory,
|
||||
InstantiationType.Delayed,
|
||||
'kaos',
|
||||
);
|
||||
registerScopedService(
|
||||
LifecycleScope.Session,
|
||||
IProcessRunner,
|
||||
ProcessRunner,
|
||||
InstantiationType.Delayed,
|
||||
'process',
|
||||
);
|
||||
dir = await mkdtemp(join(tmpdir(), 'procrunner-'));
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await rm(dir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
async function makeRunner(): Promise<IProcessRunner> {
|
||||
const host = createScopedTestHost();
|
||||
const factory = host.core.accessor.get(IKaosFactory);
|
||||
const kaos = await factory.createLocal(dir);
|
||||
const session = host.child(LifecycleScope.Session, 's', [stubPair(IKaos, kaos)]);
|
||||
return session.accessor.get(IProcessRunner);
|
||||
}
|
||||
|
||||
it('exec runs a command and captures stdout + exit code', async () => {
|
||||
const runner = await makeRunner();
|
||||
const proc = await runner.exec(['node', '-e', 'process.stdout.write("ok")']);
|
||||
const out = await collect(proc.stdout);
|
||||
expect(out).toBe('ok');
|
||||
expect(await proc.wait()).toBe(0);
|
||||
expect(proc.exitCode).toBe(0);
|
||||
});
|
||||
|
||||
it('exec overlays per-call env', async () => {
|
||||
const runner = await makeRunner();
|
||||
const proc = await runner.exec(
|
||||
['node', '-e', 'process.stdout.write(process.env.FOO ?? "")'],
|
||||
{ env: { FOO: 'bar' } },
|
||||
);
|
||||
const out = await collect(proc.stdout);
|
||||
expect(out).toBe('bar');
|
||||
expect(await proc.wait()).toBe(0);
|
||||
});
|
||||
});
|
||||
|
|
@ -8,6 +8,7 @@ import {
|
|||
} from '#/_base/di/scope';
|
||||
import { type ScopedTestHost, createScopedTestHost, stubPair } from '#/_base/di/test';
|
||||
import { IBootstrapService } from '#/bootstrap';
|
||||
import { IKaosFactory, type IKaos } from '#/kaos';
|
||||
import { ISessionService } from '#/session';
|
||||
import { ISessionLifecycleService } from '#/session-lifecycle/sessionLifecycle';
|
||||
import { SessionLifecycleService } from '#/session-lifecycle/sessionLifecycleService';
|
||||
|
|
@ -32,6 +33,26 @@ function metadataStub(): ISessionMetadata {
|
|||
};
|
||||
}
|
||||
|
||||
function kaosFactoryStub(): IKaosFactory {
|
||||
const kaos: IKaos = {
|
||||
_serviceBrand: undefined,
|
||||
name: 'local',
|
||||
cwd: '/tmp/proj',
|
||||
osEnv: { osKind: 'test', osArch: 'x64', osVersion: '', shellName: 'sh', shellPath: '/bin/sh' },
|
||||
backend: undefined as never,
|
||||
pathClass: () => 'posix',
|
||||
normpath: (p) => p,
|
||||
gethome: () => '/home',
|
||||
getcwd: () => '/tmp/proj',
|
||||
withCwd: (cwd) => ({ ...kaos, cwd, getcwd: () => cwd }),
|
||||
withEnv: () => kaos,
|
||||
};
|
||||
return {
|
||||
_serviceBrand: undefined,
|
||||
createLocal: (cwd) => Promise.resolve({ ...kaos, cwd, getcwd: () => cwd }),
|
||||
};
|
||||
}
|
||||
|
||||
describe('SessionLifecycleService', () => {
|
||||
let host: ScopedTestHost | undefined;
|
||||
|
||||
|
|
@ -55,6 +76,7 @@ describe('SessionLifecycleService', () => {
|
|||
host = createScopedTestHost([
|
||||
stubPair(IBootstrapService, bootstrapStub()),
|
||||
stubPair(ISessionMetadata, metadataStub()),
|
||||
stubPair(IKaosFactory, kaosFactoryStub()),
|
||||
...extra,
|
||||
]);
|
||||
return host.core.accessor.get(ISessionLifecycleService);
|
||||
|
|
|
|||
|
|
@ -33,7 +33,6 @@
|
|||
*/
|
||||
|
||||
import {
|
||||
IAgentLifecycleService,
|
||||
IContextMemory,
|
||||
ISessionIndex,
|
||||
ISessionLifecycleService,
|
||||
|
|
@ -50,9 +49,7 @@ import { z } from 'zod';
|
|||
|
||||
import { errEnvelope, okEnvelope } from '../envelope';
|
||||
import { defineRoute } from '../middleware/defineRoute';
|
||||
|
||||
/** Agent id that owns the session's primary conversation history. */
|
||||
const MAIN_AGENT_ID = 'main';
|
||||
import { ensureMainAgent } from '../transport/mainAgent';
|
||||
|
||||
/** One entry from the main agent's live history. */
|
||||
type MemoryMessage = ReturnType<IContextMemory['get']>[number];
|
||||
|
|
@ -232,8 +229,9 @@ async function loadProtocolMessages(core: Scope, sid: string): Promise<Message[]
|
|||
if (summary === undefined) return undefined;
|
||||
|
||||
const session = core.accessor.get(ISessionLifecycleService).get(sid);
|
||||
const agent = session?.accessor.get(IAgentLifecycleService).getHandle(MAIN_AGENT_ID);
|
||||
const history = agent?.accessor.get(IContextMemory).get() ?? [];
|
||||
if (session === undefined) return [];
|
||||
const agent = await ensureMainAgent(session);
|
||||
const history = agent.accessor.get(IContextMemory).get();
|
||||
|
||||
return history.map((msg, index) => toProtocolMessage(sid, index, msg, summary.createdAt));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@
|
|||
*/
|
||||
|
||||
import {
|
||||
IAgentLifecycleService,
|
||||
IPromptLegacyService,
|
||||
ISessionLifecycleService,
|
||||
isKimiError,
|
||||
|
|
@ -26,6 +25,7 @@ import { z } from 'zod';
|
|||
|
||||
import { errEnvelope, okEnvelope } from '../envelope';
|
||||
import { defineRoute } from '../middleware/defineRoute';
|
||||
import { ensureMainAgent } from '../transport/mainAgent';
|
||||
import { parseActionSuffix } from './action-suffix';
|
||||
|
||||
interface PromptRouteHost {
|
||||
|
|
@ -53,17 +53,12 @@ const sessionIdParamSchema = z.object({
|
|||
|
||||
const detailsSchema = z.array(z.object({ path: z.string(), message: z.string() }));
|
||||
|
||||
const MAIN_AGENT_ID = 'main';
|
||||
|
||||
function resolveLegacy(core: Scope, sessionId: string): IPromptLegacyService {
|
||||
async function resolveLegacy(core: Scope, sessionId: string): Promise<IPromptLegacyService> {
|
||||
const session = core.accessor.get(ISessionLifecycleService).get(sessionId);
|
||||
if (session === undefined) {
|
||||
throw new KimiError('session.not_found', `session ${sessionId} does not exist`);
|
||||
}
|
||||
const agent = session.accessor.get(IAgentLifecycleService).getHandle(MAIN_AGENT_ID);
|
||||
if (agent === undefined) {
|
||||
throw new KimiError('agent.not_found', `main agent not found for session ${sessionId}`);
|
||||
}
|
||||
const agent = await ensureMainAgent(session);
|
||||
return agent.accessor.get(IPromptLegacyService);
|
||||
}
|
||||
|
||||
|
|
@ -82,7 +77,7 @@ export function registerPromptsRoutes(app: PromptRouteHost, core: Scope): void {
|
|||
async (req, reply) => {
|
||||
try {
|
||||
const { session_id } = req.params;
|
||||
const result = resolveLegacy(core, session_id).list();
|
||||
const result = (await resolveLegacy(core, session_id)).list();
|
||||
reply.send(okEnvelope(result, req.id));
|
||||
} catch (error) {
|
||||
sendMappedError(reply, req.id, error);
|
||||
|
|
@ -111,7 +106,8 @@ export function registerPromptsRoutes(app: PromptRouteHost, core: Scope): void {
|
|||
async (req, reply) => {
|
||||
try {
|
||||
const { session_id } = req.params;
|
||||
const result = await resolveLegacy(core, session_id).submit(req.body);
|
||||
const legacy = await resolveLegacy(core, session_id);
|
||||
const result = await legacy.submit(req.body);
|
||||
reply.send(okEnvelope(result, req.id));
|
||||
} catch (error) {
|
||||
sendMappedError(reply, req.id, error);
|
||||
|
|
@ -139,7 +135,8 @@ export function registerPromptsRoutes(app: PromptRouteHost, core: Scope): void {
|
|||
async (req, reply) => {
|
||||
try {
|
||||
const { session_id } = req.params;
|
||||
const result = await resolveLegacy(core, session_id).steer(req.body.prompt_ids);
|
||||
const legacy = await resolveLegacy(core, session_id);
|
||||
const result = await legacy.steer(req.body.prompt_ids);
|
||||
reply.send(okEnvelope(result, req.id));
|
||||
} catch (error) {
|
||||
sendMappedError(reply, req.id, error);
|
||||
|
|
@ -176,7 +173,7 @@ export function registerPromptsRoutes(app: PromptRouteHost, core: Scope): void {
|
|||
reply.send(errEnvelope(ErrorCode.VALIDATION_FAILED, message, req.id));
|
||||
return;
|
||||
}
|
||||
const legacy = resolveLegacy(core, session_id);
|
||||
const legacy = await resolveLegacy(core, session_id);
|
||||
const result =
|
||||
parsed.action === 'abort'
|
||||
? await legacy.abort(parsed.id)
|
||||
|
|
|
|||
|
|
@ -39,7 +39,6 @@
|
|||
*/
|
||||
|
||||
import {
|
||||
IAgentLifecycleService,
|
||||
IBackgroundService,
|
||||
ISessionIndex,
|
||||
ISessionLifecycleService,
|
||||
|
|
@ -59,11 +58,9 @@ import { z } from 'zod';
|
|||
|
||||
import { errEnvelope, okEnvelope } from '../envelope';
|
||||
import { defineRoute } from '../middleware/defineRoute';
|
||||
import { ensureMainAgent } from '../transport/mainAgent';
|
||||
import { parseActionSuffix } from './action-suffix';
|
||||
|
||||
/** Agent id that owns the session's background tasks. */
|
||||
const MAIN_AGENT_ID = 'main';
|
||||
|
||||
/** Default cap (bytes) for the opt-in output preview on GET-by-id. */
|
||||
const DEFAULT_TASK_OUTPUT_PREVIEW_BYTES = 32 * 1024;
|
||||
|
||||
|
|
@ -281,8 +278,9 @@ async function resolveSessionBackground(core: Scope, sid: string): Promise<Resol
|
|||
if (summary === undefined) return { kind: 'not_found' };
|
||||
|
||||
const session = core.accessor.get(ISessionLifecycleService).get(sid);
|
||||
const agent = session?.accessor.get(IAgentLifecycleService).getHandle(MAIN_AGENT_ID);
|
||||
const bg = agent?.accessor.get(IBackgroundService);
|
||||
if (session === undefined) return { kind: 'resolved', bg: undefined };
|
||||
const agent = await ensureMainAgent(session);
|
||||
const bg = agent.accessor.get(IBackgroundService);
|
||||
return { kind: 'resolved', bg };
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -45,7 +45,6 @@
|
|||
|
||||
import {
|
||||
ErrorCodes,
|
||||
IAgentLifecycleService,
|
||||
IMcpService,
|
||||
ISessionIndex,
|
||||
ISessionLifecycleService,
|
||||
|
|
@ -67,11 +66,9 @@ import {
|
|||
|
||||
import { errEnvelope, okEnvelope } from '../envelope';
|
||||
import { defineRoute } from '../middleware/defineRoute';
|
||||
import { ensureMainAgent } from '../transport/mainAgent';
|
||||
import { parseActionSuffix } from './action-suffix';
|
||||
|
||||
/** Agent id that owns the session's tool registry and MCP connections. */
|
||||
const MAIN_AGENT_ID = 'main';
|
||||
|
||||
/** v2 MCP tool-name prefix / separator (see `mcp/tool-naming.ts`). */
|
||||
const MCP_NAME_PREFIX = 'mcp__';
|
||||
const MCP_NAME_SEPARATOR = '__';
|
||||
|
|
@ -217,7 +214,8 @@ async function resolveEffectiveAgent(core: Scope, sessionId: string | undefined)
|
|||
const sid = sessionId ?? (await mostRecentSessionId(core));
|
||||
if (sid === undefined) return undefined;
|
||||
const session = core.accessor.get(ISessionLifecycleService).get(sid);
|
||||
return session?.accessor.get(IAgentLifecycleService).getHandle(MAIN_AGENT_ID);
|
||||
if (session === undefined) return undefined;
|
||||
return ensureMainAgent(session);
|
||||
}
|
||||
|
||||
/** Pick the most-recently-created session id, mirroring v1's fallback. */
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@
|
|||
*/
|
||||
|
||||
import {
|
||||
IAgentFileSystem,
|
||||
IAgentRPCService,
|
||||
IApprovalService,
|
||||
IAuthSummaryService,
|
||||
|
|
@ -31,6 +32,7 @@ import {
|
|||
IContextMemory,
|
||||
IContextSizeService,
|
||||
IFlagService,
|
||||
IFsService,
|
||||
IGoalService,
|
||||
IHostFolderBrowser,
|
||||
IInteractionService,
|
||||
|
|
@ -139,6 +141,17 @@ export const actionMap: Record<ScopeKind, Record<string, ActionTarget>> = {
|
|||
'workspace:setWorkDir': { service: IWorkspaceContext, method: 'setWorkDir' },
|
||||
'workspace:addAdditionalDir': { service: IWorkspaceContext, method: 'addAdditionalDir' },
|
||||
'workspace:removeAdditionalDir': { service: IWorkspaceContext, method: 'removeAdditionalDir' },
|
||||
|
||||
'agentFs:readText': { service: IAgentFileSystem, method: 'readText', readonly: true },
|
||||
'agentFs:writeText': { service: IAgentFileSystem, method: 'writeText' },
|
||||
'agentFs:stat': { service: IAgentFileSystem, method: 'stat', readonly: true },
|
||||
'agentFs:readdir': { service: IAgentFileSystem, method: 'readdir', readonly: true },
|
||||
'agentFs:mkdir': { service: IAgentFileSystem, method: 'mkdir' },
|
||||
|
||||
'fs:search': { service: IFsService, method: 'search', readonly: true },
|
||||
'fs:grep': { service: IFsService, method: 'grep', readonly: true },
|
||||
'fs:gitStatus': { service: IFsService, method: 'gitStatus', readonly: true },
|
||||
'fs:diff': { service: IFsService, method: 'diff', readonly: true },
|
||||
},
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
|
|
|
|||
|
|
@ -17,16 +17,17 @@ import {
|
|||
import { resolveAction } from './actionMap';
|
||||
import type { ActionTarget, ScopeKind, ServiceAction } from './channel';
|
||||
import { assertSerializable } from './errors';
|
||||
import { MAIN_AGENT_ID, ensureMainAgent } from './mainAgent';
|
||||
|
||||
/**
|
||||
* Resolve the scope a request targets. Returns `undefined` when the referenced
|
||||
* session / agent does not exist (caller maps to `40401`).
|
||||
*/
|
||||
export function resolveScope(
|
||||
export async function resolveScope(
|
||||
core: Scope,
|
||||
scopeKind: ScopeKind,
|
||||
params: Record<string, string>,
|
||||
): Scope | IScopeHandle | undefined {
|
||||
): Promise<Scope | IScopeHandle | undefined> {
|
||||
switch (scopeKind) {
|
||||
case 'core':
|
||||
return core;
|
||||
|
|
@ -39,6 +40,7 @@ export function resolveScope(
|
|||
const agentId = params['agent_id'] ?? '';
|
||||
const session = core.accessor.get(ISessionLifecycleService).get(sessionId);
|
||||
if (session === undefined) return undefined;
|
||||
if (agentId === MAIN_AGENT_ID) return ensureMainAgent(session);
|
||||
return session.accessor.get(IAgentLifecycleService).getHandle(agentId);
|
||||
}
|
||||
}
|
||||
|
|
@ -56,7 +58,7 @@ export async function dispatch(
|
|||
sa: ServiceAction,
|
||||
arg: unknown,
|
||||
): Promise<unknown> {
|
||||
const scope = resolveScope(core, scopeKind, params);
|
||||
const scope = await resolveScope(core, scopeKind, params);
|
||||
if (scope === undefined) {
|
||||
throw new KimiError(
|
||||
ErrorCodes.SESSION_NOT_FOUND,
|
||||
|
|
|
|||
23
packages/server-v2/src/transport/mainAgent.ts
Normal file
23
packages/server-v2/src/transport/mainAgent.ts
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
/**
|
||||
* server-v2 — on-demand main-agent resolution.
|
||||
*
|
||||
* Sessions are created without a main agent; the first request that targets
|
||||
* `main` materializes it here. Both the `/api/v1` routes and the `/api/v2`
|
||||
* dispatcher resolve the main agent through {@link ensureMainAgent} so a
|
||||
* missing main agent is created instead of reported as `agent.not_found`.
|
||||
*/
|
||||
|
||||
import { IAgentLifecycleService, type IScopeHandle } from '@moonshot-ai/agent-core-v2';
|
||||
|
||||
export const MAIN_AGENT_ID = 'main';
|
||||
|
||||
/**
|
||||
* Return the session's main agent, creating it on demand when it does not
|
||||
* exist yet.
|
||||
*/
|
||||
export async function ensureMainAgent(session: IScopeHandle): Promise<IScopeHandle> {
|
||||
const agents = session.accessor.get(IAgentLifecycleService);
|
||||
const existing = agents.getHandle(MAIN_AGENT_ID);
|
||||
if (existing !== undefined) return existing;
|
||||
return agents.createMain();
|
||||
}
|
||||
|
|
@ -172,7 +172,7 @@ export class WsConnection {
|
|||
}
|
||||
}
|
||||
|
||||
private onListen(msg: ListenMessage): void {
|
||||
private async onListen(msg: ListenMessage): Promise<void> {
|
||||
if (!this.gotHello) {
|
||||
this.send({ type: 'error', id: msg.id, code: 40112, msg: 'hello required' });
|
||||
return;
|
||||
|
|
@ -190,7 +190,7 @@ export class WsConnection {
|
|||
|
||||
let scope;
|
||||
try {
|
||||
scope = resolveScope(this.core, msg.scope as ScopeKind, scopeParams(msg));
|
||||
scope = await resolveScope(this.core, msg.scope as ScopeKind, scopeParams(msg));
|
||||
} catch {
|
||||
scope = undefined;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -209,7 +209,7 @@ describe('server-v2 /api/v2 RPC', () => {
|
|||
const id = await createSession(home as string);
|
||||
const { body } = await call<null>(
|
||||
'POST',
|
||||
`/api/v2/session/${id}/agent/main/prompts:submit`,
|
||||
`/api/v2/session/${id}/agent/does-not-exist/prompts:submit`,
|
||||
{ input: [{ type: 'text', text: 'hello' }] },
|
||||
);
|
||||
expect(body.code).toBe(40401);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue