refactor(agent-core-v2): drop the @moonshot-ai/protocol dependency (#1745)

This commit is contained in:
_Kerman 2026-07-15 16:03:19 +08:00 committed by GitHub
parent 481b28b8f4
commit 27236bd75f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
82 changed files with 1389 additions and 225 deletions

View file

@ -62,7 +62,6 @@
"@modelcontextprotocol/sdk": "^1.29.0",
"@moonshot-ai/kimi-code-oauth": "workspace:^",
"@moonshot-ai/minidb": "workspace:^",
"@moonshot-ai/protocol": "workspace:^",
"@mozilla/readability": "^0.6.0",
"ajv": "^8.18.0",
"ajv-formats": "^3.0.1",

View file

@ -304,6 +304,11 @@ const ALLOWED_EXCEPTIONS = new Set([
'auth>tool',
'auth>toolRegistry',
'permissionGate>approval',
// `permissionRules` (L3) persists the approval broker's `ApprovalResponse`
// (Session, L7) verbatim in its wire-logged `PermissionApprovalResultRecord`
// — a real cross-scope dependency, surfaced here rather than hidden behind a
// re-declared copy of the shape.
'permissionRules>approval',
'userTool>interaction',
'permissionPolicy>plan',
'permissionPolicy>swarm',

View file

@ -6,13 +6,12 @@
* codes, the registry (`registerErrorDomain` / `errorInfo` / `isErrorCode`) the
* serializer reads, and the domain-independent core codes (`internal`,
* `not_implemented`). Domain-owned codes live next to their owning domain and
* are aggregated into the public `ErrorCodes` const by `#/errors`.
* are aggregated into the public `ErrorCodes` const by `#/errors`, which also
* derives the `ErrorCode` union type from that aggregate so each domain's
* `errors.ts` is the single source of truth and there is no central
* hand-maintained list to keep in sync.
*/
import type { KimiErrorCode } from '@moonshot-ai/protocol';
export type ErrorCode = KimiErrorCode;
export interface ErrorInfo {
readonly title: string;
readonly retryable: boolean;
@ -21,18 +20,25 @@ export interface ErrorInfo {
}
export interface ErrorDomain {
readonly codes: { readonly [name: string]: ErrorCode };
readonly retryable?: ReadonlyArray<ErrorCode>;
readonly codes: { readonly [name: string]: string };
readonly retryable?: ReadonlyArray<string>;
readonly info?: { readonly [code: string]: ErrorInfo };
}
const registeredCodes = new Set<ErrorCode>();
const retryableCodes = new Set<ErrorCode>();
// Maps each registered code to the `codes` object that contributed it: a
// domain re-registering itself stays idempotent, while two different domains
// claiming the same code fail loudly at registration time.
const registeredCodes = new Map<string, object>();
const retryableCodes = new Set<string>();
const infoOverrides: { [code: string]: ErrorInfo } = {};
export function registerErrorDomain(domain: ErrorDomain): void {
for (const code of Object.values(domain.codes)) {
registeredCodes.add(code);
const owner = registeredCodes.get(code);
if (owner !== undefined && owner !== domain.codes) {
throw new Error(`error code '${code}' is registered by two different domains`);
}
registeredCodes.set(code, domain.codes);
}
for (const code of domain.retryable ?? []) {
retryableCodes.add(code);
@ -42,11 +48,11 @@ export function registerErrorDomain(domain: ErrorDomain): void {
}
}
export function isErrorCode(code: unknown): code is ErrorCode {
return typeof code === 'string' && registeredCodes.has(code as ErrorCode);
export function isErrorCode(code: unknown): code is string {
return typeof code === 'string' && registeredCodes.has(code);
}
export function errorInfo(code: ErrorCode): ErrorInfo {
export function errorInfo(code: string): ErrorInfo {
const override = infoOverrides[code];
if (override !== undefined) return override;
return {
@ -60,6 +66,7 @@ export const CoreErrors = {
codes: {
INTERNAL: 'internal',
NOT_IMPLEMENTED: 'not_implemented',
VALIDATION_FAILED: 'validation.failed',
},
info: {
internal: {

View file

@ -4,7 +4,7 @@
*/
import { CoreErrors } from './codes';
import type { ErrorCode } from './codes';
import type { ErrorCode } from '#/errors';
export class ExpectedError extends Error {
readonly isExpected = true;

View file

@ -9,7 +9,7 @@
*/
import { CoreErrors, errorInfo, isErrorCode } from './codes';
import type { ErrorCode } from './codes';
import type { ErrorCode } from '#/errors';
import { Error2 } from './errors';
export interface ErrorPayload {

View file

@ -0,0 +1,26 @@
import { z } from 'zod';
const ISO_8601_REGEX =
/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,9})?(?:Z|[+-]\d{2}(?::?\d{2})?)$/;
/**
* Wire-schema primitive for ISO 8601 datetime strings: validates the shape and
* normalizes to `Date#toISOString()` output. Shared by the edge DTO schemas
* (`sessionFs`, `file`, `terminal`, `auth`, ) that expose timestamps.
*/
export const isoDateTimeSchema = z
.string()
.refine((value) => ISO_8601_REGEX.test(value), {
message: 'must be an ISO 8601 datetime string',
})
.transform((value, ctx) => {
const ms = Date.parse(value);
if (Number.isNaN(ms)) {
ctx.addIssue({
code: 'custom',
message: 'invalid ISO 8601 datetime',
});
return z.NEVER;
}
return new Date(ms).toISOString();
});

View file

@ -18,7 +18,8 @@
import { createDecorator, type ServiceIdentifier } from '#/_base/di/instantiation';
import type { IDisposable } from '#/_base/di/lifecycle';
import type { PromptOrigin } from '#/agent/contextMemory/types';
import type { TurnEndReason } from '@moonshot-ai/protocol';
export type TurnEndReason = 'completed' | 'cancelled' | 'failed' | 'blocked';
export type AgentLifecycleState = 'initializing' | 'ready' | 'disposing' | 'disposed';

View file

@ -15,7 +15,7 @@
* so REST consumers can still render the media after reload/resume.
*/
import type { Message, MessageContent, MessageRole, ToolUseContent } from '@moonshot-ai/protocol';
import type { Message, MessageContent, MessageRole, ToolUseContent } from './wireMessage';
import type { ContextMessage } from './types';

View file

@ -1,7 +1,6 @@
import type { ContentPart, Message } from '#/app/llmProtocol/message';
import type { AgentTaskStatus } from '#/agent/task/task';
import type { CronJobOrigin, CronMissedOrigin, ShellCommandOrigin } from '@moonshot-ai/protocol';
export type SkillSource = 'project' | 'user' | 'extra' | 'builtin';
@ -36,6 +35,14 @@ export interface InjectionOrigin {
readonly variant: string;
}
export interface ShellCommandOrigin {
readonly kind: 'shell_command';
readonly phase: 'input' | 'output';
/** Only present on `phase: 'output'` whether the command failed, so replay
* can colour stderr red only for actual failures (not warnings). */
readonly isError?: boolean;
}
export interface CompactionSummaryOrigin {
readonly kind: 'compaction_summary';
}
@ -52,6 +59,20 @@ export interface TaskOrigin {
readonly notificationId: string;
}
export interface CronJobOrigin {
readonly kind: 'cron_job';
readonly jobId: string;
readonly cron: string;
readonly recurring: boolean;
readonly coalescedCount: number;
readonly stale: boolean;
}
export interface CronMissedOrigin {
readonly kind: 'cron_missed';
readonly count: number;
}
export interface HookResultOrigin {
readonly kind: 'hook_result';
readonly event: string;

View file

@ -0,0 +1,100 @@
/**
* The wire `Message` shape the legacy REST/streaming message format served
* on the `messages`, `snapshot`, and `sessions` (`:undo`) edge surfaces.
* Defined next to `messageProjection.ts`, which projects `ContextMessage`
* into this shape; consumed by the `messageLegacy` edge adapter and the
* transports.
*/
import { z } from 'zod';
import { isoDateTimeSchema } from '#/_base/utils/isoDateTime';
export const messageRoleSchema = z.enum(['user', 'assistant', 'tool', 'system']);
export type MessageRole = z.infer<typeof messageRoleSchema>;
export const textContentSchema = z.object({
type: z.literal('text'),
text: z.string(),
});
export type TextContent = z.infer<typeof textContentSchema>;
export const toolUseContentSchema = z.object({
type: z.literal('tool_use'),
tool_call_id: z.string().min(1),
tool_name: z.string().min(1),
input: z.unknown(),
});
export type ToolUseContent = z.infer<typeof toolUseContentSchema>;
export const toolResultContentSchema = z.object({
type: z.literal('tool_result'),
tool_call_id: z.string().min(1),
output: z.unknown(),
is_error: z.boolean().optional(),
});
export type ToolResultContent = z.infer<typeof toolResultContentSchema>;
export const imageSourceSchema = z.discriminatedUnion('kind', [
z.object({ kind: z.literal('url'), url: z.string().min(1) }),
z.object({
kind: z.literal('base64'),
media_type: z.string().min(1),
data: z.string().min(1),
}),
z.object({ kind: z.literal('file'), file_id: z.string().min(1) }),
]);
export type ImageSource = z.infer<typeof imageSourceSchema>;
export const imageContentSchema = z.object({
type: z.literal('image'),
source: imageSourceSchema,
});
export type ImageContent = z.infer<typeof imageContentSchema>;
// Video uses the same source shape as image (url / base64 / uploaded file id).
export const videoContentSchema = z.object({
type: z.literal('video'),
source: imageSourceSchema,
});
export type VideoContent = z.infer<typeof videoContentSchema>;
export const fileContentSchema = z.object({
type: z.literal('file'),
file_id: z.string().min(1),
name: z.string(),
media_type: z.string().min(1),
size: z.number().int().nonnegative(),
});
export type FileContent = z.infer<typeof fileContentSchema>;
export const thinkingContentSchema = z.object({
type: z.literal('thinking'),
thinking: z.string(),
signature: z.string().optional(),
});
export type ThinkingContent = z.infer<typeof thinkingContentSchema>;
export const messageContentSchema = z.discriminatedUnion('type', [
textContentSchema,
toolUseContentSchema,
toolResultContentSchema,
imageContentSchema,
videoContentSchema,
fileContentSchema,
thinkingContentSchema,
]);
export type MessageContent = z.infer<typeof messageContentSchema>;
export const messageSchema = z.object({
id: z.string().min(1),
session_id: z.string().min(1),
role: messageRoleSchema,
content: z.array(messageContentSchema),
created_at: isoDateTimeSchema,
prompt_id: z.string().min(1).optional(),
parent_message_id: z.string().min(1).optional(),
metadata: z.record(z.string(), z.unknown()).optional(),
});
export type Message = z.infer<typeof messageSchema>;

View file

@ -38,7 +38,7 @@ import {
IAgentPromptService,
type PromptSubmitContext,
} from '#/agent/prompt/prompt';
import type { HookResultEvent, TurnEndedEvent } from '@moonshot-ai/protocol';
import type { TurnEndedEvent } from '#/agent/loop/turnEvents';
import { IEventBus } from '#/app/event/eventBus';
import type { ExecutableToolResult } from '#/tool/toolContract';
import type { ToolDidExecuteContext, ToolBeforeExecuteContext } from '#/agent/toolExecutor/toolHooks';
@ -53,6 +53,14 @@ import {
renderUserPromptHookResult,
} from './user-prompt';
export interface HookResultEvent {
readonly type: 'hook.result';
readonly turnId?: number;
readonly hookEvent: string;
readonly content: string;
readonly blocked?: boolean;
}
declare module '#/app/event/eventBus' {
interface DomainEventMap {
'hook.result': HookResultEvent;

View file

@ -38,14 +38,28 @@
import { z } from 'zod';
import { defineModel } from '#/wire/model';
import type {
CompactionBlockedEvent,
CompactionCancelledEvent,
CompactionCompletedEvent,
CompactionStartedEvent,
} from '@moonshot-ai/protocol';
import type { CompactionBeginData } from './types';
import type { CompactionBeginData, CompactionResult } from './types';
export interface CompactionStartedEvent {
readonly type: 'compaction.started';
readonly trigger: 'manual' | 'auto';
readonly instruction?: string;
}
export interface CompactionBlockedEvent {
readonly type: 'compaction.blocked';
readonly turnId?: number;
}
export interface CompactionCancelledEvent {
readonly type: 'compaction.cancelled';
}
export interface CompactionCompletedEvent {
readonly type: 'compaction.completed';
readonly result: CompactionResult;
}
export type CompactionPhase = 'idle' | 'running' | 'cancelled' | 'completed';

View file

@ -1,13 +1,32 @@
import type { CompactionResult as ProtocolCompactionResult } from '@moonshot-ai/protocol';
export interface CompactionResult extends ProtocolCompactionResult {
export interface CompactionResult {
summary: string;
contextSummary?: string;
compactedCount: number;
tokensBefore: number;
tokensAfter: number;
/**
* Number of real user messages kept verbatim ahead of the summary in the
* post-compaction live context. Recorded so the wire-transcript reducer can
* reproduce the live folded length without re-deriving it from the full
* transcript (which still holds the untruncated originals of messages the
* live context may have truncated, so the two would otherwise diverge).
* Optional for backward compatibility with older wire records.
*/
keptUserMessageCount?: number;
/**
* Of `keptUserMessageCount`, how many messages form the head segment (the
* oldest user input kept when the pool overflowed the budget). Present iff
* the selection split into head + tail, in which case the live context also
* holds one elision-marker message between the segments. Optional for
* backward compatibility with older wire records.
*/
keptHeadUserMessageCount?: number;
/**
* Oldest messages trimmed from the summarizer input when the compaction
* request overflowed the model window; not covered by the produced summary.
* Mirrors agent-core's `CompactionResult.droppedCount`; optional for backward
* compatibility.
*/
droppedCount?: number;
}

View file

@ -29,7 +29,7 @@
import { randomUUID } from 'node:crypto';
import type { TurnEndedEvent, TurnStartedEvent } from '@moonshot-ai/protocol';
import type { TurnEndedEvent, TurnStartedEvent } from '#/agent/loop/turnEvents';
import { Disposable, MutableDisposable, type IDisposable } from '#/_base/di/lifecycle';
import { InstantiationType } from '#/_base/di/extensions';
import { LifecycleScope, registerScopedService } from '#/_base/di/scope';

View file

@ -7,7 +7,7 @@
import { z } from 'zod';
import type { ToolInputDisplay } from '@moonshot-ai/protocol';
import type { ToolInputDisplay } from '#/tool/toolInputDisplay';
import { toInputJsonSchema } from '#/tool/input-schema';
import { IAgentPermissionModeService } from '#/agent/permissionMode/permissionMode';

View file

@ -36,16 +36,6 @@ import { Disposable, toDisposable, type IDisposable } from '#/_base/di/lifecycle
import { LifecycleScope, registerScopedService } from '#/_base/di/scope';
import { abortError, isAbortError, isUserCancellation, userCancellationReason } from '#/_base/utils/abort';
import { toErrorMessage } from '#/_base/errors/errorMessage';
import type {
AssistantDeltaEvent,
ThinkingDeltaEvent,
ToolCallDeltaEvent,
TurnEndedEvent,
TurnStartedEvent,
TurnStepCompletedEvent,
TurnStepInterruptedEvent,
TurnStepStartedEvent,
} from '@moonshot-ai/protocol';
import { IAgentLLMRequesterService, type LLMRequestFinish } from '#/agent/llmRequester/llmRequester';
import { IAgentToolExecutorService } from '#/agent/toolExecutor/toolExecutor';
import { IConfigService } from '#/app/config/config';
@ -92,19 +82,10 @@ import {
} from './stepRequest';
import { StepRequestQueue, type StepRequestBatch } from './stepRequestQueue';
import { cancelTurn, promptTurn, TurnModel } from './turnOps';
declare module '#/app/event/eventBus' {
interface DomainEventMap {
'turn.started': TurnStartedEvent;
'turn.ended': TurnEndedEvent;
'turn.step.started': TurnStepStartedEvent;
'turn.step.completed': TurnStepCompletedEvent;
'turn.step.interrupted': TurnStepInterruptedEvent;
'assistant.delta': AssistantDeltaEvent;
'thinking.delta': ThinkingDeltaEvent;
'tool.call.delta': ToolCallDeltaEvent;
}
}
// Loads the `DomainEventMap` augmentation for the `turn.*` / delta events this
// service publishes (the augmentation lives with the event definitions;
// without an import it would not enter every consumer's program).
import './turnEvents';
export type LoopInterruptReason = 'aborted' | 'max_steps' | 'error';

View file

@ -0,0 +1,101 @@
/**
* `loop` domain the `turn.*` / delta event payloads published through
* `IEventBus` as a turn runs. These are the loop's share of the agent event
* stream; consumers (transports, replay, telemetry) subscribe by `type`.
*/
import type { KimiErrorPayload } from '#/_base/errors/serialize';
import type { PromptOrigin } from '#/agent/contextMemory/types';
import type { FinishReason } from '#/app/llmProtocol/finishReason';
import type { TokenUsage } from '#/app/llmProtocol/usage';
import type { TurnEndReason } from '#/activity/activity';
export interface TurnStartedEvent {
readonly type: 'turn.started';
readonly turnId: number;
readonly origin: PromptOrigin;
}
export interface TurnEndedEvent {
readonly type: 'turn.ended';
readonly turnId: number;
readonly reason: TurnEndReason;
readonly error?: KimiErrorPayload;
readonly durationMs?: number;
}
export interface TurnStepStartedEvent {
readonly type: 'turn.step.started';
readonly turnId: number;
readonly step: number;
readonly stepId?: string;
}
export interface TurnStepCompletedEvent {
readonly type: 'turn.step.completed';
readonly turnId: number;
readonly step: number;
readonly stepId?: string;
readonly usage?: TokenUsage;
readonly finishReason?: string;
readonly llmFirstTokenLatencyMs?: number;
readonly llmStreamDurationMs?: number;
/**
* Split of `llmFirstTokenLatencyMs`: in-process request-building time on the
* client vs. network + API-server time to the first token. Both omitted when
* the provider does not report the client/server boundary.
*/
readonly llmRequestBuildMs?: number;
readonly llmServerFirstTokenMs?: number;
/**
* Split of `llmStreamDurationMs` (the decode window): time awaiting parts from
* the provider vs. time processing parts in-process. Both omitted when the
* provider stream did not report decode accounting.
*/
readonly llmServerDecodeMs?: number;
readonly llmClientConsumeMs?: number;
readonly providerFinishReason?: FinishReason;
readonly rawFinishReason?: string;
}
export interface TurnStepInterruptedEvent {
readonly type: 'turn.step.interrupted';
readonly turnId: number;
readonly step: number;
readonly stepId?: string;
readonly reason: string;
readonly message?: string;
}
export interface AssistantDeltaEvent {
readonly type: 'assistant.delta';
readonly turnId: number;
readonly delta: string;
}
export interface ThinkingDeltaEvent {
readonly type: 'thinking.delta';
readonly turnId: number;
readonly delta: string;
}
export interface ToolCallDeltaEvent {
readonly type: 'tool.call.delta';
readonly turnId: number;
readonly toolCallId: string;
readonly name?: string;
readonly argumentsPart?: string;
}
declare module '#/app/event/eventBus' {
interface DomainEventMap {
'turn.started': TurnStartedEvent;
'turn.ended': TurnEndedEvent;
'turn.step.started': TurnStepStartedEvent;
'turn.step.completed': TurnStepCompletedEvent;
'turn.step.interrupted': TurnStepInterruptedEvent;
'assistant.delta': AssistantDeltaEvent;
'thinking.delta': ThinkingDeltaEvent;
'tool.call.delta': ToolCallDeltaEvent;
}
}

View file

@ -5,12 +5,8 @@ import { LifecycleScope, registerScopedService } from '#/_base/di/scope';
import type { Tool as KosongTool } from '#/app/llmProtocol/tool';
import { Disposable, type IDisposable } from "#/_base/di/lifecycle";
import type { KimiErrorPayload } from '#/_base/errors/serialize';
import { ErrorCodes, makeErrorPayload } from "#/errors";
import type {
ErrorEvent,
McpServerStatusEvent,
ToolListUpdatedEvent,
} from '@moonshot-ai/protocol';
import { IEventBus } from '#/app/event/eventBus';
import { ITelemetryService } from '#/app/telemetry/telemetry';
import { sessionMediaOriginalsDir } from '#/agent/media/image-originals';
@ -31,6 +27,31 @@ import {
type McpToolCollision,
} from './mcpDiscoveryOps';
export interface ErrorEvent extends KimiErrorPayload {
readonly type: 'error';
}
export interface McpServerStatusPayload {
readonly name: string;
readonly transport: 'stdio' | 'http' | 'sse';
readonly status: 'pending' | 'connected' | 'failed' | 'disabled' | 'needs-auth';
readonly toolCount: number;
readonly error?: string;
}
export interface McpServerStatusEvent {
readonly type: 'mcp.server.status';
readonly server: McpServerStatusPayload;
}
export type ToolListUpdatedReason = 'mcp.connected' | 'mcp.disconnected' | 'mcp.failed';
export interface ToolListUpdatedEvent {
readonly type: 'tool.list.updated';
readonly reason: ToolListUpdatedReason;
readonly serverName: string;
}
declare module '#/app/event/eventBus' {
interface DomainEventMap {
'mcp.server.status': McpServerStatusEvent;

View file

@ -32,13 +32,20 @@ import {
type ExecutableToolResult,
} from '#/tool/toolContract';
import { toInputJsonSchema } from '#/tool/input-schema';
import {
MCP_OAUTH_AUTHORIZATION_URL_TOOL_UPDATE,
type McpOAuthAuthorizationUrlUpdateData,
} from '@moonshot-ai/protocol';
import { AlreadyAuthorizedError, type McpOAuthService } from '#/agent/mcp/oauth/service';
import { qualifyMcpToolName } from '#/agent/mcp/tool-naming';
/**
* `ToolUpdate.customKind` emitted by the MCP auth tool when the OAuth
* authorization URL is ready; clients render it as an actionable login link.
*/
export const MCP_OAUTH_AUTHORIZATION_URL_TOOL_UPDATE = 'mcp.oauth.authorization_url';
export interface McpOAuthAuthorizationUrlUpdateData {
readonly serverName: string;
readonly authorizationUrl: string;
}
const DEFAULT_AUTH_TIMEOUT_MS = 15 * 60 * 1000;
const AUTH_TOOL_TOOL_NAME = 'authenticate';

View file

@ -23,7 +23,7 @@ import { IEventBus } from '#/app/event/eventBus';
import { ITelemetryService } from '#/app/telemetry/telemetry';
import { ISessionApprovalService } from "#/session/approval/approval";
import { ISessionContext } from '#/session/sessionContext/sessionContext';
import type { ToolInputDisplay } from '@moonshot-ai/protocol';
import type { ToolInputDisplay } from '#/tool/toolInputDisplay';
import {
IAgentPermissionGate,
} from './permissionGate';

View file

@ -1,5 +1,5 @@
import type { PrepareToolExecutionResult, ResolvedToolExecutionHookContext } from '#/agent/toolExecutor/toolHooks';
import type { ToolInputDisplay } from '@moonshot-ai/protocol';
import type { ToolInputDisplay } from '#/tool/toolInputDisplay';
import type { PermissionRule } from '#/agent/permissionRules/permissionRules';
export type PermissionMode = 'manual' | 'yolo' | 'auto';

View file

@ -1,5 +1,5 @@
import { createDecorator } from "#/_base/di/instantiation";
import type { ApprovalResponse } from "@moonshot-ai/protocol";
import type { ApprovalResponse } from "#/session/approval/approval";
export interface PermissionApprovalResultRecord {
readonly turnId: number;

View file

@ -15,7 +15,7 @@
* keeps the user-approved output and the `approved` outcome.
*/
import type { ToolInputDisplay } from '@moonshot-ai/protocol';
import type { ToolInputDisplay } from '#/tool/toolInputDisplay';
import { z } from 'zod';
import type { BuiltinTool, ExecutableToolResult, ToolExecution } from '#/tool/toolContract';

View file

@ -4,7 +4,7 @@ import type { ThinkingEffort } from '#/app/llmProtocol/thinkingEffort';
import type { Model } from '#/app/model/modelInstance';
import { createDecorator } from "#/_base/di/instantiation";
import type { ErrorCode } from '#/_base/errors/codes';
import type { ErrorCode } from '#/errors';
import { Error2 } from '#/_base/errors/errors';
import type { ToolSource } from '#/tool/toolContract';

View file

@ -51,7 +51,6 @@ import { ISessionWorkspaceContext } from '#/session/workspaceContext/workspaceCo
import { ISessionSkillCatalog } from '#/session/sessionSkillCatalog/skillCatalog';
import type { ResolvedAgentProfile, SystemPromptContext } from '#/agent/profile/profile';
import type { WarningEvent } from '@moonshot-ai/protocol';
import { ITelemetryService } from '#/app/telemetry/telemetry';
import { IAgentTelemetryContextService } from '#/app/telemetry/agentTelemetryContext';
import { IWireService } from '#/wire/wire';
@ -81,6 +80,12 @@ import {
type ProfileModelState,
} from './profileOps';
export interface WarningEvent {
readonly type: 'warning';
readonly message: string;
readonly code?: string;
}
declare module '#/app/event/eventBus' {
interface DomainEventMap {
warning: WarningEvent;

View file

@ -8,7 +8,7 @@ import type { PermissionData, PermissionMode } from '#/agent/permissionPolicy/ty
import type { PlanData } from '#/agent/plan/plan';
import type { ToolInfo } from '#/tool/toolContract';
import type { SessionSummary } from '#/agent/rpc/core-api';
import type { UsageStatus } from '@moonshot-ai/protocol';
import type { UsageStatus } from '#/agent/usage/usage';
import type { SessionMeta } from '#/session/sessionMetadata/sessionMetadata';
type AgentType = 'main' | 'sub';

View file

@ -20,7 +20,8 @@ import type { ExperimentalFeatureState } from '#/app/flag/flag';
import type { ResumeSessionResult } from '#/agent/replayBuilder/types';
import type { SessionMeta } from '#/session/sessionMetadata/sessionMetadata';
import type { ContentPart } from '#/app/llmProtocol/message';
import type { SessionWarning, UsageStatus } from '@moonshot-ai/protocol';
import type { SessionWarning } from '#/app/sessionLegacy/sessionWire';
import type { UsageStatus } from '#/agent/usage/usage';
import type { ExportSessionPayload, ExportSessionResult } from '#/app/sessionExport/sessionExport';
import type { PluginCommandDef, PluginInfo, PluginSummary, ReloadSummary } from '#/app/plugin/types';

View file

@ -7,7 +7,6 @@ import { IAgentContextMemoryService } from '#/agent/contextMemory/contextMemory'
import { IAgentContextSizeService } from '#/agent/contextSize/contextSize';
import { IAgentFullCompactionService } from '#/agent/fullCompaction/fullCompaction';
import { IAgentGoalService } from '#/agent/goal/goal';
import type { PluginCommandActivatedEvent } from '@moonshot-ai/protocol';
import { IEventBus } from '#/app/event/eventBus';
import { IEventService } from '#/app/event/event';
import { ErrorCodes, Error2 } from '#/errors';
@ -65,6 +64,15 @@ import {
promptMetadataTextFromSkill,
} from './prompt-metadata';
export interface PluginCommandActivatedEvent {
readonly type: 'plugin_command.activated';
readonly activationId: string;
readonly pluginId: string;
readonly commandName: string;
readonly commandArgs?: string;
readonly trigger: 'user-slash';
}
declare module '#/app/event/eventBus' {
interface DomainEventMap {
'plugin_command.activated': PluginCommandActivatedEvent;

View file

@ -9,8 +9,6 @@
* scope.
*/
import type { ShellOutputEvent, ShellStartedEvent } from '@moonshot-ai/protocol';
import { InstantiationType } from '#/_base/di/extensions';
import { LifecycleScope, registerScopedService } from '#/_base/di/scope';
import { userCancellationReason } from '#/_base/utils/abort';
@ -27,6 +25,28 @@ import {
type RunShellCommandResult,
} from './shellCommand';
/**
* Live stdout/stderr chunk from a user-initiated `!` shell command. Transient
* (never persisted, never replayed) the final output is still recorded once
* via `context.append_message` on completion. `commandId` lets the TUI route
* chunks to the matching live entry and drop stale events from a prior run.
*/
export interface ShellOutputEvent {
readonly type: 'shell.output';
readonly commandId: string;
readonly update: ToolUpdate;
}
/**
* Fired once when a `!` shell command's foreground process task is registered,
* carrying the task id so the client can detach (ctrl+b) it. Transient.
*/
export interface ShellStartedEvent {
readonly type: 'shell.started';
readonly commandId: string;
readonly taskId: string;
}
declare module '#/app/event/eventBus' {
interface DomainEventMap {
'shell.output': ShellOutputEvent;

View file

@ -13,8 +13,6 @@
* before the first turn runs (same rationale as `fullCompaction`).
*/
import type { TurnStepRetryingEvent } from '@moonshot-ai/protocol';
import { Disposable } from '#/_base/di/lifecycle';
import { InstantiationType } from '#/_base/di/extensions';
import { LifecycleScope, registerScopedService } from '#/_base/di/scope';
@ -37,6 +35,20 @@ import { LOOP_CONTROL_SECTION, type LoopControl } from '#/agent/loop/configSecti
import { IAgentStepRetryService } from './stepRetry';
export interface TurnStepRetryingEvent {
readonly type: 'turn.step.retrying';
readonly turnId: number;
readonly step: number;
readonly stepId?: string;
readonly failedAttempt: number;
readonly nextAttempt: number;
readonly maxAttempts: number;
readonly delayMs: number;
readonly errorName: string;
readonly errorMessage: string;
readonly statusCode?: number;
}
declare module '#/app/event/eventBus' {
interface DomainEventMap {
'turn.step.retrying': TurnStepRetryingEvent;

View file

@ -0,0 +1,41 @@
/**
* `toolExecutor` domain the `tool.call.*` / `tool.progress` / `tool.result`
* event payloads published through `IEventBus` as tool calls execute.
*/
import type { ToolUpdate } from '#/tool/toolContract';
import type { ToolInputDisplay } from '#/tool/toolInputDisplay';
export interface ToolCallStartedEvent {
readonly type: 'tool.call.started';
readonly turnId: number;
readonly toolCallId: string;
readonly name: string;
readonly args: unknown;
readonly description?: string;
readonly display?: ToolInputDisplay;
}
export interface ToolProgressEvent {
readonly type: 'tool.progress';
readonly turnId: number;
readonly toolCallId: string;
readonly update: ToolUpdate;
}
export interface ToolResultEvent {
readonly type: 'tool.result';
readonly turnId: number;
readonly toolCallId: string;
readonly output: unknown;
readonly isError?: boolean;
readonly synthetic?: boolean;
}
declare module '#/app/event/eventBus' {
interface DomainEventMap {
'tool.call.started': ToolCallStartedEvent;
'tool.result': ToolResultEvent;
'tool.progress': ToolProgressEvent;
}
}

View file

@ -11,12 +11,7 @@ import { InstantiationType } from '#/_base/di/extensions';
import { toDisposable } from '#/_base/di/lifecycle';
import { LifecycleScope, registerScopedService } from '#/_base/di/scope';
import type { ContentPart } from '#/app/llmProtocol/message';
import type {
ToolCallStartedEvent,
ToolInputDisplay,
ToolProgressEvent,
ToolResultEvent,
} from '@moonshot-ai/protocol';
import type { ToolInputDisplay } from '#/tool/toolInputDisplay';
import {
compileToolArgsValidator,
@ -54,14 +49,10 @@ import {
type UnavailableToolDescriber,
} from './toolExecutor';
import { ToolScheduler } from './toolScheduler';
declare module '#/app/event/eventBus' {
interface DomainEventMap {
'tool.call.started': ToolCallStartedEvent;
'tool.result': ToolResultEvent;
'tool.progress': ToolProgressEvent;
}
}
// Loads the `DomainEventMap` augmentation for the `tool.call.*` / `tool.result`
// events this service publishes (the augmentation lives with the event
// definitions; without an import it would not enter every consumer's program).
import './toolExecutorEvents';
const ABORT_GRACE_MS = 2_000;
const TOOL_OUTPUT_EMPTY = 'Tool output is empty.';

View file

@ -11,7 +11,7 @@ import type { TokenUsage } from '#/app/llmProtocol/usage';
import { createDecorator } from '#/_base/di/instantiation';
import type { Event } from '#/_base/event';
import type { ErrorCode } from '#/_base/errors/codes';
import type { ErrorCode } from '#/errors';
import { Error2 } from '#/_base/errors/errors';
import { UsageErrors } from './errors';

View file

@ -15,7 +15,6 @@
*/
import { z } from 'zod';
import type { AgentPhase } from '@moonshot-ai/protocol';
import { addUsage, type TokenUsage } from '#/app/llmProtocol/usage';
import { defineModel } from '#/wire/model';
@ -34,7 +33,6 @@ declare module '#/app/event/eventBus' {
thinkingEffort?: string;
maxContextTokens?: number;
contextTokens?: number;
phase?: AgentPhase;
};
}
}

View file

@ -17,20 +17,19 @@ import type {
KimiOAuthLogoutResult,
KimiOAuthTokenRef,
} from '@moonshot-ai/kimi-code-oauth';
import type {
OAuthFlowSnapshot,
OAuthFlowStart,
OAuthLoginCancelResponse,
OAuthLogoutResponse,
RefreshOAuthProviderModelsResponse,
} from '@moonshot-ai/protocol';
import { createDecorator, type ServiceIdentifier } from '#/_base/di/instantiation';
import { Error2 } from '#/_base/errors/errors';
import type { OAuthRef } from '#/app/provider/provider';
import { AuthErrors } from './errors';
import type {
OAuthFlowSnapshot,
OAuthFlowStart,
OAuthLoginCancelResponse,
OAuthLogoutResponse,
RefreshOAuthProviderModelsResponse,
} from './oauthWire';
export interface AuthStatus {
readonly loggedIn: boolean;

View file

@ -39,7 +39,7 @@ import type {
OAuthLoginCancelResponse,
OAuthLogoutResponse,
RefreshOAuthProviderModelsResponse,
} from '@moonshot-ai/protocol';
} from './oauthWire';
import { InstantiationType } from '#/_base/di/extensions';
import { Disposable } from '#/_base/di/lifecycle';

View file

@ -0,0 +1,109 @@
/**
* `auth` domain the v1 OAuth wire DTO schemas.
*
* Request/response shapes of the v1 `/oauth/*` endpoints plus the managed
* OAuth provider model-refresh response, defined as zod schemas so the
* transports validate against the same contract the `IOAuthService` returns.
*/
import { z } from 'zod';
import { isoDateTimeSchema } from '#/_base/utils/isoDateTime';
export const oauthFlowStatusEnum = z.enum([
'pending',
'authenticated',
'denied',
'expired',
'cancelled',
]);
export type OAuthFlowStatus = z.infer<typeof oauthFlowStatusEnum>;
/**
* Result of `POST /v1/oauth/login`.
*
* Two shapes, discriminated by `status`:
* - `pending`: a real device-code flow was started; the `verification_*`,
* `user_code`, `expires_*`, and `interval` fields are populated so the
* client can render the device-code step and start polling.
* - `authenticated`: the toolkit already had a usable token and short-
* circuited via its `ensureFresh` fast path, so no device code was
* issued. The client can skip the device-code step and treat the login
* as already complete.
*/
export const oauthFlowStartPendingSchema = z.object({
flow_id: z.string().min(1),
provider: z.string().min(1),
status: z.literal('pending'),
verification_uri: z.string().url(),
verification_uri_complete: z.string().url(),
user_code: z.string().min(1),
expires_in: z.number().int().positive(),
interval: z.number().int().positive(),
expires_at: isoDateTimeSchema,
});
export type OAuthFlowStartPending = z.infer<typeof oauthFlowStartPendingSchema>;
export const oauthFlowStartAuthenticatedSchema = z.object({
flow_id: z.string().min(1),
provider: z.string().min(1),
status: z.literal('authenticated'),
});
export type OAuthFlowStartAuthenticated = z.infer<typeof oauthFlowStartAuthenticatedSchema>;
export const oauthFlowStartSchema = z.discriminatedUnion('status', [
oauthFlowStartPendingSchema,
oauthFlowStartAuthenticatedSchema,
]);
export type OAuthFlowStart = z.infer<typeof oauthFlowStartSchema>;
export const oauthFlowSnapshotSchema = z.object({
flow_id: z.string().min(1),
provider: z.string().min(1),
status: oauthFlowStatusEnum,
verification_uri: z.string().url(),
verification_uri_complete: z.string().url(),
user_code: z.string().min(1),
expires_in: z.number().int().positive(),
expires_at: isoDateTimeSchema,
interval: z.number().int().positive(),
resolved_at: isoDateTimeSchema.optional(),
error_message: z.string().optional(),
});
export type OAuthFlowSnapshot = z.infer<typeof oauthFlowSnapshotSchema>;
export const oauthLoginCancelResponseSchema = z.object({
cancelled: z.boolean(),
status: oauthFlowStatusEnum,
});
export type OAuthLoginCancelResponse = z.infer<typeof oauthLoginCancelResponseSchema>;
export const oauthLogoutResponseSchema = z.object({
logged_out: z.literal(true),
provider: z.string().min(1),
});
export type OAuthLogoutResponse = z.infer<typeof oauthLogoutResponseSchema>;
const providerRefreshChangeSchema = z.object({
provider_id: z.string().min(1),
provider_name: z.string().min(1),
added: z.number().int().min(0),
removed: z.number().int().min(0),
});
const providerRefreshFailureSchema = z.object({
provider: z.string().min(1),
reason: z.string().min(1),
});
// Same response shape as the modelCatalog refresh endpoint; defined
// self-contained here because the two domains sit at different layers and the
// `auth` domain owns the OAuth-provider refresh operation.
export const refreshOAuthProviderModelsResponseSchema = z.object({
changed: z.array(providerRefreshChangeSchema),
unchanged: z.array(z.string().min(1)),
failed: z.array(providerRefreshFailureSchema),
});
export type RefreshOAuthProviderModelsResponse = z.infer<
typeof refreshOAuthProviderModelsResponseSchema
>;

View file

@ -9,10 +9,32 @@
* stateless projector over the global provider / model / credential state.
*/
import type { AuthSummary } from '@moonshot-ai/protocol';
import { z } from 'zod';
import { createDecorator, type ServiceIdentifier } from '#/_base/di/instantiation';
export const managedProviderStatusSchema = z.enum([
'authenticated',
'expired',
'revoked',
'unauthenticated',
]);
export type ManagedProviderStatus = z.infer<typeof managedProviderStatusSchema>;
export const managedProviderSummarySchema = z.object({
name: z.string().min(1),
status: managedProviderStatusSchema,
});
export type ManagedProviderSummary = z.infer<typeof managedProviderSummarySchema>;
export const authSummarySchema = z.object({
ready: z.boolean(),
providers_count: z.number().int().nonnegative(),
default_model: z.string().nullable(),
managed_provider: managedProviderSummarySchema.nullable(),
});
export type AuthSummary = z.infer<typeof authSummarySchema>;
export interface IAuthLegacyService {
readonly _serviceBrand: undefined;

View file

@ -11,7 +11,7 @@
*/
import { KIMI_CODE_PROVIDER_NAME } from '@moonshot-ai/kimi-code-oauth';
import type { AuthSummary } from '@moonshot-ai/protocol';
import type { AuthSummary } from './authLegacy';
import { InstantiationType } from '#/_base/di/extensions';
import { LifecycleScope, registerScopedService } from '#/_base/di/scope';

View file

@ -7,7 +7,7 @@
* them without pulling in the rest of the cron stack.
*/
import type { CronJobOrigin } from '@moonshot-ai/protocol';
import type { CronJobOrigin } from '#/agent/contextMemory/types';
export function formatLocalIsoWithOffset(ms: number): string {
const date = new Date(ms);

View file

@ -8,11 +8,22 @@
import type { Readable } from 'node:stream';
import type { FileMeta } from '@moonshot-ai/protocol';
import { z } from 'zod';
import { createDecorator, type ServiceIdentifier } from '#/_base/di/instantiation';
import { registerErrorDomain, type ErrorDomain } from '#/_base/errors/codes';
import { Error2 } from '#/_base/errors/errors';
import { isoDateTimeSchema } from '#/_base/utils/isoDateTime';
export const fileMetaSchema = z.object({
id: z.string().min(1),
name: z.string().min(1),
media_type: z.string().min(1),
size: z.number().int().nonnegative(),
created_at: isoDateTimeSchema,
expires_at: isoDateTimeSchema.optional(),
});
export type FileMeta = z.infer<typeof fileMetaSchema>;
export const DEFAULT_MAX_UPLOAD_BYTES = 50 * 1024 * 1024;

View file

@ -11,7 +11,7 @@
import { randomUUID } from 'node:crypto';
import { Readable } from 'node:stream';
import type { FileMeta } from '@moonshot-ai/protocol';
import type { FileMeta } from './fileService';
import { InstantiationType } from '#/_base/di/extensions';
import { LifecycleScope, registerScopedService } from '#/_base/di/scope';

View file

@ -9,8 +9,62 @@
* already-resolved absolute `cwd` and repo-relative paths.
*/
import { z } from 'zod';
import { createDecorator, type ServiceIdentifier } from '#/_base/di/instantiation';
import type { FsDiffResponse, FsGitStatusResponse } from '@moonshot-ai/protocol';
export const fsGitStatusSchema = z.enum([
'clean',
'modified',
'added',
'deleted',
'renamed',
'untracked',
'ignored',
'conflicted',
]);
export type FsGitStatus = z.infer<typeof fsGitStatusSchema>;
export const fsPullRequestSchema = z.object({
number: z.number().int().positive(),
state: z.enum(['open', 'merged', 'closed', 'draft']),
url: z.string().url(),
});
export type FsPullRequest = z.infer<typeof fsPullRequestSchema>;
export const fsGitStatusRequestSchema = z.object({
paths: z.array(z.string().min(1)).optional(),
});
export type FsGitStatusRequest = z.infer<typeof fsGitStatusRequestSchema>;
export const fsGitStatusResponseSchema = z.object({
branch: z.string(),
ahead: z.number().int().nonnegative(),
behind: z.number().int().nonnegative(),
entries: z.record(z.string(), fsGitStatusSchema),
// Aggregate working-tree diff against HEAD (`git diff --numstat HEAD`):
// summed added/deleted lines across all changed files. Binary files (numstat
// `-`) contribute 0. Both 0 for a clean tree or a repo with no commits yet.
additions: z.number().int().nonnegative(),
deletions: z.number().int().nonnegative(),
// GitHub pull request for the current branch, looked up via `gh pr view`.
// Null when not a GitHub repo, `gh` is unavailable/unauthenticated, the
// branch has no PR, or the lookup failed/timed out. Never fails the request.
pullRequest: fsPullRequestSchema.nullable(),
});
export type FsGitStatusResponse = z.infer<typeof fsGitStatusResponseSchema>;
export const fsDiffRequestSchema = z.object({
path: z.string().min(1),
});
export type FsDiffRequest = z.infer<typeof fsDiffRequestSchema>;
export const fsDiffResponseSchema = z.object({
path: z.string(),
diff: z.string(),
truncated: z.boolean(),
});
export type FsDiffResponse = z.infer<typeof fsDiffResponseSchema>;
export interface IGitService {
readonly _serviceBrand: undefined;

View file

@ -8,7 +8,7 @@
* `services/fs/fsGit.ts`).
*/
import type { FsGitStatus, FsGitStatusResponse, FsPullRequest } from '@moonshot-ai/protocol';
import type { FsGitStatus, FsGitStatusResponse, FsPullRequest } from './git';
export function parsePorcelain(
stdout: string,

View file

@ -10,7 +10,7 @@
* paths.
*/
import type { FsDiffResponse, FsGitStatusResponse, FsPullRequest } from '@moonshot-ai/protocol';
import type { FsDiffResponse, FsGitStatusResponse, FsPullRequest } from './git';
import { InstantiationType } from '#/_base/di/extensions';
import { LifecycleScope, registerScopedService } from '#/_base/di/scope';

View file

@ -6,17 +6,42 @@
* folder. Distinct from the Session-side `sessionFs`, which is sandboxed and may
* be remote. App-scoped.
*
* The wire shapes (`FsBrowseResponse` / `FsHomeResponse`) are sourced from
* `@moonshot-ai/protocol` so the `/api/v1` and `/api/v2` transports share one
* contract. Domain errors (`HostFolder*Error`) carry the failing path and are
* translated to protocol error codes at the transport boundary.
* The wire shapes (`FsBrowseResponse` / `FsHomeResponse`) are defined here as
* zod schemas so the `/api/v1` and `/api/v2` transports share one contract.
* Domain errors (`HostFolder*Error`) carry the failing path and are
* translated to wire error codes at the transport boundary.
*/
import { z } from 'zod';
import { createDecorator, type ServiceIdentifier } from '#/_base/di/instantiation';
import type { FsBrowseResponse, FsHomeResponse } from '@moonshot-ai/protocol';
export const fsBrowseQuerySchema = z.object({
path: z.string().min(1).optional(),
});
export type FsBrowseQuery = z.infer<typeof fsBrowseQuerySchema>;
export type { FsBrowseResponse, FsHomeResponse };
export const fsBrowseEntrySchema = z.object({
name: z.string().min(1),
path: z.string().min(1),
is_dir: z.literal(true),
is_git_repo: z.boolean(),
branch: z.string().optional(),
});
export type FsBrowseEntry = z.infer<typeof fsBrowseEntrySchema>;
export const fsBrowseResponseSchema = z.object({
path: z.string().min(1),
parent: z.string().min(1).nullable(),
entries: z.array(fsBrowseEntrySchema),
});
export type FsBrowseResponse = z.infer<typeof fsBrowseResponseSchema>;
export const fsHomeResponseSchema = z.object({
home: z.string().min(1),
recent_roots: z.array(z.string().min(1)),
});
export type FsHomeResponse = z.infer<typeof fsHomeResponseSchema>;
export class HostFolderNotAbsoluteError extends Error {
readonly path: string;

View file

@ -12,7 +12,7 @@ import { lstat, readFile, readdir, realpath } from 'node:fs/promises';
import { homedir } from 'node:os';
import { dirname, isAbsolute, join } from 'node:path';
import type { FsBrowseEntry, FsBrowseResponse, FsHomeResponse } from '@moonshot-ai/protocol';
import type { FsBrowseEntry, FsBrowseResponse, FsHomeResponse } from './hostFolderBrowser';
import { InstantiationType } from '#/_base/di/extensions';
import { LifecycleScope, registerScopedService } from '#/_base/di/scope';

View file

@ -19,15 +19,22 @@
* - `message.not_found` 40403
*/
import type {
CursorQuery,
Message,
MessageRole,
PageResponse,
} from '@moonshot-ai/protocol';
import type { Message, MessageRole } from '#/agent/contextMemory/wireMessage';
import { createDecorator, type ServiceIdentifier } from '#/_base/di/instantiation';
/** Cursor pagination query shared by the v1 history/list endpoints. */
export interface CursorQuery {
before_id?: string | undefined;
after_id?: string | undefined;
page_size?: number | undefined;
}
export interface PageResponse<T> {
items: T[];
has_more: boolean;
}
export interface MessageListQuery extends CursorQuery {
readonly role?: MessageRole;
}

View file

@ -18,7 +18,9 @@
* (`packages/agent-core/src/services/message/messageService.ts`).
*/
import type { Message, PageResponse } from '@moonshot-ai/protocol';
import type { Message } from '#/agent/contextMemory/wireMessage';
import type { PageResponse } from './messageLegacy';
import { InstantiationType } from '#/_base/di/extensions';
import { type IAgentScopeHandle, LifecycleScope, registerScopedService } from '#/_base/di/scope';

View file

@ -3,20 +3,15 @@
* model aliases, plus the global default-model selection.
*
* Projects the `provider` / `model` configuration registries into the
* protocol `ProviderCatalogItem` / `ModelCatalogItem` wire shapes that the
* edge (`server-v2` `/api/v1` routes) serves. App-scoped provider and
* `ProviderCatalogItem` / `ModelCatalogItem` wire shapes (defined below as zod
* schemas) that the edge (`server-v2` `/api/v1` routes) serves. App-scoped provider and
* model configuration is global and shared across sessions. This domain is a
* thin facade over `provider`, `model`, `config`, and `auth`; it owns no
* persistence of its own. The OAuth-provider model refresh lives in
* The OAuth-provider model refresh lives in `auth` (`IOAuthService`), not here.
*/
import type {
ModelCatalogItem,
ProviderCatalogItem,
RefreshProviderModelsResponse,
SetDefaultModelResponse,
} from '@moonshot-ai/protocol';
import { z } from 'zod';
import { createDecorator, type ServiceIdentifier } from '#/_base/di/instantiation';
@ -24,6 +19,64 @@ import type { ModelAlias } from '#/app/model/model';
import { effectiveModelConfig } from '#/app/model/modelAuth';
import type { ProviderConfig } from '#/app/provider/provider';
export const modelCatalogItemSchema = z.object({
provider: z.string().min(1),
model: z.string().min(1),
display_name: z.string().min(1).optional(),
max_context_size: z.number().int().min(1),
capabilities: z.array(z.string()).optional(),
support_efforts: z.array(z.string()).optional(),
default_effort: z.string().optional(),
});
export type ModelCatalogItem = z.infer<typeof modelCatalogItemSchema>;
export const providerCatalogStatusSchema = z.enum([
'connected',
'error',
'unconfigured',
]);
export type ProviderCatalogStatus = z.infer<typeof providerCatalogStatusSchema>;
export const providerCatalogItemSchema = z.object({
id: z.string().min(1),
type: z.string().min(1),
base_url: z.string().min(1).optional(),
default_model: z.string().min(1).optional(),
has_api_key: z.boolean(),
status: providerCatalogStatusSchema,
models: z.array(z.string().min(1)).optional(),
});
export type ProviderCatalogItem = z.infer<typeof providerCatalogItemSchema>;
export const providerRefreshChangeSchema = z.object({
provider_id: z.string().min(1),
provider_name: z.string().min(1),
added: z.number().int().min(0),
removed: z.number().int().min(0),
});
export type ProviderRefreshChange = z.infer<typeof providerRefreshChangeSchema>;
export const providerRefreshFailureSchema = z.object({
provider: z.string().min(1),
reason: z.string().min(1),
});
export type ProviderRefreshFailure = z.infer<typeof providerRefreshFailureSchema>;
export const setDefaultModelResponseSchema = z.object({
default_model: z.string().min(1),
model: modelCatalogItemSchema,
});
export type SetDefaultModelResponse = z.infer<typeof setDefaultModelResponseSchema>;
export const refreshProviderModelsResponseSchema = z.object({
changed: z.array(providerRefreshChangeSchema),
unchanged: z.array(z.string().min(1)),
failed: z.array(providerRefreshFailureSchema),
});
export type RefreshProviderModelsResponse = z.infer<
typeof refreshProviderModelsResponseSchema
>;
export type RefreshProviderModelsScope = 'all' | 'oauth';
export interface RefreshProviderModelsOptions {

View file

@ -23,7 +23,7 @@ import type {
ProviderCatalogItem,
RefreshProviderModelsResponse,
SetDefaultModelResponse,
} from '@moonshot-ai/protocol';
} from './modelCatalog';
import { InstantiationType } from '#/_base/di/extensions';
import { LifecycleScope, registerScopedService } from '#/_base/di/scope';

View file

@ -22,14 +22,12 @@
* it is a stateless dispatcher that resolves the target session/agent per call.
*/
import type {
GoalSnapshot,
SessionStatusResponse,
UpdateSessionProfileRequest,
} from '@moonshot-ai/protocol';
import type { GoalSnapshot } from '#/agent/goal/types';
import { createDecorator, type ServiceIdentifier } from '#/_base/di/instantiation';
import type { SessionStatusResponse, UpdateSessionProfileRequest } from './sessionWire';
export interface SessionWireFields {
readonly id: string;
readonly workspaceId: string;

View file

@ -13,11 +13,9 @@
* the real work stays in the native services.
*/
import type {
GoalSnapshot,
SessionStatusResponse,
UpdateSessionProfileRequest,
} from '@moonshot-ai/protocol';
import type { GoalSnapshot } from '#/agent/goal/types';
import type { SessionStatusResponse, UpdateSessionProfileRequest } from './sessionWire';
import { InstantiationType } from '#/_base/di/extensions';
import { type IAgentScopeHandle, LifecycleScope, registerScopedService } from '#/_base/di/scope';

View file

@ -0,0 +1,102 @@
/**
* `sessionLegacy` domain the v1 session wire DTO schemas.
*
* These zod schemas define the request/response shapes of the v1 session
* endpoints this adapter backs (`POST /sessions/{id}/profile`,
* `GET /sessions/{id}/status`, session warnings); the transports validate
* against them and the adapter's contract consumes the inferred types.
* Field-level changes here are wire breaks see the schema-fidelity rule in
* `server-align.md`.
*/
import { z } from 'zod';
import { isoDateTimeSchema } from '#/_base/utils/isoDateTime';
export const sessionStatusSchema = z.enum([
'idle',
'running',
'awaiting_approval',
'awaiting_question',
'aborted',
]);
export type SessionStatus = z.infer<typeof sessionStatusSchema>;
export const sessionWarningSchema = z.object({
code: z.string(),
message: z.string(),
severity: z.enum(['info', 'warning', 'error']),
});
export type SessionWarning = z.infer<typeof sessionWarningSchema>;
export const sessionWarningsResponseSchema = z.object({
warnings: z.array(sessionWarningSchema),
});
export type SessionWarningsResponse = z.infer<typeof sessionWarningsResponseSchema>;
export const promptThinkingSchema = z.string().min(1);
export type PromptThinking = z.infer<typeof promptThinkingSchema>;
export const promptPermissionModeSchema = z.enum(['manual', 'yolo', 'auto']);
export type PromptPermissionMode = z.infer<typeof promptPermissionModeSchema>;
export const sessionMetadataSchema = z
.object({
cwd: z.string().min(1),
})
.catchall(z.unknown());
export type SessionMetadata = z.infer<typeof sessionMetadataSchema>;
export const sessionAgentConfigSchema = z.object({
model: z.string(),
system_prompt: z.string().optional(),
tools: z.array(z.string()).optional(),
mcp_servers: z.array(z.string()).optional(),
thinking: promptThinkingSchema.optional(),
permission_mode: promptPermissionModeSchema.optional(),
plan_mode: z.boolean().optional(),
swarm_mode: z.boolean().optional(),
goal_objective: z.string().optional(),
goal_control: z.enum(['pause', 'resume', 'cancel']).optional(),
});
export type SessionAgentConfig = z.infer<typeof sessionAgentConfigSchema>;
export const sessionAgentConfigPartialSchema = sessionAgentConfigSchema.partial();
export type SessionAgentConfigPartial = z.infer<typeof sessionAgentConfigPartialSchema>;
export const permissionRuleMatcherSchema = z.object({
kind: z.enum(['command_prefix', 'path_glob', 'exact_input', 'always']),
value: z.string().optional(),
});
export type PermissionRuleMatcher = z.infer<typeof permissionRuleMatcherSchema>;
export const permissionRuleSchema = z.object({
id: z.string().min(1),
tool_name: z.string().min(1),
matcher: permissionRuleMatcherSchema.optional(),
decision: z.literal('approved'),
created_at: isoDateTimeSchema,
created_by: z.enum(['user', 'agent']),
});
export type PermissionRule = z.infer<typeof permissionRuleSchema>;
export const updateSessionProfileRequestSchema = z.object({
title: z.string().min(1).optional(),
metadata: sessionMetadataSchema.partial().optional(),
agent_config: sessionAgentConfigPartialSchema.optional(),
permission_rules: z.array(permissionRuleSchema).optional(),
});
export type UpdateSessionProfileRequest = z.infer<typeof updateSessionProfileRequestSchema>;
export const sessionStatusResponseSchema = z.object({
status: sessionStatusSchema,
model: z.string().optional(),
thinking_level: z.string(),
permission: z.string(),
plan_mode: z.boolean(),
swarm_mode: z.boolean(),
context_tokens: z.number().int().nonnegative(),
max_context_tokens: z.number().int().nonnegative(),
context_usage: z.number().min(0).max(1),
});
export type SessionStatusResponse = z.infer<typeof sessionStatusResponseSchema>;

View file

@ -333,7 +333,7 @@ export class SessionLifecycleService extends Disposable implements ISessionLifec
try {
const workspace = await this.workspaceRegistry.get(workspaceId);
if (workspace === undefined) {
throw new Error2('workspace.not_found', `workspace ${workspaceId} does not exist`);
throw new Error2(ErrorCodes.WORKSPACE_NOT_FOUND, `workspace ${workspaceId} does not exist`);
}
const sourceMeta =

View file

@ -0,0 +1,9 @@
import { registerErrorDomain, type ErrorDomain } from '#/_base/errors/codes';
export const WorkspaceErrors = {
codes: {
WORKSPACE_NOT_FOUND: 'workspace.not_found',
},
} as const satisfies ErrorDomain;
registerErrorDomain(WorkspaceErrors);

View file

@ -34,6 +34,7 @@ import { StorageErrors } from '#/persistence/interface/storage';
import { TerminalErrors } from '#/os/interface/terminalErrors';
import { UsageErrors } from '#/agent/usage/errors';
import { WireErrors } from '#/wire/errors';
import { WorkspaceErrors } from '#/app/workspaceRegistry/errors';
export * from '#/_base/errors/codes';
export * from '#/_base/errors/errorMessage';
@ -66,6 +67,7 @@ export { StorageErrors } from '#/persistence/interface/storage';
export { TerminalErrors } from '#/os/interface/terminalErrors';
export { UsageErrors } from '#/agent/usage/errors';
export { WireErrors } from '#/wire/errors';
export { WorkspaceErrors } from '#/app/workspaceRegistry/errors';
export const ErrorCodes = {
...CoreErrors.codes,
@ -95,4 +97,13 @@ export const ErrorCodes = {
...TerminalErrors.codes,
...UsageErrors.codes,
...WireErrors.codes,
...WorkspaceErrors.codes,
} as const;
/**
* The closed union of every error code a Kimi domain may throw derived from
* the `ErrorCodes` aggregate rather than declared centrally, so each domain's
* `errors.ts` is the single source of truth: adding or renaming a code is a
* domain-local change with no central list to keep in sync.
*/
export type ErrorCode = (typeof ErrorCodes)[keyof typeof ErrorCodes];

View file

@ -8,24 +8,74 @@
* (`ISessionTerminalService`) lives in `src/session/terminal` and is the
* surface most business code and the edge consume.
*
* Wire types (`Terminal`, `CreateTerminalRequest`, frame messages) are sourced
* from `@moonshot-ai/protocol`.
* Wire types (`Terminal`, `CreateTerminalRequest`, frame messages) are defined
* here the terminal REST schemas as zod, the attach-frame messages as plain
* types.
*/
import type {
CreateTerminalRequest,
Terminal,
TerminalExitMessage,
TerminalOutputMessage,
} from '@moonshot-ai/protocol';
import { z } from 'zod';
import { createDecorator, type ServiceIdentifier } from '#/_base/di/instantiation';
import type { Event } from '#/_base/event';
import { isoDateTimeSchema } from '#/_base/utils/isoDateTime';
export type { CreateTerminalRequest, Terminal, TerminalExitMessage, TerminalOutputMessage };
const relativeCwdSchema = z
.string()
.min(1)
.refine((value) => !isAbsolutePath(value), 'cwd must be relative to the session workspace');
export const terminalStatusSchema = z.enum(['running', 'exited']);
export type TerminalStatus = z.infer<typeof terminalStatusSchema>;
export const terminalSchema = z.object({
id: z.string().min(1),
session_id: z.string().min(1),
cwd: z.string().min(1),
shell: z.string().min(1),
cols: z.number().int().positive(),
rows: z.number().int().positive(),
status: terminalStatusSchema,
created_at: isoDateTimeSchema,
exited_at: isoDateTimeSchema.optional(),
exit_code: z.number().int().nullable().optional(),
});
export type Terminal = z.infer<typeof terminalSchema>;
export const createTerminalRequestSchema = z.object({
cwd: relativeCwdSchema.optional(),
shell: z.string().min(1).optional(),
cols: z.number().int().positive().optional(),
rows: z.number().int().positive().optional(),
});
export type CreateTerminalRequest = z.infer<typeof createTerminalRequestSchema>;
export interface TerminalOutputMessage {
type: 'terminal_output';
seq: number;
session_id: string;
terminal_id: string;
timestamp: string;
payload: { data: string };
}
export interface TerminalExitMessage {
type: 'terminal_exit';
session_id: string;
terminal_id: string;
timestamp: string;
payload: { exit_code?: number | null | undefined };
}
export type TerminalFrame = TerminalOutputMessage | TerminalExitMessage;
function isAbsolutePath(value: string): boolean {
return (
value.startsWith('/') ||
value.startsWith('\\') ||
/^[A-Za-z]:[\\/]/.test(value)
);
}
export interface TerminalAttachSink {
readonly id: string;
send(frame: TerminalFrame): void;

View file

@ -8,7 +8,7 @@
*/
import { createDecorator, type ServiceIdentifier } from '#/_base/di/instantiation';
import type { ToolInputDisplay } from '@moonshot-ai/protocol';
import type { ToolInputDisplay } from '#/tool/toolInputDisplay';
export interface ApprovalRequest {
readonly id?: string;

View file

@ -17,7 +17,7 @@
* `OP_REGISTRY` at import time.
*/
import type { CronJobOrigin } from '@moonshot-ai/protocol';
import type { CronJobOrigin } from '#/agent/contextMemory/types';
import { z } from 'zod';
import { defineModel } from '#/wire/model';

View file

@ -17,7 +17,7 @@
import { ulid } from 'ulid';
import type { ContentPart } from '#/app/llmProtocol/message';
import type { CronJobOrigin, CronMissedOrigin } from '@moonshot-ai/protocol';
import type { CronJobOrigin, CronMissedOrigin } from '#/agent/contextMemory/types';
import { Disposable, toDisposable } from '#/_base/di/lifecycle';
import { InstantiationType } from '#/_base/di/extensions';

View file

@ -2,36 +2,219 @@
* `sessionFs` domain (L2) wire-shaped filesystem operations.
*
* Defines the `ISessionFsService` that backs the fs REST surface: content search,
* content grep, and git status/diff. It orchestrates the os `IHostFileSystem`
* content grep, and git status/diff, together with the zod DTO schemas the
* transports validate against. It orchestrates the os `IHostFileSystem`
* (file IO, resolved against the workspace root) plus `ISessionProcessRunner`
* (for `rg` / `git` / `gh`) and returns protocol-shaped responses.
* (for `rg` / `git` / `gh`). Git status/diff DTOs live in the `git` domain.
* Session-scoped the scope itself is the session, so no `sessionId` is
* threaded through.
*/
import { z } from 'zod';
import { createDecorator, type ServiceIdentifier } from '#/_base/di/instantiation';
import type {
FsDiffRequest,
FsDiffResponse,
FsGitStatusRequest,
FsGitStatusResponse,
FsGrepRequest,
FsGrepResponse,
FsListManyRequest,
FsListManyResponse,
FsListRequest,
FsListResponse,
FsMkdirRequest,
FsMkdirResponse,
FsReadRequest,
FsReadResponse,
FsSearchRequest,
FsSearchResponse,
FsStatManyRequest,
FsStatManyResponse,
FsStatRequest,
FsStatResponse,
} from '@moonshot-ai/protocol';
import { isoDateTimeSchema } from '#/_base/utils/isoDateTime';
import {
fsGitStatusSchema,
type FsDiffRequest,
type FsDiffResponse,
type FsGitStatusRequest,
type FsGitStatusResponse,
} from '#/app/git/git';
export {
fsDiffRequestSchema,
fsDiffResponseSchema,
fsGitStatusRequestSchema,
fsGitStatusResponseSchema,
} from '#/app/git/git';
export type { FsDiffRequest, FsDiffResponse, FsGitStatusRequest, FsGitStatusResponse };
export const fsKindSchema = z.enum(['file', 'directory', 'symlink']);
export type FsKind = z.infer<typeof fsKindSchema>;
export const fsEntrySchema = z.object({
path: z.string(),
name: z.string(),
kind: fsKindSchema,
size: z.number().int().nonnegative().optional(),
modified_at: isoDateTimeSchema,
etag: z.string().optional(),
mime: z.string().optional(),
language_id: z.string().optional(),
is_binary: z.boolean().optional(),
is_symlink_to: z.string().optional(),
git_status: fsGitStatusSchema.optional(),
child_count: z.number().int().nonnegative().optional(),
});
export type FsEntry = z.infer<typeof fsEntrySchema>;
export const fsSearchHitSchema = z.object({
path: z.string(),
name: z.string(),
kind: fsKindSchema,
score: z.number().min(0).max(1),
match_positions: z.array(z.number().int().nonnegative()),
});
export type FsSearchHit = z.infer<typeof fsSearchHitSchema>;
export const fsGrepMatchSchema = z.object({
line: z.number().int().positive(),
col: z.number().int().positive(),
text: z.string(),
before: z.array(z.string()),
after: z.array(z.string()),
});
export type FsGrepMatch = z.infer<typeof fsGrepMatchSchema>;
export const fsGrepFileHitSchema = z.object({
path: z.string(),
matches: z.array(fsGrepMatchSchema),
});
export type FsGrepFileHit = z.infer<typeof fsGrepFileHitSchema>;
export const fsListSortSchema = z.enum([
'type_first',
'name_asc',
'name_desc',
'mtime_desc',
'size_desc',
]);
export type FsListSort = z.infer<typeof fsListSortSchema>;
export const fsListRequestSchema = z.object({
path: z.string().default('.'),
depth: z.number().int().min(1).max(10).default(1),
limit: z.number().int().min(1).max(1000).default(200),
show_hidden: z.boolean().default(false),
follow_gitignore: z.boolean().default(true),
exclude_globs: z.array(z.string()).optional(),
sort: fsListSortSchema.default('type_first'),
include_git_status: z.boolean().default(false),
});
export type FsListRequest = z.infer<typeof fsListRequestSchema>;
export const fsListResponseSchema = z.object({
items: z.array(fsEntrySchema),
children_by_path: z.record(z.string(), z.array(fsEntrySchema)).optional(),
truncated: z.boolean(),
});
export type FsListResponse = z.infer<typeof fsListResponseSchema>;
export const fsReadEncodingRequestSchema = z.enum(['auto', 'utf-8', 'base64']);
export const fsReadEncodingResponseSchema = z.enum(['utf-8', 'base64']);
export type FsReadEncoding = z.infer<typeof fsReadEncodingResponseSchema>;
export const fsReadRequestSchema = z.object({
path: z.string().min(1),
offset: z.number().int().nonnegative().default(0),
length: z.number().int().min(1).max(10_485_760).default(1_048_576),
encoding: fsReadEncodingRequestSchema.default('auto'),
});
export type FsReadRequest = z.infer<typeof fsReadRequestSchema>;
export const fsReadResponseSchema = z.object({
path: z.string(),
content: z.string(),
encoding: fsReadEncodingResponseSchema,
size: z.number().int().nonnegative(),
truncated: z.boolean(),
etag: z.string(),
mime: z.string(),
language_id: z.string().optional(),
line_count: z.number().int().nonnegative().optional(),
is_binary: z.boolean(),
});
export type FsReadResponse = z.infer<typeof fsReadResponseSchema>;
export const fsMkdirRequestSchema = z.object({
path: z.string().min(1),
recursive: z.boolean().default(false),
});
export type FsMkdirRequest = z.infer<typeof fsMkdirRequestSchema>;
export const fsMkdirResponseSchema = fsEntrySchema;
export type FsMkdirResponse = z.infer<typeof fsMkdirResponseSchema>;
export const fsListManyRequestSchema = z.object({
paths: z.array(z.string().min(1)).min(1).max(100),
depth: z.number().int().min(1).max(10).default(1),
limit: z.number().int().min(1).max(1000).default(200),
show_hidden: z.boolean().default(false),
follow_gitignore: z.boolean().default(true),
exclude_globs: z.array(z.string()).optional(),
sort: fsListSortSchema.default('type_first'),
include_git_status: z.boolean().default(false),
});
export type FsListManyRequest = z.infer<typeof fsListManyRequestSchema>;
export const fsListManyPartialErrorSchema = z.object({
code: z.number().int(),
msg: z.string(),
});
export type FsListManyPartialError = z.infer<typeof fsListManyPartialErrorSchema>;
export const fsListManyResponseSchema = z.object({
results: z.record(z.string(), z.array(fsEntrySchema)),
truncated_paths: z.array(z.string()).optional(),
partial_errors: z.record(z.string(), fsListManyPartialErrorSchema).optional(),
});
export type FsListManyResponse = z.infer<typeof fsListManyResponseSchema>;
export const fsStatRequestSchema = z.object({
path: z.string().min(1),
});
export type FsStatRequest = z.infer<typeof fsStatRequestSchema>;
export const fsStatResponseSchema = fsEntrySchema;
export type FsStatResponse = z.infer<typeof fsStatResponseSchema>;
export const fsStatManyRequestSchema = z.object({
paths: z.array(z.string().min(1)).min(1).max(1000),
});
export type FsStatManyRequest = z.infer<typeof fsStatManyRequestSchema>;
export const fsStatManyResponseSchema = z.object({
entries: z.record(z.string(), fsEntrySchema.nullable()),
});
export type FsStatManyResponse = z.infer<typeof fsStatManyResponseSchema>;
export const fsSearchRequestSchema = z.object({
query: z.string().min(1),
limit: z.number().int().min(1).max(200).default(50),
include_globs: z.array(z.string()).optional(),
exclude_globs: z.array(z.string()).optional(),
follow_gitignore: z.boolean().default(true),
});
export type FsSearchRequest = z.infer<typeof fsSearchRequestSchema>;
export const fsSearchResponseSchema = z.object({
items: z.array(fsSearchHitSchema),
truncated: z.boolean(),
});
export type FsSearchResponse = z.infer<typeof fsSearchResponseSchema>;
export const fsGrepRequestSchema = z.object({
pattern: z.string().min(1),
regex: z.boolean().default(false),
case_sensitive: z.boolean().default(true),
include_globs: z.array(z.string()).optional(),
exclude_globs: z.array(z.string()).optional(),
follow_gitignore: z.boolean().default(true),
max_files: z.number().int().min(1).max(10_000).default(200),
max_matches_per_file: z.number().int().min(1).max(10_000).default(50),
max_total_matches: z.number().int().min(1).max(100_000).default(5000),
context_lines: z.number().int().min(0).max(10).default(2),
});
export type FsGrepRequest = z.infer<typeof fsGrepRequestSchema>;
export const fsGrepResponseSchema = z.object({
files: z.array(fsGrepFileHitSchema),
files_scanned: z.number().int().nonnegative(),
truncated: z.boolean(),
elapsed_ms: z.number().int().nonnegative(),
});
export type FsGrepResponse = z.infer<typeof fsGrepResponseSchema>;
export interface FsPathResolved {
readonly absolute: string;

View file

@ -6,7 +6,7 @@
* be unit-tested directly. Ported from v1 `services/fs/fsSearchService.ts`.
*/
import type { FsGrepRequest } from '@moonshot-ai/protocol';
import type { FsGrepRequest } from './fs';
export function computeFuzzyScore(name: string, queryLower: string): number {
if (queryLower.length === 0) return 0;

View file

@ -16,7 +16,6 @@
import { basename, extname, isAbsolute, join, relative, sep } from 'node:path';
import {
ErrorCode,
type FsDiffRequest,
type FsDiffResponse,
type FsEntry,
@ -41,7 +40,22 @@ import {
type FsStatManyResponse,
type FsStatRequest,
type FsStatResponse,
} from '@moonshot-ai/protocol';
} from './fs';
/**
* The v1 numeric wire codes this edge surface throws inside its
* `{ code, msg }` wire errors (`toWireError`). Mirrors the envelope error
* table owned by the transport (kap-server); kept as local literals because
* they are part of this service's v1 edge contract.
*/
const FsWireErrorCode = {
FS_PATH_NOT_FOUND: 40409,
FS_IS_DIRECTORY: 40906,
FS_IS_BINARY: 40907,
FS_TOO_LARGE: 41302,
FS_TOO_MANY_RESULTS: 41303,
INTERNAL_ERROR: 50001,
} as const;
import ignore, { type Ignore } from 'ignore';
import { InstantiationType } from '#/_base/di/extensions';
@ -919,19 +933,19 @@ function toWireError(err: unknown): { code: number; msg: string } {
if (err instanceof Error2) {
switch (err.code) {
case ErrorCodes.FS_PATH_NOT_FOUND:
return { code: ErrorCode.FS_PATH_NOT_FOUND, msg: err.message };
return { code: FsWireErrorCode.FS_PATH_NOT_FOUND, msg: err.message };
case ErrorCodes.FS_IS_DIRECTORY:
return { code: ErrorCode.FS_IS_DIRECTORY, msg: err.message };
return { code: FsWireErrorCode.FS_IS_DIRECTORY, msg: err.message };
case ErrorCodes.FS_IS_BINARY:
return { code: ErrorCode.FS_IS_BINARY, msg: err.message };
return { code: FsWireErrorCode.FS_IS_BINARY, msg: err.message };
case ErrorCodes.FS_TOO_LARGE:
return { code: ErrorCode.FS_TOO_LARGE, msg: err.message };
return { code: FsWireErrorCode.FS_TOO_LARGE, msg: err.message };
case ErrorCodes.FS_TOO_MANY_RESULTS:
return { code: ErrorCode.FS_TOO_MANY_RESULTS, msg: err.message };
return { code: FsWireErrorCode.FS_TOO_MANY_RESULTS, msg: err.message };
}
}
return {
code: ErrorCode.INTERNAL_ERROR,
code: FsWireErrorCode.INTERNAL_ERROR,
msg: err instanceof Error ? err.message : 'internal error',
};
}

View file

@ -11,7 +11,25 @@
import { createDecorator, type ServiceIdentifier } from '#/_base/di/instantiation';
import type { Event } from '#/_base/event';
import type { FsChangeEvent } from '@moonshot-ai/protocol';
export type FsChangeKind = 'file' | 'directory' | 'symlink';
export type FsChangeAction = 'created' | 'modified' | 'deleted';
export interface FsChangeEntry {
path: string;
change: FsChangeAction;
kind: FsChangeKind;
size_delta?: number | undefined;
etag?: string | undefined;
}
export interface FsChangeEvent {
changes: FsChangeEntry[];
coalesced_window_ms: number;
truncated?: boolean | undefined;
count?: number | undefined;
}
export interface ISessionFsWatchService {
readonly _serviceBrand: undefined;

View file

@ -26,7 +26,7 @@ import {
IHostFsWatchService,
} from '#/os/interface/hostFsWatch';
import { ISessionWorkspaceContext } from '#/session/workspaceContext/workspaceContext';
import type { FsChangeEntry, FsChangeEvent } from '@moonshot-ai/protocol';
import type { FsChangeEntry, FsChangeEvent } from './fsWatch';
import { ISessionFsWatchService } from './fsWatch';

View file

@ -27,18 +27,44 @@ import { IAgentContextSizeService } from '#/agent/contextSize/contextSize';
import { isProviderRateLimitError } from '#/app/llmProtocol/errors';
import { type TokenUsage } from '#/app/llmProtocol/usage';
import { ITelemetryService } from '#/app/telemetry/telemetry';
import type {
SubagentCompletedEvent,
SubagentFailedEvent,
SubagentSpawnedEvent,
SubagentStartedEvent,
} from '@moonshot-ai/protocol';
import { IEventBus } from '#/app/event/eventBus';
import { isAbortError } from '#/_base/utils/abort';
import { IAgentLifecycleService } from '#/session/agentLifecycle/agentLifecycle';
import { type AgentRunHandle, ISessionSubagentService } from './subagent';
export interface SubagentSpawnedEvent {
readonly type: 'subagent.spawned';
readonly subagentId: string;
readonly subagentName: string;
readonly parentToolCallId: string;
readonly parentToolCallUuid?: string;
readonly parentAgentId?: string;
readonly callerAgentId?: string;
readonly description?: string;
readonly swarmIndex?: number;
readonly runInBackground: boolean;
}
export interface SubagentStartedEvent {
readonly type: 'subagent.started';
readonly subagentId: string;
}
export interface SubagentCompletedEvent {
readonly type: 'subagent.completed';
readonly subagentId: string;
readonly resultSummary: string;
readonly usage?: TokenUsage;
readonly contextTokens?: number;
}
export interface SubagentFailedEvent {
readonly type: 'subagent.failed';
readonly subagentId: string;
readonly error: string;
}
declare module '#/app/event/eventBus' {
interface DomainEventMap {
'subagent.spawned': SubagentSpawnedEvent;

View file

@ -23,7 +23,6 @@ import { IAgentProfileService } from '#/agent/profile/profile';
import { IAgentPermissionModeService } from '#/agent/permissionMode/permissionMode';
import { IAgentLoopService } from '#/agent/loop/loop';
import { IAgentUserToolService } from '#/agent/userTool/userTool';
import type { SubagentSuspendedEvent } from '@moonshot-ai/protocol';
import { IEventBus } from '#/app/event/eventBus';
import { IAgentProfileCatalogService } from '#/app/agentProfileCatalog/agentProfileCatalog';
import { applyProfilePromptPrefix } from '#/app/agentProfileCatalog/promptPrefix';
@ -56,6 +55,12 @@ import {
type AgentRunAttemptHandle,
} from './agentRunBatch';
export interface SubagentSuspendedEvent {
readonly type: 'subagent.suspended';
readonly subagentId: string;
readonly reason: string;
}
declare module '#/app/event/eventBus' {
interface DomainEventMap {
'subagent.suspended': SubagentSuspendedEvent;

View file

@ -17,7 +17,7 @@
import type { ContentPart, ToolCall } from '#/app/llmProtocol/message';
import type { Tool } from '#/app/llmProtocol/tool';
import type { ToolInputDisplay } from '@moonshot-ai/protocol';
import type { ToolInputDisplay } from '#/tool/toolInputDisplay';
export type ExecutableToolOutput = string | ContentPart[];

View file

@ -0,0 +1,87 @@
/**
* `ToolInputDisplay` structured UI hint describing a tool call's input, so
* approval panels and tool renderers can present it without re-deriving it
* from raw arguments. Carried by `RunnableToolExecution.display`,
* `ToolResult.display`, and the `tool.call.started` event.
*/
export type ToolInputDisplay =
| {
kind: 'command';
command: string;
cwd?: string | undefined;
description?: string | undefined;
language?: 'bash' | undefined;
}
| {
kind: 'file_io';
operation: 'read' | 'write' | 'edit' | 'glob' | 'grep';
path: string;
detail?: string | undefined;
content?: string | undefined;
before?: string | undefined;
after?: string | undefined;
}
| {
kind: 'diff';
path: string;
before: string;
after: string;
hunks?: number | undefined;
}
| {
kind: 'search';
query: string;
scope?: string | undefined;
}
| {
kind: 'url_fetch';
url: string;
method?: string | undefined;
}
| {
kind: 'agent_call';
agent_name: string;
prompt: string;
background?: boolean | undefined;
}
| {
kind: 'skill_call';
skill_name: string;
args?: string | undefined;
}
| {
kind: 'todo_list';
items: { title: string; status: string }[];
}
| {
kind: 'task';
task_id: string;
status: string;
description: string;
task_kind?: string | undefined;
}
| {
kind: 'task_stop';
task_id: string;
task_description: string;
}
| {
kind: 'plan_review';
plan: string;
path?: string | undefined;
options?: readonly { label: string; description: string }[] | undefined;
}
| {
kind: 'goal_start';
objective: string;
completionCriterion?: string | undefined;
// Current permission mode at approval time. The client uses it to pick the
// start menu (manual vs yolo); `auto` never reaches this display because it
// auto-approves the goal without a prompt.
mode: 'manual' | 'yolo';
}
| {
kind: 'generic';
summary: string;
detail?: unknown;
};

View file

@ -6,7 +6,7 @@
*/
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import type { TurnEndedEvent } from '@moonshot-ai/protocol';
import type { TurnEndedEvent } from '#/agent/loop/turnEvents';
import type { IDisposable } from '#/_base/di/lifecycle';
import { IAgentContextMemoryService } from '#/agent/contextMemory/contextMemory';

View file

@ -1,4 +1,4 @@
import { MCP_OAUTH_AUTHORIZATION_URL_TOOL_UPDATE } from '@moonshot-ai/protocol';
import { MCP_OAUTH_AUTHORIZATION_URL_TOOL_UPDATE } from '#/agent/mcp/tools/auth';
import { describe, expect, it } from 'vitest';
import { AlreadyAuthorizedError, type BeginAuthorizationResult, type McpOAuthService } from '#/agent/mcp/oauth/service';

View file

@ -34,7 +34,7 @@ import { ISessionWorkspaceContext } from '#/session/workspaceContext/workspaceCo
import type { ToolCall } from '#/app/llmProtocol/message';
import { IEventBus } from '#/app/event/eventBus';
import { EventBusService } from '#/app/event/eventBusService';
import type { ToolInputDisplay } from '@moonshot-ai/protocol';
import type { ToolInputDisplay } from '#/tool/toolInputDisplay';
import { stubApprovalService } from '../../session/approval/stubs';
import { stubPermissionModeService } from '../permissionMode/stubs';

View file

@ -3,7 +3,7 @@ import { tmpdir } from 'node:os';
import { join } from 'node:path';
import type { ToolCall } from '#/app/llmProtocol/message';
import type { ToolInputDisplay } from '@moonshot-ai/protocol';
import type { ToolInputDisplay } from '#/tool/toolInputDisplay';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { DisposableStore } from '#/_base/di/lifecycle';

View file

@ -1,5 +1,6 @@
import type { ToolCall } from '#/app/llmProtocol/message';
import type { ApprovalResponse, ToolInputDisplay } from '@moonshot-ai/protocol';
import type { ToolInputDisplay } from '#/tool/toolInputDisplay';
import type { ApprovalResponse } from '#/session/approval/approval';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { DisposableStore } from '#/_base/di/lifecycle';

View file

@ -1,5 +1,5 @@
import type { ToolCall } from '#/app/llmProtocol/message';
import type { ToolInputDisplay } from '@moonshot-ai/protocol';
import type { ToolInputDisplay } from '#/tool/toolInputDisplay';
import { describe, expect, it } from 'vitest';
import type { ResolvedToolExecutionHookContext } from '#/agent/toolExecutor/toolHooks';

View file

@ -1,5 +1,6 @@
import type { ToolCall } from '#/app/llmProtocol/message';
import type { AgentEvent, ToolInputDisplay } from '@moonshot-ai/protocol';
import type { DomainEvent } from '#/app/event/eventBus';
import type { ToolInputDisplay } from '#/tool/toolInputDisplay';
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import { SyncDescriptor } from '#/_base/di/descriptors';
@ -33,7 +34,7 @@ let ix: TestInstantiationService;
let executor: IAgentToolExecutorService;
let registry: IAgentToolRegistryService;
let events: ToolExecutorEvent[];
let protocolEvents: AgentEvent[];
let protocolEvents: DomainEvent[];
let telemetryEvents: TelemetryRecord[];
let truncateForModel: IAgentToolResultTruncationService['truncateForModel'];
@ -56,7 +57,7 @@ beforeEach(() => {
reg.defineInstance(IEventBus, {
publish: (event: { type: string }) => {
if (event.type.startsWith('tool.')) {
protocolEvents.push(event as unknown as AgentEvent);
protocolEvents.push(event as unknown as DomainEvent);
}
},
subscribe: (..._args: unknown[]) => ({ dispose: () => {} }),
@ -178,7 +179,7 @@ describe('AgentToolExecutorService', () => {
note: '<system>Image compressed.</system>',
});
const protocolResult = protocolEvents.find(
(event): event is Extract<AgentEvent, { type: 'tool.result' }> =>
(event): event is Extract<DomainEvent, { type: 'tool.result' }> =>
event.type === 'tool.result',
);
expect(protocolResult).toMatchObject({
@ -334,7 +335,7 @@ describe('AgentToolExecutorService', () => {
}),
]);
const toolCallEvent = protocolEvents.find(
(event): event is Extract<AgentEvent, { type: 'tool.call.started' }> =>
(event): event is Extract<DomainEvent, { type: 'tool.call.started' }> =>
event.type === 'tool.call.started',
);
expect(toolCallEvent?.args).toEqual({ x: 1 });
@ -753,7 +754,7 @@ function eventTypes(): ToolExecutorEvent['type'][] {
return events.map((event) => event.type);
}
function protocolEventTypes(): AgentEvent['type'][] {
function protocolEventTypes(): DomainEvent['type'][] {
return protocolEvents.map((event) => event.type);
}
@ -761,13 +762,13 @@ function pairedToolCallIds(): { readonly calls: string[]; readonly results: stri
return {
calls: protocolEvents
.filter(
(event): event is Extract<AgentEvent, { type: 'tool.call.started' }> =>
(event): event is Extract<DomainEvent, { type: 'tool.call.started' }> =>
event.type === 'tool.call.started',
)
.map((event) => event.toolCallId),
results: protocolEvents
.filter(
(event): event is Extract<AgentEvent, { type: 'tool.result' }> =>
(event): event is Extract<DomainEvent, { type: 'tool.result' }> =>
event.type === 'tool.result',
)
.map((event) => event.toolCallId),

View file

@ -1,4 +1,4 @@
import type { ToolInputDisplay } from '@moonshot-ai/protocol';
import type { ToolInputDisplay } from '#/tool/toolInputDisplay';
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import { SyncDescriptor } from '#/_base/di/descriptors';

View file

@ -1,6 +1,6 @@
import { describe, expect, it } from 'vitest';
import type { CronJobOrigin } from '@moonshot-ai/protocol';
import type { CronJobOrigin } from '#/agent/contextMemory/types';
import type {
ExecutableTool,

View file

@ -17,7 +17,7 @@ import {
IHostFsWatchService,
} from '#/os/interface/hostFsWatch';
import { ISessionWorkspaceContext } from '#/session/workspaceContext/workspaceContext';
import type { FsChangeEvent } from '@moonshot-ai/protocol';
import type { FsChangeEvent } from '#/session/sessionFs/fsWatch';
import { ISessionFsWatchService } from '#/session/sessionFs/fsWatch';
import { SessionFsWatchService } from '#/session/sessionFs/fsWatchService';

View file

@ -476,11 +476,7 @@ export class SessionEventBroadcaster {
const disposables: IDisposable[] = [
eventBus.subscribe((event) => {
let projected = event;
if (
handle.id === MAIN_AGENT_ID &&
event.type === 'agent.status.updated' &&
event.phase === undefined
) {
if (handle.id === MAIN_AGENT_ID && event.type === 'agent.status.updated') {
const snapshot = readLegacyStatus(handle);
if (snapshot !== undefined) {
lastLegacyStatus = JSON.stringify(snapshot);

3
pnpm-lock.yaml generated
View file

@ -508,9 +508,6 @@ importers:
'@moonshot-ai/minidb':
specifier: workspace:^
version: link:../minidb
'@moonshot-ai/protocol':
specifier: workspace:^
version: link:../protocol
'@mozilla/readability':
specifier: ^0.6.0
version: 0.6.0