Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
ba6e2b3
refactor: move KIMI_AGENTS_MD to first message from system prompt.
Eric-Guo Nov 14, 2025
ef15547
fix: update test snapshots after AGENTS.md refactoring (#2)
Copilot Nov 15, 2025
aad51e1
Dropping KIMI_AGENTS_MD breaks existing custom templates
Eric-Guo Nov 15, 2025
1ff9a91
✨Ensured AGENTS.md instructions survive compaction so project guidanc…
Eric-Guo Nov 15, 2025
5d871b5
✨Added an early guard in /init so it now inspects both AGENTS.md and …
Eric-Guo Nov 15, 2025
ab692c8
✨Injected AGENTS.md reinsertion now handles restored histories and is…
Eric-Guo Nov 20, 2025
b24ed9b
✨Updated /init so newly generated AGENTS metadata is cached immediate…
Eric-Guo Nov 20, 2025
2ae304c
Fix due to upstream rename CustomToolset to KimiToolset
Eric-Guo Nov 20, 2025
24a706e
✨Compaction now reverts then strips any pre-checkpoint AGENTS.md mess…
Eric-Guo Nov 20, 2025
d5df905
Fix for #340
Eric-Guo Nov 21, 2025
e2231c5
✨/init now handles KaosPath paths correctly (awaits async is_file/loa…
Eric-Guo Nov 21, 2025
1532f08
✨Runtime cloning now keeps the cached AGENTS.md when spawning subagen…
Eric-Guo Nov 21, 2025
67a3cdd
✨Refactored test for init_meta_command to ensure proper type casting …
Eric-Guo Nov 21, 2025
55a9cc4
✨The newly added src/kimi_cli/soul/runtime.py introduces another Runt…
Eric-Guo Nov 21, 2025
9dcc79c
✨Synced refreshed AGENTS.md across the soul runtime, agent runtime, t…
Eric-Guo Nov 21, 2025
29291ba
✨Tagged AGENTS.md system message with a stable marker/hash and update…
Eric-Guo Nov 21, 2025
ac96b34
✨Added Context.filter_messages to rewrite history while preserving ch…
Eric-Guo Nov 22, 2025
ca76751
✨Do refactor by moving filter_messages method to utils/history.py, th…
Eric-Guo Nov 23, 2025
088d3d4
✨Enhanced KimiSoul class by adding read-only properties for runtime a…
Eric-Guo Nov 23, 2025
89ddeae
✨When AGENTS.md disappears, _ensure_initial_system_messages now clear…
Eric-Guo Nov 24, 2025
f631ee1
Fix: error: Method declaration "runtime" is obscured by a declaration…
Eric-Guo Nov 27, 2025
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 AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,6 @@ Builtin variables available in system prompts:
- `${KIMI_NOW}`: Current timestamp
- `${KIMI_WORK_DIR}`: Working directory path
- `${KIMI_WORK_DIR_LS}`: Directory listing output
- `${KIMI_AGENTS_MD}`: Project AGENTS.md content

## Deployment

Expand Down
12 changes: 0 additions & 12 deletions src/kimi_cli/agents/default/system.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,15 +58,3 @@ Use this as your basic understanding of the project structure.
## Date and Time

The current date and time in ISO format is `${KIMI_NOW}`. This is only a reference for you when searching the web, or checking file modification time, etc. If you need the exact time, use Shell tool with proper command.

# Project Information

Markdown files named `AGENTS.md` usually contain the background, structure, coding styles, user preferences and other relevant information about the project. You should use this information to understand the project and the user's preferences. `AGENTS.md` files may exist at different locations in the project, but typically there is one in the project root. The following content between two `---`s is the content of the root-level `AGENTS.md` file.

`${KIMI_WORK_DIR}/AGENTS.md`:

---

${KIMI_AGENTS_MD}

---
26 changes: 9 additions & 17 deletions src/kimi_cli/soul/agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import inspect
import string
from collections.abc import Mapping
from dataclasses import asdict, dataclass
from dataclasses import asdict, dataclass, replace
from datetime import datetime
from pathlib import Path
from typing import Any
Expand Down Expand Up @@ -35,8 +35,6 @@ class BuiltinSystemPromptArgs:
"""The absolute path of current working directory."""
KIMI_WORK_DIR_LS: str
"""The directory listing of current working directory."""
KIMI_AGENTS_MD: str # TODO: move to first message from system prompt
"""The content of AGENTS.md."""


async def load_agents_md(work_dir: KaosPath) -> str | None:
Expand All @@ -63,6 +61,7 @@ class Runtime:
denwa_renji: DenwaRenji
approval: Approval
labor_market: LaborMarket
agents_md: str

@staticmethod
async def create(
Expand All @@ -84,34 +83,26 @@ async def create(
KIMI_NOW=datetime.now().astimezone().isoformat(),
KIMI_WORK_DIR=session.work_dir,
KIMI_WORK_DIR_LS=ls_output,
KIMI_AGENTS_MD=agents_md or "",
),
denwa_renji=DenwaRenji(),
approval=Approval(yolo=yolo),
labor_market=LaborMarket(),
agents_md=agents_md or "",
)

def copy_for_fixed_subagent(self) -> Runtime:
"""Clone runtime for fixed subagent."""
return Runtime(
config=self.config,
llm=self.llm,
session=self.session,
builtin_args=self.builtin_args,
return replace(
self,
denwa_renji=DenwaRenji(), # subagent must have its own DenwaRenji
approval=self.approval,
labor_market=LaborMarket(), # fixed subagent has its own LaborMarket
)

def copy_for_dynamic_subagent(self) -> Runtime:
"""Clone runtime for dynamic subagent."""
return Runtime(
config=self.config,
llm=self.llm,
session=self.session,
builtin_args=self.builtin_args,
return replace(
self,
denwa_renji=DenwaRenji(), # subagent must have its own DenwaRenji
approval=self.approval,
labor_market=self.labor_market, # dynamic subagent shares LaborMarket with main agent
)

Expand Down Expand Up @@ -220,7 +211,8 @@ def _load_system_prompt(
builtin_args=builtin_args,
spec_args=args,
)
return string.Template(system_prompt).substitute(asdict(builtin_args), **args)
template = string.Template(system_prompt)
return template.safe_substitute(asdict(builtin_args), **args)


# TODO: maybe move to `KimiToolset`
Expand Down
82 changes: 80 additions & 2 deletions src/kimi_cli/soul/context.py
Original file line number Diff line number Diff line change
@@ -1,20 +1,24 @@
from __future__ import annotations

import json
from collections.abc import Sequence
from collections.abc import Callable, Sequence
from pathlib import Path

import aiofiles
import aiofiles.os
from kosong.message import Message

from kaos.path import KaosPath
from kimi_cli.soul.message import system
from kimi_cli.utils.history import filter_messages
from kimi_cli.utils.logging import logger
from kimi_cli.utils.path import next_available_rotation


class Context:
def __init__(self, file_backend: Path):
def __init__(self, file_backend: Path | KaosPath):
if isinstance(file_backend, KaosPath):
file_backend = file_backend.unsafe_to_local_path()
self._file_backend = file_backend
self._history: list[Message] = []
self._token_count: int = 0
Expand Down Expand Up @@ -154,6 +158,67 @@ async def clear(self):
self._token_count = 0
self._next_checkpoint_id = 0

async def reset_history(self, history: Sequence[Message]) -> None:
"""Replace the history with the provided messages and reset counters."""
self._history = list(history)
self._token_count = 0
self._next_checkpoint_id = 0

async with aiofiles.open(self._file_backend, "w", encoding="utf-8") as f:
for message in self._history:
await f.write(message.model_dump_json(exclude_none=True) + "\n")

async def filter_messages(self, keep: Callable[[Message], bool]) -> bool:
"""
Rewrite the history file, keeping messages that satisfy the predicate.
Checkpoint and usage records are preserved in place. Returns True when any message is
removed.
"""
if not self._file_backend.exists():
filtered_history = [message for message in self._history if keep(message)]
removed = len(filtered_history) != len(self._history)
self._history = filtered_history
return removed

self._token_count = 0
self._next_checkpoint_id = 0
temp_file = self._file_backend.with_suffix(self._file_backend.suffix + ".tmp")
removed = False
new_history: list[Message] = []

async with (
aiofiles.open(self._file_backend, encoding="utf-8") as src,
aiofiles.open(temp_file, "w", encoding="utf-8") as dst,
):
async for line in src:
if not line.strip():
continue

line_json = json.loads(line)
role = line_json.get("role")
if role == "_usage":
self._token_count = line_json["token_count"]
await dst.write(line)
continue
if role == "_checkpoint":
self._next_checkpoint_id = line_json["id"] + 1
await dst.write(line)
continue

message = Message.model_validate(line_json)
if keep(message):
new_history.append(message)
await dst.write(message.model_dump_json(exclude_none=True) + "\n")
else:
removed = True

self._history = new_history
if removed:
await aiofiles.os.replace(temp_file, self._file_backend)
else:
await aiofiles.os.remove(temp_file)
return removed

async def append_message(self, message: Message | Sequence[Message]):
logger.debug("Appending message(s) to context: {message}", message=message)
messages = message if isinstance(message, Sequence) else [message]
Expand All @@ -163,6 +228,19 @@ async def append_message(self, message: Message | Sequence[Message]):
for message in messages:
await f.write(message.model_dump_json(exclude_none=True) + "\n")

async def filter_history(self, keep: Callable[[Message], bool]) -> bool:
result = await filter_messages(
file_backend=self._file_backend,
history=self._history,
token_count=self._token_count,
next_checkpoint_id=self._next_checkpoint_id,
keep=keep,
)
self._history = result.history
self._token_count = result.token_count
self._next_checkpoint_id = result.next_checkpoint_id
return result.removed

async def update_token_count(self, token_count: int):
logger.debug("Updating token count in context: {token_count}", token_count=token_count)
self._token_count = token_count
Expand Down
Loading