-
Notifications
You must be signed in to change notification settings - Fork 910
feat: offload large base64 media payloads to external blob files #117
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
3cd02df
8eec7f0
58156d3
547b9dc
cfa00dc
b4d1dfc
e232cec
113e250
7639023
1edbeea
b00ec47
de81345
299d7d1
3eac4c4
0b5ac87
6281872
d776679
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| --- | ||
| "@moonshot-ai/agent-core": patch | ||
| "@moonshot-ai/kimi-code": patch | ||
| --- | ||
|
|
||
| Offload large base64 media payloads from wire.jsonl into external blob files to reduce wire size and memory pressure during session replay. Includes an in-memory read-through cache on `BlobStore` so repeated rehydration avoids redundant disk reads. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,97 @@ | ||
| import type { ContentPart, WireEntry } from './agent-record-types'; | ||
|
|
||
| const BLOBREF_PROTOCOL = 'blobref:'; | ||
|
|
||
| function isBlobRef(url: string): boolean { | ||
| return url.startsWith(BLOBREF_PROTOCOL); | ||
| } | ||
|
|
||
| /** Convert a `blobref:<mime>;<hash>` URL into a vis-server blob route. | ||
| * Non-blobref URLs are returned unchanged. */ | ||
| export function resolveBlobRefUrl( | ||
| url: string, | ||
| sessionId: string, | ||
| agentId: string, | ||
| baseUrl: string = '', | ||
| ): string { | ||
| if (!isBlobRef(url)) return url; | ||
| const rest = url.slice(BLOBREF_PROTOCOL.length); | ||
| const semiIdx = rest.indexOf(';'); | ||
| if (semiIdx === -1) return url; | ||
| const mimeType = rest.slice(0, semiIdx); | ||
| const hash = rest.slice(semiIdx + 1); | ||
| if (hash.length === 0) return url; | ||
| const path = `/api/sessions/${encodeURIComponent(sessionId)}/blobs/${encodeURIComponent(hash)}?agent=${encodeURIComponent(agentId)}&mime=${encodeURIComponent(mimeType)}`; | ||
| return baseUrl ? `${baseUrl}${path}` : path; | ||
| } | ||
|
|
||
| /** Walk every record in a wire and replace blobref URLs with vis-server | ||
| * blob routes so the UI can render them. Only mutates `entry.data`; | ||
| * `entry.raw` is left untouched. */ | ||
| export function rehydrateWireEntries( | ||
| entries: readonly WireEntry[], | ||
| sessionId: string, | ||
| agentId: string, | ||
| baseUrl: string = '', | ||
| ): void { | ||
| for (const entry of entries) { | ||
| rehydrateRecord(entry.data as Record<string, unknown>, sessionId, agentId, baseUrl); | ||
| } | ||
| } | ||
|
|
||
| function rehydrateRecord( | ||
| record: Record<string, unknown>, | ||
| sessionId: string, | ||
| agentId: string, | ||
| baseUrl: string, | ||
| ): void { | ||
| const type = record['type']; | ||
| if (type === 'turn.prompt' || type === 'turn.steer') { | ||
| rehydrateParts(record['input'] as unknown as ContentPart[], sessionId, agentId, baseUrl); | ||
| return; | ||
| } | ||
| if (type === 'context.append_message') { | ||
| const message = record['message'] as { content: ContentPart[] }; | ||
| rehydrateParts(message.content, sessionId, agentId, baseUrl); | ||
| return; | ||
| } | ||
| if (type === 'context.append_loop_event') { | ||
| const event = record['event'] as Record<string, unknown>; | ||
| if (event['type'] === 'tool.result') { | ||
| const result = event['result'] as Record<string, unknown>; | ||
| if (typeof result['output'] !== 'string') { | ||
| rehydrateParts(result['output'] as ContentPart[], sessionId, agentId, baseUrl); | ||
| } | ||
| } else if (event['type'] === 'content.part') { | ||
| rehydrateParts([event['part'] as ContentPart], sessionId, agentId, baseUrl); | ||
| } | ||
| return; | ||
| } | ||
| } | ||
|
|
||
| function rehydrateParts( | ||
| parts: ContentPart[], | ||
| sessionId: string, | ||
| agentId: string, | ||
| baseUrl: string, | ||
| ): void { | ||
| for (const part of parts) { | ||
| switch (part.type) { | ||
| case 'image_url': | ||
| part.imageUrl.url = resolveBlobRefUrl(part.imageUrl.url, sessionId, agentId, baseUrl); | ||
| break; | ||
| case 'audio_url': | ||
| part.audioUrl.url = resolveBlobRefUrl(part.audioUrl.url, sessionId, agentId, baseUrl); | ||
| break; | ||
| case 'video_url': | ||
| part.videoUrl.url = resolveBlobRefUrl(part.videoUrl.url, sessionId, agentId, baseUrl); | ||
| break; | ||
| default: | ||
| break; | ||
| } | ||
| } | ||
| } | ||
|
|
||
| export function isSafeBlobHash(hash: string): boolean { | ||
| return /^[a-f0-9]{64}$/.test(hash); | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,45 @@ | ||
| import { Hono } from 'hono'; | ||
| import { join } from 'node:path'; | ||
| import { readFile } from 'node:fs/promises'; | ||
|
|
||
| import { KIMI_CODE_HOME } from '../config'; | ||
| import { isSafeAgentId, readSessionDetail } from '../lib/session-store'; | ||
| import { isSafeBlobHash } from '../lib/blob-resolver'; | ||
|
|
||
| export function blobsRoute(home: string = KIMI_CODE_HOME): Hono { | ||
| const r = new Hono(); | ||
| r.get('/:id/blobs/:hash', async (c) => { | ||
| const id = c.req.param('id'); | ||
| const agentId = c.req.query('agent') ?? 'main'; | ||
| const hash = c.req.param('hash'); | ||
| if (!isSafeAgentId(agentId)) { | ||
| return c.json({ error: 'invalid agent id', code: 'BAD_REQUEST' }, 400); | ||
| } | ||
| if (!isSafeBlobHash(hash)) { | ||
| return c.json({ error: 'invalid blob hash', code: 'BAD_REQUEST' }, 400); | ||
| } | ||
| const detail = await readSessionDetail(home, id); | ||
| if (!detail) { | ||
| return c.json({ error: 'session not found', code: 'NOT_FOUND' }, 404); | ||
| } | ||
| const agent = detail.agents.find((a) => a.agentId === agentId); | ||
| if (!agent) { | ||
| return c.json( | ||
| { error: `agent "${agentId}" not found`, code: 'NOT_FOUND' }, | ||
| 404, | ||
| ); | ||
| } | ||
| const blobPath = join(agent.homedir, 'blobs', hash); | ||
| let content: Buffer; | ||
| try { | ||
| content = await readFile(blobPath); | ||
| } catch { | ||
| return c.json({ error: 'blob not found', code: 'NOT_FOUND' }, 404); | ||
| } | ||
| const mimeType = c.req.query('mime') ?? 'application/octet-stream'; | ||
| return new Response(content, { | ||
|
Comment on lines
+35
to
+40
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
For offloaded media, this route sends the blob file bytes directly with the requested media content type, but Useful? React with 👍 / 👎. |
||
| headers: { 'content-type': mimeType }, | ||
| }); | ||
| }); | ||
| return r; | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.