Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion packages/agent-core-v2/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
5 changes: 5 additions & 0 deletions packages/agent-core-v2/scripts/check-domain-layers.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
33 changes: 20 additions & 13 deletions packages/agent-core-v2/src/_base/errors/codes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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);
Expand All @@ -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 {
Expand All @@ -60,6 +66,7 @@ export const CoreErrors = {
codes: {
INTERNAL: 'internal',
NOT_IMPLEMENTED: 'not_implemented',
VALIDATION_FAILED: 'validation.failed',
},
info: {
internal: {
Expand Down
2 changes: 1 addition & 1 deletion packages/agent-core-v2/src/_base/errors/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
2 changes: 1 addition & 1 deletion packages/agent-core-v2/src/_base/errors/serialize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
26 changes: 26 additions & 0 deletions packages/agent-core-v2/src/_base/utils/isoDateTime.ts
Original file line number Diff line number Diff line change
@@ -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();
});
3 changes: 2 additions & 1 deletion packages/agent-core-v2/src/activity/activity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down
23 changes: 22 additions & 1 deletion packages/agent-core-v2/src/agent/contextMemory/types.ts
Original file line number Diff line number Diff line change
@@ -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';

Expand Down Expand Up @@ -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';
}
Expand All @@ -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;
Expand Down
100 changes: 100 additions & 0 deletions packages/agent-core-v2/src/agent/contextMemory/wireMessage.ts
Original file line number Diff line number Diff line change
@@ -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>;
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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;
Expand Down
28 changes: 21 additions & 7 deletions packages/agent-core-v2/src/agent/fullCompaction/compactionOps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down
25 changes: 22 additions & 3 deletions packages/agent-core-v2/src/agent/fullCompaction/types.ts
Original file line number Diff line number Diff line change
@@ -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;
}

Expand Down
Loading
Loading