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
5 changes: 5 additions & 0 deletions .changeset/deepseek-image-rejection-recovery.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@moonshot-ai/kimi-code": patch
---

Fix image upload rejections freezing the session: when a provider rejects an image, the image is no longer re-sent, so the conversation continues without restarting.
5 changes: 5 additions & 0 deletions .changeset/image-file-api-upload.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@moonshot-ai/kimi-code": minor
---

Send images to vision models through the provider's Files API. Enable it by adding `image_file_api` to a model's `capabilities` in config.toml.
2 changes: 2 additions & 0 deletions apps/kimi-code/src/tui/utils/message-replay.ts
Original file line number Diff line number Diff line change
Expand Up @@ -376,6 +376,8 @@ function contentPartToText(part: ContentPart): string {
return mediaUrlPartToText('video', part.videoUrl.url);
case 'audio_url':
return mediaUrlPartToText('audio', part.audioUrl.url);
case 'file':
return `[file:${part.fileId}]`;
}
}

Expand Down
1 change: 1 addition & 0 deletions apps/vscode/src/utils/session-context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,7 @@ function formatPartMarkdown(part: ContentPart): string {
case "image_url": return "[image]";
case "audio_url": return "[audio]";
case "video_url": return "[video]";
case "file": return "[file]";
}
}

