Skip to content
Merged
7 changes: 7 additions & 0 deletions .changeset/wire-request-trace.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"@moonshot-ai/agent-core": minor
"@moonshot-ai/kimi-code": minor
"@moonshot-ai/kosong": minor
---

Record a request trace in each agent's `wire.jsonl`: content-addressed snapshots of the tool schemas sent to the model, one record per model request (including retries, strict resends, and compaction rounds) with the effective request parameters, and the raw MCP tool listing per server, so sessions carry enough data to reconstruct every model request for debugging. Chat providers now expose the effective completion-token cap they send on the wire, so the trace records the provider-clamped value rather than the requested budget.
7 changes: 6 additions & 1 deletion apps/vis/server/src/lib/context-projector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -496,7 +496,9 @@ export function projectContext(
case 'swarm_mode.exit':
swarm = { active: false };
break;
// Kinds that don't affect the projected timeline / derived state:
// Kinds that don't affect the projected timeline / derived state,
// including the observability records (request trace — `llm.*`,
// `mcp.tools_discovered`), which are never part of context state:
case 'metadata':
case 'forked':
case 'turn.prompt':
Expand All @@ -510,6 +512,9 @@ export function projectContext(
case 'tools.unregister_user_tool':
case 'tools.set_active_tools':
case 'tools.update_store':
case 'llm.tools_snapshot':
case 'llm.request':
case 'mcp.tools_discovered':
break;
default: {
const _exhaustive: never = rec;
Expand Down
147 changes: 147 additions & 0 deletions apps/vis/web/src/components/wire/renderers.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -592,6 +592,153 @@ export const WIRE_RENDERERS: RendererMap = {
label: 'goal×',
headline: () => ({ main: <Dim>goal cleared</Dim> }),
},

// Observability records — the request trace (see agent-core records/types.ts).

'llm.tools_snapshot': {
tone: 'tools',
label: 'req·tools',
headline: (r) => ({
main: (
<span className="flex items-center gap-2 min-w-0">
<Mono>
{r.tools.length} tool{r.tools.length === 1 ? '' : 's'}
</Mono>
<Dim className="truncate">
{r.tools
.slice(0, 4)
.map((tool) => tool.name)
.join(', ')}
{r.tools.length > 4 ? ` +${r.tools.length - 4} more` : ''}
</Dim>
</span>
),
right: <Mono>#{r.hash.slice(0, 8)}</Mono>,
}),
},

'llm.request': {
tone: 'meta',
label: 'llm→',
headline: (r) => {
const parts: string[] = [];
if (r.turnStep !== undefined) parts.push(`step ${r.turnStep}`);
if (r.attempt !== undefined) parts.push(`attempt ${r.attempt}`);
parts.push(`${r.messageCount} msgs`);
if (r.maxTokens !== undefined) parts.push(`max ${r.maxTokens} tok`);
return {
main: (
<span className="flex items-center gap-2 min-w-0">
<Pill tone={r.kind === 'compaction' ? 'compaction' : 'turn'} variant="soft">
{r.kind}
</Pill>
<Mono>{r.model}</Mono>
<Dim className="truncate">{parts.join(' · ')}</Dim>
</span>
),
right:
r.projection !== undefined ? (
<Pill tone="warning" variant="soft">
{r.projection}
</Pill>
) : undefined,
};
},
detail: (r) => (
<div className="grid grid-cols-[140px_1fr] gap-x-3 gap-y-[2px]">
<FieldRow label="provider">
<Mono>{r.provider}</Mono>
</FieldRow>
<FieldRow label="model">
<Mono>{r.model}</Mono>
</FieldRow>
{r.modelAlias !== undefined ? (
<FieldRow label="modelAlias">
<Mono>{r.modelAlias}</Mono>
</FieldRow>
) : null}
{r.thinkingEffort !== undefined ? (
<FieldRow label="thinkingEffort">
<Mono>{r.thinkingEffort}</Mono>
</FieldRow>
) : null}
{r.thinkingKeep !== undefined ? (
<FieldRow label="thinkingKeep">
<Mono>{r.thinkingKeep}</Mono>
</FieldRow>
) : null}
{r.temperature !== undefined ? (
<FieldRow label="temperature">
<span className="text-[var(--color-sev-info)]">{r.temperature}</span>
</FieldRow>
) : null}
{r.topP !== undefined ? (
<FieldRow label="topP">
<span className="text-[var(--color-sev-info)]">{r.topP}</span>
</FieldRow>
) : null}
{r.maxTokens !== undefined ? (
<FieldRow label="maxTokens">
<span className="text-[var(--color-sev-info)]">{r.maxTokens}</span>
</FieldRow>
) : null}
{r.betaApi !== undefined ? (
<FieldRow label="betaApi">
<Mono>{String(r.betaApi)}</Mono>
</FieldRow>
) : null}
<FieldRow label="toolSelect">
<Mono>{String(r.toolSelect)}</Mono>
</FieldRow>
<FieldRow label="toolsHash" wide>
<Mono className="break-all">{r.toolsHash}</Mono>
</FieldRow>
<FieldRow label="systemPromptHash" wide>
<Mono className="break-all">{r.systemPromptHash}</Mono>
</FieldRow>
{r.systemPrompt !== undefined ? (
<FieldRow label="systemPrompt" wide>
<SizePreview
label="systemPrompt"
sizeBytes={r.systemPrompt.length}
preview={r.systemPrompt}
>
<pre className="whitespace-pre-wrap break-words text-fg-1">{r.systemPrompt}</pre>
</SizePreview>
</FieldRow>
) : null}
{r.droppedCount !== undefined ? (
<FieldRow label="droppedCount">
<span className="text-[var(--color-sev-info)]">{r.droppedCount}</span>
</FieldRow>
) : null}
</div>
),
},

'mcp.tools_discovered': {
tone: 'tools',
label: 'mcp·list',
headline: (r) => ({
main: (
<span className="flex items-center gap-2 min-w-0">
<Mono>{r.serverName}</Mono>
<Dim>
{r.tools.length} tool{r.tools.length === 1 ? '' : 's'} · {r.enabledNames.length}{' '}
enabled
</Dim>
</span>
),
right:
r.collisions !== undefined && r.collisions.length > 0 ? (
<Pill tone="warning" variant="soft">
{r.collisions.length} collision{r.collisions.length === 1 ? '' : 's'}
</Pill>
) : (
<Mono>#{r.hash.slice(0, 8)}</Mono>
),
}),
},
};

/** Look up a renderer by a runtime `type` string. Returns `undefined` for kinds
Expand Down
2 changes: 1 addition & 1 deletion docs/en/guides/sessions.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ All sessions are saved under `$KIMI_CODE_HOME/sessions/` (default: `~/.kimi-code
```

- `state.json`: session metadata such as title and creation time.
- `agents/*/wire.jsonl`: the agent event stream, used for session recovery and replay.
- `agents/*/wire.jsonl`: the agent event stream, used for session recovery and replay. It also carries a request trace — the tool schemas, request parameters, and MCP tool listings sent to the model — for debugging.

::: warning
Do not manually edit files inside the `sessions/` directory — doing so may prevent sessions from being restored correctly.
Expand Down
2 changes: 1 addition & 1 deletion docs/zh/guides/sessions.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ Kimi Code CLI 把每次对话持久化为一个「会话」,保留消息历史
```

- `state.json`:会话标题、创建时间等元数据。
- `agents/*/wire.jsonl`:Agent 事件流,用于会话恢复和回放。
- `agents/*/wire.jsonl`:Agent 事件流,用于会话恢复和回放;同时记录发给模型的请求轨迹(工具 schema、请求参数、MCP 工具清单),便于调试

::: warning 注意
`sessions/` 目录下的文件请勿手动编辑,否则可能导致会话无法正常恢复。
Expand Down
10 changes: 7 additions & 3 deletions packages/agent-core/src/agent/compaction/full.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
} from '@moonshot-ai/kosong';

import type { Agent } from '..';
import type { GenerateOptionsWithRequestLogFields } from '../llm-request-logger';
import type { ContextMessage } from '../context/types';
import {
collectLoadedDynamicToolNames,
Expand All @@ -43,8 +44,7 @@ import {
import {
applyCompletionBudget,
resolveCompletionBudget,
} from '../../utils/completion-budget';
import { renderPrompt } from '../../utils/render-prompt';
} from '../../utils/completion-budget';import { renderPrompt } from '../../utils/render-prompt';
import compactionInstructionTemplate from './compaction-instruction.md?raw';
import type { CompactionBeginData, CompactionResult } from './types';
import {
Expand Down Expand Up @@ -514,13 +514,17 @@ export class FullCompaction {
];
const estimatedCompactionRequestTokens = this.estimateRequestTokens(messages);
try {
const generateOptions: GenerateOptionsWithRequestLogFields = {
signal,
requestLogFields: { kind: 'compaction', droppedCount },
};
const response = await this.agent.generate(
provider,
this.agent.config.systemPrompt,
[...this.agent.tools.loopTools],
messages,
undefined,
{ signal },
generateOptions,
);
if (response.finishReason === 'truncated') {
throw new CompactionTruncatedError();
Expand Down
32 changes: 24 additions & 8 deletions packages/agent-core/src/agent/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ import { TurnFlow } from './turn';
import { KosongLLM } from './turn/kosong-llm';
import { UsageRecorder } from './usage';
import { LlmRequestLogger, splitGenerateOptions } from './llm-request-logger';
import { LlmRequestRecorder } from './llm-request-recorder';
import { resolveCompletionBudget } from '../utils/completion-budget';
import type { Kaos } from '@moonshot-ai/kaos';
import type { ToolServices } from '../tools/support/services';
Expand Down Expand Up @@ -125,6 +126,7 @@ export class Agent {
readonly experimentalFlags: ExperimentalFlagResolver;

readonly llmRequestLogger: LlmRequestLogger;
readonly llmRequestRecorder: LlmRequestRecorder;
readonly blobStore: BlobStore | undefined;
readonly records: AgentRecords;
readonly fullCompaction: FullCompaction;
Expand Down Expand Up @@ -187,6 +189,7 @@ export class Agent {
this.systemPromptContextProvider = options.systemPromptContextProvider;

this.llmRequestLogger = new LlmRequestLogger(this.log);
this.llmRequestRecorder = new LlmRequestRecorder(this);
this.blobStore = options.homedir
? new BlobStore({ blobsDir: join(options.homedir, 'blobs') })
: undefined;
Expand Down Expand Up @@ -262,14 +265,27 @@ export class Agent {
const { requestLogFields, generateOptions } = splitGenerateOptions(options);
const modelAlias = this.config.modelAlias;
const run = (requestOptions: Parameters<typeof generate>[5]) => {
this.llmRequestLogger.logRequest({
provider,
modelAlias,
systemPrompt,
tools,
messages: history,
fields: requestLogFields,
});
// Mirror kosong generate()'s pre-flight abort check: a call whose
// signal is already aborted never reaches the wire (generate throws
// before dispatching), so it must not leave a request trace or a
// diagnostic log line claiming a request was sent.
if (requestOptions?.signal?.aborted !== true) {
this.llmRequestLogger.logRequest({
provider,
modelAlias,
systemPrompt,
tools,
messages: history,
fields: requestLogFields,
});
this.llmRequestRecorder.record({

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Defer request records until provider dispatch

For providers that fail locally before dispatching HTTP, this records a durable llm.tools_snapshot/llm.request even though no model request went on the wire. For example, a custom Kimi/OpenAI provider with no API key throws while building the client before the provider sends the request; the abort guard covers only one pre-flight path, so these other pre-send failures still leave misleading billed-request traces. Consider recording from the provider dispatch seam (onRequestSent/after pre-dispatch validation) rather than before rawGenerate.

Useful? React with 👍 / 👎.

provider,
systemPrompt,
tools,
messages: history,
fields: requestLogFields,
});
}
return this.rawGenerate(provider, systemPrompt, tools, history, callbacks, requestOptions);
};
if (generateOptions?.auth !== undefined) {
Expand Down
4 changes: 2 additions & 2 deletions packages/agent-core/src/agent/llm-request-logger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,10 +68,10 @@ export function splitGenerateOptions(options: GenerateOptionsWithRequestLogField
return { requestLogFields, generateOptions };
}

function toolSignature(tools: readonly Tool[]) {
export function toolSignature(tools: readonly Tool[]) {
return tools.map(({ name, description, parameters }) => ({ name, description, parameters }));
}

function fingerprint(content: string): string {
export function fingerprint(content: string): string {
return createHash('sha256').update(content).digest('hex');
}
Loading
Loading