Commit graph

24 commits

Author SHA1 Message Date
Kai
373abb02f0
fix(agent-core): record and close tool calls interrupted mid-stream (#1790)
Some checks are pending
CI / build (push) Waiting to run
CI / VSIX package audit (darwin-arm64) (push) Waiting to run
CI / VSIX package audit (all) (push) Waiting to run
CI / typecheck (push) Waiting to run
CI / VSIX package audit (win32-x64) (push) Waiting to run
CI / test (1) (push) Waiting to run
CI / test (2) (push) Waiting to run
CI / test (3) (push) Waiting to run
CI / test (4) (push) Waiting to run
CI / test (5) (push) Waiting to run
CI / test-pi-tui (push) Waiting to run
CI / test-windows (push) Waiting to run
CI / lint (push) Waiting to run
Nix Build / Check flake.nix workspace sync (push) Waiting to run
Nix Build / nix build .#kimi-code (push) Blocked by required conditions
Release / Release (push) Waiting to run
Release / Deploy docs (push) Blocked by required conditions
Release / Native release artifact (push) Blocked by required conditions
Release / Desktop release artifact (push) Blocked by required conditions
Release / Publish native release assets (push) Blocked by required conditions
When the provider stream breaks off mid-tool-call (pause_turn, engine
overload, token limit), the step ended with the partial tool call dropped
silently: never executed, never recorded. If the response carried no other
usable content, the persisted assistant message was effectively empty and
every subsequent request — including compaction — was rejected with
"assistant must not be empty" (HTTP 400), wedging the session.

Record each unexecuted call instead (arguments sanitized to {} when the
truncated JSON is unparseable) and immediately close it with a synthetic
interrupted error result. The exchange stays wire-valid, the history stays
truthful, and the model learns the calls never ran so it can re-issue them.
2026-07-16 21:12:00 +08:00
Haozhe
a74ab44ac7
feat: raise default per-step LLM retry budget to 10 attempts (#1740)
* feat: raise default per-step LLM retry budget to 10 attempts

The default of 3 attempts only produced ~1.5s of backoff (0.5s/1s), so
sustained provider overload (429) surfaced to the user almost
immediately. With 10 attempts the exponential ramp (500ms base, x2,
32s cap, 25% jitter) waits out multi-minute overload windows before
failing the turn. Applies to both agent-core (chatWithRetry) and
agent-core-v2 (stepRetry service); loop_control.max_retries_per_step
still overrides.

* test(agent-core): pin retry-dependent tests to the new default budget

- error-paths: the warn-log attempt field now reads 1/10 with the
  default 10-attempt budget
- goal-session pause tests: every LLM call throws a retryable error, so
  the default 10-attempt backoff (~2min) blew the 5s test timeout; pin
  maxRetriesPerStep=1 since the pause behavior does not depend on the
  retry count
2026-07-15 15:24:17 +08:00
Kai
32a89c3643
fix(agent-core): prevent oversized media from poisoning sessions (#1657) 2026-07-14 12:46:45 +08:00
Haozhe
9f66ec416c
feat: harden LLM API fault tolerance against 429 and overload (#1530)
* feat(retry): harden LLM API fault tolerance against 429/overload

- retry more transient errors: 408/409/429/5xx/529, an embedded upstream
  status_code=429 in OpenAI Responses stream errors, and unclassified
  provider errors as a last-resort fallback
- honor server Retry-After (parsed into APIStatusError.retryAfterMs by the
  OpenAI and Anthropic providers); chatWithRetry prefers it over its backoff
- align app-level backoff with claude-code (500ms base, 32s cap, factor 2,
  up to 25% jitter) so high-attempt configs ride out multi-minute overload
- emit a turn.step.retrying meta line in -p --output-format stream-json
2026-07-10 15:47:12 +08:00
Kai
1bf2c9afee
feat: keep image-heavy sessions within provider request-size limits (#1508)
* feat(kosong): classify HTTP 413 request-body-too-large as a dedicated error type

* feat(agent-core): lower default image downscale cap to 2000px and make it configurable

* feat(agent-core): strip media to text markers and retry when the compaction request is too large

* feat(agent-core): cap model-initiated image reads with a configurable byte budget

* feat(agent-core): resend with degraded media when the provider rejects the request body as too large

* test(agent-core): add explicit timeouts to encode-heavy image budget tests

* feat: add WebP decoding support with wasm integration

- Introduced a new WebP decoding module using @jsquash/webp's wasm decoder.
- Implemented functions to decode WebP images and check for animated WebP formats.
- Updated image compression tests to include scenarios for WebP handling, including encoding and decoding.
- Enhanced error handling for API request size limits to accommodate various error messages.
- Updated pnpm lockfile to include new dependencies for WebP encoding and decoding.

* chore(changeset): consolidate this PR's entries into one

* fix(nix): update pnpmDeps hash for merged lockfile

* feat(agent-core): refuse HEIC/HEIF reads with platform-matched conversion guidance
2026-07-09 18:05:14 +08:00
Kai
65d30177ad
feat(agent-core): record llm request trace in wire.jsonl (#1448)
* feat(agent-core): record llm request trace in wire.jsonl

Add three observability record types so every request sent to the model
can be reconstructed from the wire log at the logical-request level:

- llm.tools_snapshot: content-addressed snapshot of the top-level tools
  table as sent (post deferred-strip), written once per unique table
- llm.request: one record per outbound request (retries, strict resends,
  and compaction rounds included) carrying the effective request params
  and hash links to the system prompt and tools snapshot
- mcp.tools_discovered: the server's verbatim tools/list result plus the
  agent's gating (allow-list, collisions), deduplicated by content hash

Observability records never feed state rebuild; replay only restores the
write-dedup cursors. The records/types.ts contract now documents the two
record classes explicitly (persisted is not the same as replayed).

Recording happens at the single Agent.generate choke point. The
LLMRequestLogFields side channel gains kind/projection/maxTokens/
droppedCount, chatWithRetry preserves caller-set fields, and compaction
tags its requests. The vis wire view renders the new record kinds.

* fix(agent-core): record the provider-clamped completion cap in the request trace

The llm.request trace recorded the client-requested budget cap, but
chat-completions providers tighten the actual wire value inside
withMaxCompletionTokens (remaining-context sizing, transport ceilings,
model-default resolution) — with the default budget the clamp is active
on nearly every non-empty-context request, so the recorded value did not
match what was sent.

Providers now expose the effective cap they computed as a readonly
maxCompletionTokens field on the clone, and the recorder reads it from
the effective provider at the Agent.generate choke point. This replaces
the side-channel recomputation, which is removed along with the
appliedCompletionBudgetCap helper.

* fix(agent-core): park pre-replay MCP discovery records and hash the collision outcome

Two wire-hygiene fixes for the mcp.tools_discovered trace:

Parking: the real Session ordering connects MCP servers concurrently with
agent construction, so ToolManager can observe a connected server before
agent.resume() has replayed the wire. Recording at that point bypassed
the restored dedup cursor (duplicating a 1-50KB record on every resume)
and appended a stray metadata record ahead of replay. AgentRecords now
exposes a one-shot opened latch — set when replay completes (after the
migration rewrite flushes) or when the first live record is logged — and
ToolManager parks discoveries until then, re-running the dedup check at
drain time. A frozen range-limited replay never opens; those agents are
transient previews.

Collision hashing: the dedup hash now covers the collision outcome, not
just the raw list and allow-list. Collisions depend on which other
servers hold a sanitized qualified name at registration time, so a
server can re-register with identical tools but a flipped outcome; that
gating change must produce a new record instead of being suppressed.

* fix(agent-core): skip the request trace for pre-flight-aborted calls

Mirror kosong generate()'s pre-flight abort check at the Agent.generate
choke point: a call whose signal is already aborted never reaches the
wire (generate throws before dispatching), so it must not leave an
llm.request/llm.tools_snapshot trace or a diagnostic log line claiming a
request was sent. Recording stays before dispatch for every call that
passes the gate, preserving the crash-safety of the trace.

* chore(agent-core): remove a leftover adaptive-thinking override hook

The adaptiveThinkingOverride option was a temporary local hook explicitly
marked for removal before commit. Nothing passes it, so resolution falls
back to the alias-level adaptiveThinking value in all cases; drop the
option and the dead indirection.

* fix(kosong): derive the exposed completion cap from generation kwargs

maxCompletionTokens was a field stored only by withMaxCompletionTokens,
so caps that reach the wire through other paths were invisible to the
request trace: with completion budgeting disabled via env, Anthropic
still sends the constructor-resolved max_tokens (required by the
Messages API), and constructor-level kwargs like OpenAILegacyOptions
maxTokens were likewise unreported.

Replace the stored field with a getter derived from each provider's
generation kwargs — the single source the request body reads — covering
constructor defaults, direct withGenerationKwargs configuration, and
budget application in one place. Kimi mirrors its request-time legacy
max_tokens alias normalization; openai-legacy reuses the same
normalizeGenerationKwargs the request path uses.

* feat(agent-core): add thinkingKeep passthrough for Kimi providers and update tests
2026-07-07 14:09:19 +08:00
Kai
743f66e547
refactor: move tool-result metadata into a structured note side channel (#1437)
* fix: stop rendering <system> notes from tool results in the terminal and web UIs

Tool results carry <system> blocks as side-channel notes for the model (ReadMediaFile summaries, Read status, MCP image captions, error/empty sentinels). Keep them in history for the model, but strip them at every core-to-UI boundary so they no longer render as plain text. vis is intentionally left untouched to preserve the model's-eye view for debugging.

* fix: keep error/empty status text visible when stripping tool-result <system> tags

Unwrap the tool error/empty sentinels (<system>ERROR: ...</system>, <system>Tool output is empty.</system>) instead of deleting them: keep the human-readable text and drop only the tags. Otherwise a failed or empty tool result rendered as a blank output, indistinguishable from a rendering bug. The model still reads the wrapped form in history.

* refactor: move tool-result metadata into a structured note side channel

Tool-produced model-facing metadata (ReadMediaFile summaries, Read status
lines, MCP image-compression captions) was baked into tool output as
<system> text, so every UI had to strip it back out and three copies of
the model-view normalization had silently drifted apart.

- ExecutableToolResult gains `note`: content rendered to the model but
  never to UIs; records and history now store the raw output plus the
  structured isError/note fields
- the model view is rendered exactly once at the LLM projection boundary
  by renderToolResultForModel; the transcript and vis hand-copies are
  deleted (vis now calls the same function for its model view, fixing
  their drifted empty-output checks)
- ReadMediaFile / Read / MCP captions write `note`; tool outputs stay
  pure data, and text-only results keep a single text part (note joined
  with a newline) so provider tool content stays a plain string
- all UI-side <system> stripping is removed; failed tools show their own
  error text with the structured isError flag
- wire protocol 1.4 -> 1.5 migrates existing records' tool-produced
  <system> blocks into `note` on resume

* fix: provider-neutral wording, no wire migration, direct optional fields

- "The attached image was downsampled" replaces directional wording that
  depended on provider serialization order (inline media vs flatten-and-
  re-attach)
- drop the 1.4 -> 1.5 wire migration: legacy records replay verbatim, so
  the model view of old sessions stays byte-identical to what the model
  originally saw and UIs show the legacy <system> text as-is; this also
  removes the risk of the migration misclassifying user data that quotes
  tool metadata, and the additive note field needs no version bump
- pass optional result fields as undefined instead of conditional spreads
  (repo convention)

* fix: enforce the note contract at the trust boundary; narrow the TUI system-tag guard

- normalizeToolResult now keeps a note only when it is a non-empty string:
  tools and finalize hooks are arbitrary JS, and a malformed note (null,
  number, object) would previously persist into the record and crash every
  subsequent LLM projection of the session. Everything downstream now
  trusts note to be string | undefined.
- the TUI tool body suppression matches the full <system-reminder> tag
  instead of any <system prefix: reminder piggy-backing stays hidden,
  while real output that merely starts with a literal <system> tag (file
  contents, MCP text) stays visible, covered through the real
  ToolCallComponent path.

* fix: return MCP compression captions as data instead of extracting them from text

compressImageContentParts now returns { parts, captions } — captions come
back from the compressor as structured data and are never inserted into
the parts, so the MCP pipeline no longer pattern-matches text to move
them into the note side channel. Tool output that merely quotes a
caption (a doc, a log, a test fixture) stays verbatim in the output.
Also corrects the stale claim that prompt ingestion uses this helper
(it compresses per image while constructing the part).

* docs: correct the image-compression re-export comment; export CompressedContentParts

The package-root comment still described compressImageContentParts as the
input-stage helper every ingestion site calls; prompt ingestion compresses
per image with compressBase64ForModel / compressImageForModel, and the MCP
pipeline is the walker's only caller. Also export the walker's
CompressedContentParts return type so public-API consumers can name it.

* feat: wrap tool status sentinels in <system> so the model can tell harness verdicts from tool output

The error/empty status text is model-only after the note refactor (UIs
render the raw output and style failures via the structured isError
flag), so the earlier plain-text wording served no remaining audience.
Wrapping the statuses in <system> gives every piece of system-generated
text inside a tool result the same marker:

- failed calls get '<system>ERROR: Tool execution failed.</system>'
  unconditionally — the ERROR:-prefix guard is removed, so the harness
  verdict can no longer be confused with tool output that happens to
  start with error-like text
- empty outputs render as '<system>Tool output is empty.</system>'; the
  plain placeholder the loop layer bakes into records is still
  recognized and upgraded at projection time

* style: collapse an internal helper docstring per the services subtree convention
2026-07-07 11:40:27 +08:00
liruifengv
e9db9cafcf
feat: record model response id in wire logs (#1349)
Some checks are pending
CI / build (push) Waiting to run
CI / test (push) Waiting to run
CI / test-windows (push) Waiting to run
CI / lint (push) Waiting to run
CI / typecheck (push) Waiting to run
Nix Build / Check flake.nix workspace sync (push) Waiting to run
Nix Build / nix build .#kimi-code (push) Blocked by required conditions
Release / Release (push) Waiting to run
Release / Deploy docs (push) Blocked by required conditions
Release / Native release artifact (push) Blocked by required conditions
Release / Desktop release artifact (push) Blocked by required conditions
Release / Publish native release assets (push) Blocked by required conditions
* feat: record model response id in wire logs

* chore: include sdk in response id changeset

* chore: include agent-core in response id changeset
2026-07-03 17:15:04 +08:00
Kai
4dd926b0ac
fix(agent-core): recover sessions bricked by orphan tool results (#1308)
* fix(agent-core): recover sessions bricked by orphan tool results

A stray `tool` message with no preceding assistant `tool_calls` permanently
bricked a session on OpenAI-compatible providers: every turn re-sent the same
malformed history and got a 400, and switching model/provider did not help.

Two independent gaps caused this:

- kosong did not recognize the OpenAI / DeepSeek / vLLM / Qwen phrasings of the
  tool-exchange structural 400 (`role 'tool' must be a response to a preceding
  message with 'tool_calls'` and the mirror `assistant message with 'tool_calls'
  must be followed by tool messages`), so the post-400 strict-resend fallback
  that drops the orphan never fired.

- The legacy-restore compaction path kept a verbatim tail
  `history.slice(compactedCount)`; when the cut landed inside a tool exchange the
  tail began with an orphan tool result whose assistant was summarized away. The
  normal projection does not repair a leading orphan, so the malformed history
  was baked in and re-sent every turn.

Recognize the additional phrasings so the strict resend un-bricks any session,
and trim leading tool results from the legacy-restore tail so the orphan is
never persisted in the first place.

* fix(agent-core): drop orphan tool results at the projection boundary

Rework the legacy-restore half of the fix based on review feedback: mutating
`_history` at restore time desyncs every consumer that models the history from
the wire records — the transcript reducer's fold length would overcount and
make MessageService skip unflushed live-tail messages.

Keep the restored history faithful to the wire records instead, and drop a
`tool` result whose call is nowhere in the history at the projection boundary,
on every request-building projection: the normal wire (`messages`), the
post-400 strict resend (`strictMessages`), and the compaction summarizer. An
orphan is wire-invalid on strict providers and useless to the model either
way, so it never reaches a provider — no longer relying on recognizing the
provider's 400 phrasing to recover. Fragment projections (e.g. token-estimating
a history slice) leave results untouched, since a matching call may
legitimately sit outside the slice.
2026-07-02 19:29:05 +08:00
Kai
93ec6cb652
fix(kosong): recognize OpenAI-compatible tool_call_id 400 as a recoverable tool-exchange error (#1292)
Some checks are pending
CI / build (push) Waiting to run
CI / test (push) Waiting to run
CI / test-windows (push) Waiting to run
CI / lint (push) Waiting to run
CI / typecheck (push) Waiting to run
Nix Build / Check flake.nix workspace sync (push) Waiting to run
Nix Build / nix build .#kimi-code (push) Blocked by required conditions
Release / Release (push) Waiting to run
Release / Deploy docs (push) Blocked by required conditions
Release / Native release artifact (push) Blocked by required conditions
Release / Desktop release artifact (push) Blocked by required conditions
Release / Publish native release assets (push) Blocked by required conditions
Moonshot / Kimi (OpenAI-compatible) rejects a history whose tool message
references a tool_call_id with no matching tool_calls entry in the preceding
assistant message as `400 tool_call_id  is not found`. The
TOOL_EXCHANGE_ADJACENCY_MESSAGE_PATTERNS only covered Anthropic's
tool_use/tool_result phrasing, so isRecoverableRequestStructureError returned
false, the strict-resend fallback in executeLoopStep never fired, and the
session stayed permanently stuck re-sending the same rejected history every
turn (observed in the field after a manual compaction busted the prompt cache
and forced full revalidation of a latently misordered prefix).

Add the tool_call_id-anchored pattern so the whole recovery chain — strict
projection (adjacency repair, orphan-result drop, synthetic results) plus the
one-shot resend — now also covers the default provider. Covered by classifier
unit tests and an e2e resend-and-recover case.
2026-07-02 13:52:44 +08:00
Kai
8ac337a2b2
fix(agent-core): harden strict-provider wire compliance so malformed history can't brick a session (#1241)
* feat(agent-core): rework compaction to keep only user prompts and summary

* refactor(agent-core): rewrite compaction summary as first-person handoff

Rework the full-compaction summary to read as the agent's own continuing
notes instead of a third-party report:

- compaction-instruction.md: free-form first-person continuation that
  preserves exact commands, paths and outcomes, states the precise next
  action, and flags claimed-but-unverified work rather than trusting it.
- compaction-summary-prefix.md: skeptical "your own working notes"
  framing; drop the collaborative third-party prefix.
- system.md: add compaction-awareness guidance so the model continues
  naturally from a summary and re-checks any reported "done".
- Rename the compaction helpers module to handoff.ts.

Update tests and regenerate snapshots for the new prompt text, and fill
in contextSummary in the restored-compaction replay expectations.

* fix(agent-core): count image/audio/video parts in token estimation

estimateTokensForContentPart returned 0 for image_url/audio_url/video_url,
so auto-compaction triggers, the overflow-shrink budget, the kept-user
budget, and the reported context size all went blind to media — a
media-heavy session could overflow the model window while the estimate
reported a near-empty context. Media parts now carry a fixed estimate
(MEDIA_TOKEN_ESTIMATE), and the content-part switch is exhaustive so a new
ContentPart kind must declare its estimate rather than silently count as
zero.

* feat(agent-core): re-surface active background tasks after compaction

Folding the live context to [recent user prompts, summary] drops the
messages that started background tasks and their status updates, so the
model could forget a task is still running and spawn a duplicate.
injectAfterCompaction now appends a system-reminder listing active
background tasks (with guidance to use TaskOutput/TaskList/TaskStop
instead of re-spawning). It runs only post-compaction and carries an
injection origin, so the next compaction drops and rebuilds it rather
than stacking copies; the all-user-role post-compaction shape is
preserved (no tool-pairing reintroduced).

* test(agent-core): add compaction scenario guards and risk probes

Adds compaction-scenarios.test.ts driving the real Agent/ContextMemory/
FullCompaction machinery:

- A guard test locking in that repeated compaction folds the prior summary
  into the new one instead of stacking two summaries.
- Seven `it.fails` probes that executably reproduce known, currently-accepted
  edge-case defects so the suite stays green while documenting each one
  precisely; any of them will flip red (forcing removal of `.fails`) the day
  the behavior is fixed. They cover: assistant/tool appended during an
  in-flight summarizer call being dropped; unbounded shrink on empty
  summaries; the fixed 20k kept-user budget overflowing a small model window;
  a tool result orphaned when compaction starts mid-exchange; legacy
  compaction records dropping their verbatim tail on replay; micro-compaction
  clearing recent tool results in an overflow-shrunk suffix; and media being
  discarded when the oldest kept user message is truncated.

* fix(agent-core): repair tool_use/tool_result adjacency in projected context

A tool call and its result can end up non-adjacent in history — a
background-task notification or flushed steer lands between them, or an
interrupted/nested step delays the result — which strict providers reject
with HTTP 400. The projector now moves each tool_use's result up to
immediately follow it (projection-time only; the stored history is
untouched), and full compaction projects its summarizer input with a
synthetic result for any still-open call so the summary request stays
well-formed. Micro-compaction only surfaced this latent ordering by busting
the prompt cache, so it now defaults off.

Includes projector adjacency regression tests, a context-level integration
test, and a compaction synthesize-missing guard; the prior "keeps an
unresolved tool exchange out of the compaction prompt" test is updated to
the now-well-formed (synthetic-result) behavior.

* fix(agent-core): preserve the verbatim tail when restoring legacy compactions

A pre-rework `context.apply_compaction` record used
`[summary, ...history.slice(compactedCount)]` semantics and kept a verbatim
recent tail, but it has no `keptUserMessageCount`. The reworked applyCompaction
re-folded such records into the all-user shape, dropping the recent
assistant/tool tail — so resuming a session compacted by an older version
silently lost its most recent context.

On restore of such a record (gated on records.restoring, no keptUserMessageCount,
and compactedCount < history length) reproduce the old shape instead. The
forward/live path is unchanged; the projector's tool-adjacency repair keeps the
restored tail well-formed, and compaction only runs at clean step boundaries so
the tail has no open exchange. The legacy-tail probe now passes as a regression
guard via the real restore path.

* fix(agent-core): align legacy compaction foldedLength with live restore

The transcript reducer re-derived foldedLength for pre-rework
context.apply_compaction records (no keptUserMessageCount) using the new
kept-user+summary rule, but ContextMemory's restore now reproduces the legacy
[summary, ...history.slice(compactedCount)] shape for those records. The two
diverged for legacy sessions, so MessageService's foldedLength-vs-live-history
comparison could mis-handle GET /messages (miss or misorder recent output).

The reducer now mirrors the live legacy fold: when compactedCount is below the
pre-compaction length it computes 1 + (length - compactedCount); otherwise it
falls back to the kept-user derivation. The MessageService transcript test's
fixture is corrected to a new-format record, matching its all-user live mock.

* fix(kosong): merge a follow-up user turn into the preceding tool_results

The Anthropic message merge keyed on isToolResultOnly(last) ===
isToolResultOnly(converted), which left a tool_result-only user turn
followed by a plain-text user turn unmerged. After tool-exchange repair
this shape (assistant tool_use -> tool_result -> injected notification)
produces two adjacent user messages, which strict Anthropic-compatible
backends reject with HTTP 400.

Switch to the asymmetric predicate isToolResultOnly(last) ||
!isToolResultOnly(converted): a tool-result-only running message absorbs
whatever user turn follows (parallel tool_results or a trailing text),
yielding a valid [tool_result, ..., text] message; a plain-text running
message still only absorbs plain text. [tool_result, text] is valid for
both native Anthropic (which concatenates anyway) and strict backends.

* test(agent-core): pin micro-compaction flag in the shrunk-suffix probe

The 'does not clear recent tool results when projecting a shrunk suffix'
probe is an it.fails that only documents a real defect while
micro-compaction is active. It inherited the ambient
KIMI_CODE_EXPERIMENTAL master switch, so its pass/fail flipped with the
runner: green locally (master switch on) but a hard failure in CI, where
the flag defaults off and MicroCompaction.compact() is a no-op that
leaves the tool result intact.

Enable KIMI_CODE_EXPERIMENTAL_MICRO_COMPACTION explicitly for this probe
so it deterministically exercises the micro-compaction path regardless of
the environment.

* fix(agent-core): harden full compaction against in-flight races, unbounded shrink, and media loss

Three compaction-path fixes surfaced by review, each flipping its
documenting it.fails probe to a passing it:

- Append race (CMP-02): after the summarizer returns, the post-summary
  history check only compared the compacted prefix. A live step appending
  to the tail while a manual/SDK compaction was in flight slipped through —
  an appended assistant/tool turn is neither summarized (the summary covers
  only the snapshot) nor kept (the rebuild keeps user input), so it
  vanished. Now cancel when the appended tail contains a non-user message;
  an appended user message is still kept (rebuild picks it up), preserving
  the existing 'keeps messages appended while compacting an unchanged
  prefix' behavior.

- Unbounded empty/truncated shrink: an empty or truncated summary dropped
  the oldest message and reset retryCount, so a model that kept returning
  empty could issue ~one request per history entry. Bound the shrink
  attempts by MAX_COMPACTION_RETRY_ATTEMPTS, mirroring the overflow-shrink
  counter.

- Media dropped on truncation (CMP-07): truncating the oldest kept user
  message replaced its whole content with one text block, discarding any
  image/audio/video. Keep the non-text parts and spend the remaining budget
  (maxTokens minus their cost) on truncated text.

* fix(vis): mirror legacy compaction tail in the model-mode projector

For a pre-rework context.apply_compaction record (no keptUserMessageCount),
agent-core's ContextMemory restore and the transcript reducer keep the old
[summary, ...history.slice(compactedCount)] tail — a verbatim recent tail
including assistant/tool. The vis model-mode projector always applied the
new kept-user selection, so opening an older compacted session in model
mode hid the assistant/tool tail the resumed agent still holds (and
surfaced a pre-compaction user message the agent dropped).

Branch on a missing keptUserMessageCount with compactedCount < history
length and reproduce the legacy shape, matching the agent-core restore.

* fix(agent-core): cancel compaction on any droppable user-role tail

The in-flight append guard cancelled only when the tail grew with a
non-user role. A user-role message that compaction would still drop — a
background-task notification, hook/cron reminder, or shell-command output —
slipped through: appended after the summary snapshot (so absent from the
summary) and dropped by the all-user rebuild (which keeps only real user
input), vanishing silently.

Key the guard on the same predicate applyCompaction uses (!isRealUserInput)
so it cancels whenever the appended tail holds anything compaction would
drop. A real user message is still kept, so a live user turn racing a
manual/SDK compaction continues to complete.

* fix(agent-core): exclude pre-clear prompts from legacy folded length

The transcript reducer's legacy fallback (records predating
keptUserMessageCount, compacted with no verbatim tail) re-derived the
kept-user count from the whole transcript, including messages before the
last context.clear. Live ContextMemory rebuilds _history from post-clear
messages only, so counting pre-clear prompts overstated foldedLength;
MessageService then saw context.history.length <= foldedLength and skipped
appending unflushed live tail messages, dropping recent output from the
messages endpoint for old sessions compacted after a clear.

Derive only from entries at or after clearFloor to match the live context.

* fix(agent-core): drop media when truncating the oldest kept prompt

Revert the media-preserving truncation: keeping non-text parts on the
truncated boundary message overshot the kept-user budget when the media
alone exceeded it, and reordered interleaved text/media parts. Both codex
(no media-aware truncation) and Claude Code (strips media at compaction)
decline to preserve media on a truncated message, since media cannot be
partially truncated and keeping it whole breaks the budget.

truncateUserMessage now keeps only the truncated text. Recent messages
that fit the budget are still kept verbatim with their media; only the
oldest, partially-overflowing boundary message loses its attachments.

* fix(agent-core): make manual compaction and turns mutually exclusive

A manual/SDK compaction could start while a turn was streaming, or a new
turn could launch while a compaction was in flight. Either way the turn
mutates the shared context (streaming content into an existing assistant
message, or appending new messages) during the summarizer await, and that
output is neither summarized nor preserved by the all-user rebuild —
silent loss that object-identity checks can't detect (the streamed message
is mutated in place).

Guard both directions so the agent does one of {turn, compaction} at a
time: begin() refuses a manual compaction while a turn is active, and
launch() refuses a new turn while a compaction is in progress. Auto
compaction is exempt — it runs from within the turn at a step boundary,
which blocks the turn for its duration.

* chore(changeset): consolidate compaction changesets into one

* chore(agent-core): drop external-product references from compaction comments

* test(agent-core): add Anthropic wire-compliance smoke tests for compaction

Drive real compaction output and the compaction summarizer projection
through the real Anthropic provider conversion and assert the wire request
is well-formed: strict user/assistant alternation and every tool_use
answered by an adjacent tool_result. Locks in the cross-layer guarantee
(projector merge + Anthropic consecutive-user merge + adjacency repair +
synthesizeMissing) that compacted sessions stay valid for strict
Anthropic-compatible backends.

* fix(agent-core): defer and replay inputs during manual compaction instead of rejecting

Manual/SDK compaction runs outside a turn, so the earlier guard rejected
prompts/steers that arrived while it held the context. That broke three
things: a REST/web prompt got stuck 'running' (no terminal turn event), a
background-task/cron steer was silently lost (null was read as 'buffered'
but nothing was), and a follow-up prompt could land in the window after
isCompacting cleared but before reminders were reinjected.

Reuse the existing defer-and-replay model instead of rejecting:

- steer() and launch() buffer into steerBuffer while a compaction is in
  progress (returning null = buffered), mirroring how an active turn defers
  input.
- FullCompaction.compactionWorker keeps isCompacting true through
  refreshSystemPrompt + injectAfterCompaction (moving markCompleted and the
  completed event after reinjection), then replays the buffer via
  TurnFlow.onCompactionFinished — on success, on an A1 prefix/tail cancel,
  and on failure/abort.
- onCompactionFinished flushes into an active turn if one exists, else
  launches a fresh turn from the deferred input.

No PromptService change: a deferred prompt's eventual turn.started lets it
associate the pending prompt and clear it on turn.ended.

* feat(kosong): detect tool_use/tool_result adjacency errors

Add isToolExchangeAdjacencyError to classify the strict-provider 400 raised
when an assistant tool_use is not correctly paired with its tool_result
(missing, stray, or non-adjacent), excluding context-overflow 400s. Lets the
agent loop recognize the error and resend a wire-compliant request instead of
leaving the session stuck.

* fix(agent-core): close mid-history orphan tool calls and resend wire-compliant after a strict 400

Strict providers (Anthropic) reject a request whose assistant tool_use is not
answered by an adjacent tool_result, and the same malformed history is re-sent
every turn, permanently bricking the session.

- Projector now closes a mid-history tool call whose result is missing entirely
  (a later turn proves it is not in-flight) with a synthetic result; the
  trailing in-flight call is still left untouched.
- Add a strict projection (synthesize every open call, drop stray results) and,
  on a tool_use/tool_result adjacency 400, resend the request once with it.
- Report every projection repair (reorder / synthesize / drop) via log and
  telemetry, deduped by signature, so a silently-mangled history leaves a trace.
  Trailing-tail synthesis (expected under compaction) is not flagged.

* fix(kosong): merge consecutive user turns for strict providers

Gemini/Vertex require strictly alternating user/model turns and reject
consecutive user turns with HTTP 400. They arise after compaction (kept
prompts + user-role summary + injected reminders) and when a turn is
steered in right after a tool result. Anthropic already merged them
inline; the Google converter did not, so post-compaction requests failed.

Extract the asymmetric merge into a shared mergeConsecutiveUserMessages
helper applied at each strict provider's conversion boundary: refactor
Anthropic to use it (behavior unchanged) and apply it at the Google
converter's exit. A conformance suite drives every strict provider with
the post-compaction shape and a steer-after-tool-result shape, asserting
no consecutive same-role turns reach the wire, so a new strict provider
cannot silently omit the merge.

The provider-agnostic projector stays structure-preserving: lenient
providers (OpenAI/Kimi) keep distinct turns for clearer message
boundaries; only strict providers normalize, where the requirement lives.

* feat(kosong): recognize the broader structural request-rejection family

Add isRecoverableRequestStructureError, covering the strict-provider 400s that
stem from a malformed message array re-sent every turn: tool_use/tool_result
pairing, empty/whitespace-only text blocks, a non-user first message, and
non-alternating roles. Context-overflow 400s are excluded (handled by
compaction). Lets the loop trigger one strict, wire-compliant resend for the
whole family rather than only tool-pairing errors.

* fix(agent-core): sanitize whitespace and strict-resend structural 400s, with diagnostics

- Drop empty AND whitespace-only text blocks in projection (Anthropic rejects
  whitespace-only with "text content blocks must contain non-whitespace text",
  which otherwise sticks a session); treat whitespace-only tool output as empty.
- Broaden the post-400 strict resend to the whole structural family and add two
  strict-only passes to the strict projection: drop leading non-user messages
  (first message must be user) and merge consecutive assistant turns.
- Log + telemetry for every wire repair the projector applies (reorder,
  synthesize, drop orphan, drop leading, merge assistants, drop whitespace),
  deduped by signature; log the strict resend outcome (recovered or still
  rejected) so a stuck session always leaves a trace.

* fix(agent-core): normalize empty-equivalent tool result arrays to the empty placeholder

A tool result whose ContentPart[] output has no sendable content (an empty array,
or only empty/whitespace-only text blocks) was returned verbatim, so projection
stripped the blank blocks, left the tool message empty, and threw on every send —
bricking the session locally. String outputs were already normalized; do the same
for arrays. A non-text part or any non-whitespace text still keeps the real
output.

* chore(changeset): simplify the wire-compliance changeset
2026-07-01 02:16:19 +08:00
Kai
063538744f
refactor(agent-core): align malformed tool args with schema validation (#1209)
Some checks are pending
CI / test-windows (push) Waiting to run
CI / lint (push) Waiting to run
CI / build (push) Waiting to run
CI / test (push) Waiting to run
CI / typecheck (push) Waiting to run
Nix Build / Check flake.nix workspace sync (push) Waiting to run
Nix Build / nix build .#kimi-code (push) Blocked by required conditions
Release / Native release artifact (push) Blocked by required conditions
Release / Release (push) Waiting to run
Release / Deploy docs (push) Blocked by required conditions
Release / Publish native release assets (push) Blocked by required conditions
* feat(agent-core): repair malformed tool args JSON

Attempt jsonrepair when tool call arguments fail JSON.parse, then continue schema validation. Return malformed JSON errors with a concise expected schema hint so the model can retry with corrected arguments.

* chore(nix): update pnpm deps hash

Update the fixed-output pnpmDeps hash after adding jsonrepair so the Nix build can fetch dependencies.

* refactor(agent-core): drop tool args JSON repair

Stop repairing malformed tool call arguments. Fall back to an empty object on JSON parse failure and let schema validation produce the retry error, while preserving valid arguments for unknown tools in the transcript.

* chore: add changeset for tool args validation
2026-06-29 23:24:03 +08:00
7Sageer
843a731097
fix: classify OAuth token refresh errors by cause (#838)
Map OAuth token-fetch failures to distinct public error codes instead of collapsing them all to auth.login_required:

- missing/revoked tokens or 401/403 from the refresh endpoint -> auth.login_required
- transport failures and 429/5xx after internal retries -> provider.connection_error
- anything else is rethrown as-is (surfaces as internal) rather than guessed

The refresh helper already retries internally, so the agent loop does not
re-retry these. Both the managed provider and the standalone SDK provider
share the same mapping.
2026-06-17 18:34:42 +08:00
_Kerman
4e5043b03b
fix: require AgentSwarm to run alone (#643)
Some checks are pending
CI / build (push) Waiting to run
CI / test (push) Waiting to run
CI / lint (push) Waiting to run
CI / typecheck (push) Waiting to run
Nix Build / Check flake.nix workspace sync (push) Waiting to run
Nix Build / nix build .#kimi-code (push) Blocked by required conditions
Release / Release (push) Waiting to run
Release / Deploy docs (push) Blocked by required conditions
Release / Native release artifact (push) Blocked by required conditions
Release / Publish native release assets (push) Blocked by required conditions
2026-06-11 13:58:35 +08:00
Luyu Cheng
ac37d74484
feat: pursue a goal autonomously (#270) 2026-06-02 22:52:23 +08:00
Kai
933cf6727e
fix(agent-core): surface user interruptions to the model instead of a neutral abort (#236)
When the user interrupts running tools or parallel subagents, the tool_result
fed back to the model was a neutral `Tool "X" was aborted` or a weak "stopped by
the user", so the model treated it as a system fault and speculated about
capacity/concurrency limits instead of recognizing a deliberate stop.

Carry a UserCancellationError as the AbortSignal reason from the cancel sites
(Turn.cancel/abortTurn, SessionSubagentHost.cancelAll) through to the message
sites (tool-call settle paths and the AgentTool catches), which now emit an
explicit "deliberate user action, not a system error/timeout/capacity limit"
message. Aborts propagated from another signal (e.g. a subagent's deadline via
waitForCurrentTurn) carry their original reason, so a timeout is not mislabeled
as a user interruption. The telemetry outcome classifier matches the new
"manually interrupted" phrase to keep counting these as cancelled.
2026-05-30 09:53:21 +08:00
qer
3da4daeade
fix(kosong): retry when a response stream is terminated mid-flight (#201)
A mid-stream SSE drop surfaces as a raw undici `TypeError: terminated`, which was classified as a non-retryable generic error and failed the turn on the first attempt. Route raw transport-layer errors through the connection-error heuristic so a dropped stream becomes a retryable APIConnectionError and is retried transparently. User aborts (ESC) are unaffected — the retry loop checks the abort signal before retrying.

Related to #149.
2026-05-29 15:18:55 +08:00
qer
1873859b0e
refactor(agent-core): slim llm request log line (#190)
Merge turnId/step into a single `turnStep` field ("0.1") and
attempt/maxAttempts into `attempt` ("2/3"), and drop the
messageCount/toolCallCount fields. The per-request `llm request`
line goes from up to 8 fields down to ~3; the `llm config` line
(including thinkingEffort, logged for all providers) is unchanged.
2026-05-29 13:36:34 +08:00
_Kerman
537cf20d18
feat: remove default per-turn step limit of 1000 (#186) 2026-05-29 13:11:45 +08:00
_Kerman
2b74025302
feat: rework permission decision policies (#26) 2026-05-27 20:07:24 +08:00
qer
55870616ca
feat: expose LLM stream timing events (#101)
* feat: expose LLM stream timing events

* feat(kimi-code): show LLM timing in debug mode (KIMI_CODE_DEBUG=1)

* refactor: extract debug timing formatting into standalone module

* chore: remove changeset

---------

Co-authored-by: liruifengv <liruifeng1024@gmail.com>
2026-05-27 13:46:57 +08:00
7Sageer
7d9216d5aa
fix(agent-core): ensure tool.call always has paired tool.result (#83)
* fix(agent-core): always pair tool.call with tool.result on malformed returns

A tool returning undefined, a primitive, or an object without a valid
output field crashed normalization, exited the batch loop without
dispatching the matching tool.result, and left the next provider
request to fail with a missing tool_call_id response.

- Validate the tool return at the boundary in runRunnableToolCall via a
  new coerceToolResult helper; malformed returns become an isError
  result.
- Harden normalizeToolResult and toolResultStopsTurn against
  non-conformant input from synthetic results or finalize hooks.
- Track unpaired tool.call ids in runToolCallBatch and emit
  compensating error results in finally for any that did not receive a
  result.

* refactor(agent-core): simplify tool result pairing

Drop the unpairedCallIds tracking and finally-compensation wrapper from
runToolCallBatch. The set was updated before dispatchEvent, so a failure
between the two left it inconsistent; the per-call try/catch already
produces a paired error result, making the outer wrapper redundant.

Coerce hook returns at the synthetic and finalizeToolResult boundaries
so a malformed hook output is normalized into a paired error result the
same way a malformed tool return already is.
2026-05-26 20:14:54 +08:00
_Kerman
c4dd1c7ff2
feat: flatten tool call records (#25) 2026-05-25 23:02:25 +08:00
Kaiyi
842e699a64 Kimi For Coding 2026-05-22 15:54:50 +08:00