Expand Down
12 changes: 9 additions & 3 deletions packages/agent-core-v2/docs/state-manifest.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1069,6 +1069,9 @@ export interface AgentStateSnapshot {
url: string;
id?: string;
};
} | /* FilePart — packages/agent-core-v2/src/kosong/contract/message.ts */ {
type: 'file';
fileId: string;
})[];
readonly toolCalls: /* ToolCall — packages/agent-core-v2/src/kosong/contract/message.ts */ {
type: 'function';
Expand Down Expand Up @@ -1192,9 +1195,8 @@ export interface AgentStateSnapshot {
'llmRequester.emittedThinkingEffortWarnings': Set<string>;
'llmRequester.lastConfigLogSignature': string | undefined;
'llmRequester.mediaDegradedTurns': Set<number>;
'llmRequester.mediaStrippedTurns': Map<number, /* MediaStripSnapshot — packages/agent-core-v2/src/agent/contextProjector/contextProjector.ts */ {
readonly "__@mediaStripSnapshotBrand": undefined;
}>;
// replayable · durable — folds: MediaStripped
'llmRequester.mediaStrippedTurns': readonly string[];
'llmRequester.turnConfigs': Map<number, /* TurnRequestConfig — packages/agent-core-v2/src/agent/llmRequester/llmRequesterService.ts */ {
readonly resolved: /* ProfileModelContext — packages/agent-core-v2/src/agent/profile/profile.ts */ {
readonly modelAlias: string;
Expand All @@ -1207,6 +1209,7 @@ export interface AgentStateSnapshot {
readonly max_context_tokens: number;
readonly max_input_tokens?: number;
readonly dynamically_loaded_tools?: boolean;
readonly image_file_api?: boolean;
};
readonly maxOutputSize: number | undefined;
readonly alwaysThinking: boolean | undefined;
Expand Down Expand Up @@ -1280,6 +1283,9 @@ export interface AgentStateSnapshot {
url: string;
id?: string;
};
} | /* FilePart — packages/agent-core-v2/src/kosong/contract/message.ts */ {
type: 'file';
fileId: string;
}>;
// src/agent/media/mediaToolsRegistrar.ts
'media.registeredKey': string | undefined;
Expand Down
16 changes: 14 additions & 2 deletions packages/agent-core-v2/docs/wire-manifest.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
// cross-reducers), blobs (the folding states whose blob codec offloads inline
// media to blob storage), owner (the source file declaring the class).

// Index (57 record types)
// Index (58 record types)
// config.update profile src/agent/profile/profileOps.ts
// context.append_loop_event contextMemory, turn src/agent/contextMemory/contextEvents.ts
// context.append_message contextMemory, plan, task.notificationDelivery src/agent/contextMemory/contextEvents.ts
Expand All @@ -44,6 +44,7 @@
// interaction.request (none) src/features/interaction/interactionOps.ts
// interaction.resolved (none) src/features/interaction/interactionOps.ts
// interruptionReminder.recorded interruptionReminder src/agent/interruptionReminder/interruptionReminderOps.ts
// llm.media_stripped llmRequester.mediaStrippedTurns src/agent/llmRequester/llmRequestOps.ts
// llm.request llm.requestTrace src/agent/llmRequester/llmRequestOps.ts
// llm.tools_snapshot llm.requestTrace src/agent/llmRequester/llmRequestOps.ts
// mcp.tools_discovered mcp.discovery src/agent/mcp/mcpDiscoveryOps.ts
Expand Down Expand Up @@ -128,7 +129,7 @@ interface ContextAppendMessagePayload {
message: {
role: 'system' | 'user' | 'assistant' | 'tool';
name?: string;
content: ('text' | 'think' | 'image_url' | 'audio_url' | 'video_url')[];
content: ('text' | 'think' | 'image_url' | 'audio_url' | 'video_url' | 'file')[];
toolCalls: {
type: 'function';
id: string;
Expand Down Expand Up @@ -339,6 +340,16 @@ interface InterruptionReminderRecordedPayload {
turnId: number;
}

/**
* states: llmRequester.mediaStrippedTurns
* owner: src/agent/llmRequester/llmRequestOps.ts
*/
interface LlmMediaStrippedPayload {
_name: 'llm.media_stripped';
agentId: string;
keys: string[];
}

/**
* states: llm.requestTrace
* owner: src/agent/llmRequester/llmRequestOps.ts
Expand Down Expand Up @@ -867,6 +878,7 @@ interface WirePayloadMap {
"interaction.request": InteractionRequestPayload;
"interaction.resolved": InteractionResolvedPayload;
"interruptionReminder.recorded": InterruptionReminderRecordedPayload;
"llm.media_stripped": LlmMediaStrippedPayload;
"llm.request": LlmRequestPayload;
"llm.tools_snapshot": LlmToolsSnapshotPayload;
"mcp.tools_discovered": McpToolsDiscoveredPayload;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export function isVacuousContentPart(part: ContentPart): boolean {
case 'image_url':
case 'audio_url':
case 'video_url':
case 'file':
return false;
default: {
const exhaustive: never = part;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,6 @@ interface MediaContainer {
readonly id?: string;
}

interface MediaStripSnapshotData {
readonly keys: ReadonlySet<string>;
}

type MediaContainerKeyCache = Partial<Record<DegradableMediaPart['type'], string>>;

const MEDIA_CONTAINER_KEY_CACHE = new WeakMap<MediaContainer, MediaContainerKeyCache>();
Expand Down Expand Up @@ -77,8 +73,13 @@ function mediaStripKey(part: DegradableMediaPart): string {
return key;
}

function mediaStripSnapshotKeys(snapshot: MediaStripSnapshot): ReadonlySet<string> {
return (snapshot as unknown as MediaStripSnapshotData).keys;
export function mediaStripSnapshotKeys(snapshot: MediaStripSnapshot): ReadonlySet<string> {
const keys = (snapshot as unknown as { keys?: ReadonlySet<string> | undefined }).keys;
return keys ?? new Set();
}

export function mediaStripSnapshotFromKeys(keys: Iterable<string>): MediaStripSnapshot {
return Object.freeze({ keys: new Set(keys) }) as unknown as MediaStripSnapshot;
}

export function captureMediaStripSnapshot(
Expand All @@ -93,6 +94,15 @@ export function captureMediaStripSnapshot(
return Object.freeze({ keys }) as unknown as MediaStripSnapshot;
}

export function mergeMediaStripSnapshots(
a: MediaStripSnapshot,
b: MediaStripSnapshot,
): MediaStripSnapshot {
const keys = new Set(mediaStripSnapshotKeys(a));
for (const key of mediaStripSnapshotKeys(b)) keys.add(key);
return Object.freeze({ keys }) as unknown as MediaStripSnapshot;
}

export function stripMediaPartsBySnapshot(
messages: readonly Message[],
snapshot: MediaStripSnapshot,
Expand Down
15 changes: 15 additions & 0 deletions packages/agent-core-v2/src/agent/llmRequester/llmRequestOps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,21 @@ export interface LlmRequest {
readonly droppedCount?: number;
}

const mediaStrippedSchema = z.object({
agentId: z.string(),
keys: z.array(z.string()).readonly(),
});

export class MediaStripped extends AgentEvent2<z.infer<typeof mediaStrippedSchema>> {
static override readonly type = 'llm.media_stripped';
static override readonly durable = true;
static override readonly schema = mediaStrippedSchema;
}
export interface MediaStripped {
readonly agentId: string;
readonly keys: readonly string[];
}

export const llmRequestTraceKey = defineState(
'llm.requestTrace',
(): LlmRequestTraceState => ({ seenToolsHashes: [] }),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { createHash } from 'node:crypto';
import { z } from 'zod';
import { LifecycleScope } from '#/app/scopes';
import { ScopeActivation, registerScopedService } from '#/_base/di/scope';
import { defineState } from '#/state/state';
Expand All @@ -8,6 +9,10 @@ import {
type MediaStripSnapshot,
type ProjectionPolicy,
} from '#/agent/contextProjector/contextProjector';
import {
mediaStripSnapshotFromKeys,
mediaStripSnapshotKeys,
} from '#/agent/contextProjector/mediaProjection';
import { ISessionTokenCountingService } from '#/session/tokenCounting/sessionTokenCounting';
import { IAgentProfileService, type ProfileModelContext } from '#/agent/profile/profile';
import { IAgentStateService } from '#/agent/state/agentState';
Expand Down Expand Up @@ -69,6 +74,7 @@ import {
LlmRequest,
llmRequestTraceKey,
LlmToolsSnapshot,
MediaStripped,
type LlmRequestPayload,
type LlmRequestToolSchema,
} from './llmRequestOps';
Expand Down Expand Up @@ -136,10 +142,18 @@ export const llmRequesterMediaDegradedTurnsKey = defineState<Set<number>>(
'llmRequester.mediaDegradedTurns',
() => new Set(),
);
export const llmRequesterMediaStrippedTurnsKey = defineState<Map<number, MediaStripSnapshot>>(
export const llmRequesterMediaStrippedTurnsKey = defineState(
'llmRequester.mediaStrippedTurns',
() => new Map(),
);
(): readonly string[] => [],
)
.replayable({ schema: z.array(z.string()).readonly() })
.on(MediaStripped, (s, e) => {
for (const key of e.keys) {
if (!s.includes(key)) {
s.push(key);
}
}
});
export const llmRequesterEmittedThinkingEffortWarningsKey = defineState<Set<string>>(
'llmRequester.emittedThinkingEffortWarnings',
() => new Set(),
Expand Down Expand Up @@ -193,10 +207,6 @@ export class AgentLLMRequesterService implements IAgentLLMRequesterService {
return this.states.get(llmRequesterMediaDegradedTurnsKey);
}

private get mediaStrippedTurns(): Map<number, MediaStripSnapshot> {
return this.states.get(llmRequesterMediaStrippedTurnsKey);
}

private get emittedThinkingEffortWarnings(): Set<string> {
return this.states.get(llmRequesterEmittedThinkingEffortWarningsKey);
}
Expand Down Expand Up @@ -594,21 +604,22 @@ export class AgentLLMRequesterService implements IAgentLLMRequesterService {
}

private mediaStripSnapshotForTurn(
source: AgentLLMRequestSource | undefined,
_source: AgentLLMRequestSource | undefined,
): MediaStripSnapshot | undefined {
if (source?.type !== 'turn') return undefined;
return this.mediaStrippedTurns.get(source.turnId);
const keys = this.states.get(llmRequesterMediaStrippedTurnsKey);
return keys.length === 0 ? undefined : mediaStripSnapshotFromKeys(keys);
}

private markMediaStrippedRecoveryTurn(
snapshot: MediaStripSnapshot,
source: AgentLLMRequestSource | undefined,
_source: AgentLLMRequestSource | undefined,
): void {
if (source?.type !== 'turn') return;
for (const id of this.mediaStrippedTurns.keys()) {
if (id < source.turnId) this.mediaStrippedTurns.delete(id);
}
this.mediaStrippedTurns.set(source.turnId, snapshot);
void this.dispatcher.dispatch(
new MediaStripped({
agentId: this.scopeContext.agentId,
keys: [...mediaStripSnapshotKeys(snapshot)],
}),
);
}

private markRecoveryTurn(set: Set<number>, source: AgentLLMRequestSource | undefined): void {
Expand Down
1 change: 1 addition & 0 deletions packages/agent-core-v2/src/agent/loop/loopService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1151,6 +1151,7 @@ export class AgentLoopService extends Disposable implements IAgentLoopService {
case 'image_url':
case 'audio_url':
case 'video_url':
case 'file':
return;
case 'function': {
onResponseEvent();
Expand Down
2 changes: 1 addition & 1 deletion packages/agent-core-v2/src/agent/mcp/output.ts
Original file line number Diff line number Diff line change
Expand Up @@ -217,7 +217,7 @@ function applyBinaryPartCap(parts: readonly ContentPart[]): {
const notices: string[] = [];

for (const part of parts) {
if (part.type === 'text' || part.type === 'think') {
if (part.type === 'text' || part.type === 'think' || part.type === 'file') {
out.push(part);
continue;
}
Expand Down
Loading
Loading