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
1 change: 1 addition & 0 deletions MANIFEST.in
Original file line number Diff line number Diff line change
Expand Up @@ -63,3 +63,4 @@ graft src/lingtai/tools/bash/manual
graft src/lingtai/tools/avatar/manual
graft src/lingtai/tools/daemon/manual
graft src/lingtai/tools/mcp/manual
graft src/lingtai/tools/web_search/manual
1 change: 1 addition & 0 deletions src/lingtai/tools/ANATOMY.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ composes them.
| `i18n/` | `en/zh/wen` string catalogs for every tool; registers into the kernel i18n cache via `register_strings` on import |
| `_catalog.py` | Shared scan/manifest helpers for `knowledge` + `skills` |
| `_file_paths.py` | `resolve_workdir_path` — shared by the five file tools |
| `_manual.py` | `load_installed_manual` — read-only loader for tools exposing an already-installed intrinsic manual skill |
| `_media_host.py`, `_zhipu_mode.py` | Provider-host / z.ai-mode helpers for `vision` + `web_search` |

**Tool sub-packages:** `email/`, `system/`, `psyche/`, `soul/`, `notification/`
Expand Down
29 changes: 29 additions & 0 deletions src/lingtai/tools/_manual.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
"""Shared loader for manuals installed in an agent's intrinsic skill catalog."""
from __future__ import annotations


def load_installed_manual(agent, skill_name: str) -> dict:
"""Return one installed intrinsic manual without mutating agent state."""
manual_path = (
agent._working_dir
/ ".library"
/ "intrinsic"
/ "capabilities"
/ skill_name
/ "SKILL.md"
)
if not manual_path.is_file():
return {
"status": "degraded",
"manual": "",
"manual_path": str(manual_path),
"error": (
f"{skill_name} manual missing — initializer may have failed or "
"capability not installed correctly"
),
}
return {
"status": "ok",
"manual": manual_path.read_text(encoding="utf-8"),
"manual_path": str(manual_path),
}
11 changes: 7 additions & 4 deletions src/lingtai/tools/bash/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
from typing import TYPE_CHECKING

from ._shell_dialect import ShellDialect, ShellInvocation, extract_posix_commands
from .._manual import load_installed_manual

from ._async_supervisor import (
load_state,
Expand Down Expand Up @@ -234,7 +235,7 @@ def get_description(
"Returns exit_code, stdout, stderr, plus ok (bool) and command_status ('success'/'failed'). IMPORTANT: top-level status stays 'ok' even when the command FAILS — it only means the shell ran. "
"Always check exit_code/ok and read the warning field (it names nonzero exits, Python tracebacks, and missing modules); never assume success from status alone. "
"Avoid broad recursive scans (find … -name, rglob, os.walk, glob('**')) — they time out; prefer `rg --files`. Parse JSONL line-by-line, not as one JSON blob. "
"Supports async mode (async=true → job_id, then poll/cancel). Before using this tool, read the `shell-manual` skill — it covers async hygiene and advanced usage; no exceptions.")
"Supports async mode (async=true → job_id, then poll/cancel). Call shell(action='manual') to return the installed shell-manual skill. Before ordinary shell work, read that manual — it covers async hygiene and advanced usage; no exceptions.")


def get_schema(lang: str = "en") -> dict:
Expand All @@ -243,8 +244,8 @@ def get_schema(lang: str = "en") -> dict:
"properties": {
"action": {
"type": "string",
"enum": ["run", "poll", "cancel"],
"description": "Action to perform: 'run' (default) executes a command, 'poll' checks async job status, 'cancel' kills an async job",
"enum": ["run", "poll", "cancel", "manual"],
"description": "Action to perform: 'run' (default) executes a command, 'poll' checks async job status, 'cancel' kills an async job, 'manual' returns the installed shell-manual skill",
"default": "run",
},
"command": {
Expand Down Expand Up @@ -280,7 +281,7 @@ def get_schema(lang: str = "en") -> dict:
"default": False,
},
},
"required": ["reminder"], # command/job_id are enforced per action; handler defaults omitted reminder for runtime compatibility
"required": [], # action-specific inputs are enforced by the handler; manual needs none
}


Expand Down Expand Up @@ -479,6 +480,8 @@ def _validate_reminder(value) -> tuple[float | None, dict | None]:

def handle(self, args: dict) -> dict:
action = args.get("action", "run")
if action == "manual":
return load_installed_manual(self._agent, "shell")
if action == "poll":
return self._handle_poll(args)
if action == "cancel":
Expand Down
9 changes: 6 additions & 3 deletions src/lingtai/tools/daemon/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
from lingtai.kernel.tool_executor import ToolExecutor
from lingtai.kernel.meta_block import attach_daemon_agent_meta
from lingtai.kernel.trace_redaction import redact_text
from .._manual import load_installed_manual
from lingtai.adapters.posix.process_identity import (
process_identity,
process_identity_matches,
Expand Down Expand Up @@ -1005,7 +1006,7 @@ def __getattr__(self, n):


def get_description(lang: str = "en") -> str:
return 'Daemon (神識) — delegate work to ephemeral subagents for context isolation. Each is a disposable LLM session sharing your working directory, retaining no memory after completion. Use for noisy work where you only need the conclusion. Results truncated to ~2000 chars — instruct the emanation to write detailed output to a file. Actions: emanate (dispatch), list (status), ask (follow-up), check (inspect recent events), reclaim (kill all). Every terminal outcome is push-notified exactly once — done, failed, cancelled, or timed out — so after you dispatch you can safely go idle and wait for the notification; do not poll for "is it done". The notification carries the daemon id, terminal status, task summary, and the result/error path; act on it with daemon(action="check", id=...). LingTai daemons also receive compact; compact(action="manual") is read-only procedures, while explicit compact(action="run", _reason="...") is the repeatable sole-call context reset; action is required. Before using this tool, read the `daemon-manual` skill — it covers inspection patterns, polling cadence, preset/capability inheritance, and compact procedures; no exceptions.'
return 'Daemon (神識) — delegate work to ephemeral subagents for context isolation. Each is a disposable LLM session sharing your working directory, retaining no memory after completion. Use for noisy work where you only need the conclusion. Results truncated to ~2000 chars — instruct the emanation to write detailed output to a file. Actions: emanate (dispatch), list (status), ask (follow-up), check (inspect recent events), reclaim (kill all), manual (return the installed daemon-manual skill). Every terminal outcome is push-notified exactly once — done, failed, cancelled, or timed out — so after you dispatch you can safely go idle and wait for the notification; do not poll for "is it done". The notification carries the daemon id, terminal status, task summary, and the result/error path; act on it with daemon(action="check", id=...). LingTai daemons also receive compact; compact(action="manual") is read-only procedures, while explicit compact(action="run", _reason="...") is the repeatable sole-call context reset; action is required. Before using this tool, read the `daemon-manual` skill — it covers inspection patterns, polling cadence, preset/capability inheritance, and compact procedures; no exceptions.'


def get_schema(lang: str = "en") -> dict:
Expand All @@ -1014,8 +1015,8 @@ def get_schema(lang: str = "en") -> dict:
"properties": {
"action": {
"type": "string",
"enum": ["emanate", "list", "ask", "check", "reclaim"],
"description": "Action to perform: 'emanate' (dispatch subagents), 'list' (show status), 'ask' (follow-up message), 'check' (read recent events of one emanation), 'reclaim' (kill all). LingTai emanations additionally receive compact(action='manual') for read-only procedures or explicit compact(action='run', _reason='...') for the non-terminal sole-call reset; action is required.",
"enum": ["emanate", "list", "ask", "check", "reclaim", "manual"],
"description": "Action to perform: 'emanate' (dispatch subagents), 'list' (show status), 'ask' (follow-up message), 'check' (read recent events of one emanation), 'reclaim' (kill all), 'manual' (return the installed daemon-manual skill). LingTai emanations additionally receive compact(action='manual') for read-only compaction procedures or explicit compact(action='run', _reason='...') for the non-terminal sole-call reset; action is required.",
},
"tasks": {
"type": "array",
Expand Down Expand Up @@ -1527,6 +1528,8 @@ def _terminal_notification_text_from_state(

def handle(self, args: dict) -> dict:
action = args.get("action")
if action == "manual":
return load_installed_manual(self._agent, "daemon")
backend = _normalize_backend(args.get("backend", "lingtai"))
if action == "emanate":
return self._handle_emanate(
Expand Down
22 changes: 15 additions & 7 deletions src/lingtai/tools/edit/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,25 +7,27 @@
from typing import TYPE_CHECKING

from .._file_paths import resolve_workdir_path
from .._manual import load_installed_manual

if TYPE_CHECKING:
from lingtai.kernel.base_agent import BaseAgent


def get_description(lang: str = "en") -> str:
return 'Replace an exact string in a file. Fails if old_string is not found or is ambiguous.'
return "Replace an exact string in a file. Fails if old_string is not found or is ambiguous. Call edit(action='manual') to return the installed file-manual skill."


def get_schema(lang: str = "en") -> dict:
return {
"type": "object",
"properties": {
"file_path": {"type": "string", "description": 'Absolute path to the file to edit'},
"old_string": {"type": "string", "description": 'The exact text to find and replace'},
"new_string": {"type": "string", "description": 'The replacement text'},
"action": {"type": "string", "enum": ["manual"], "description": "Use action='manual' to return the installed file-manual skill without editing a file."},
"file_path": {"type": "string", "description": "Absolute path to the file to edit. Required for ordinary edits; omit for action='manual'."},
"old_string": {"type": "string", "description": "The exact text to find and replace. Required for ordinary edits; omit for action='manual'."},
"new_string": {"type": "string", "description": "The replacement text. Required for ordinary edits; omit for action='manual'."},
"replace_all": {"type": "boolean", "description": 'Replace all occurrences', "default": False},
},
"required": ["file_path", "old_string", "new_string"],
"required": [],
}


Expand All @@ -34,12 +36,18 @@ def setup(agent: "BaseAgent") -> None:
"""Set up the edit capability on an agent."""

def handle_edit(args: dict) -> dict:
if args.get("action") == "manual":
return load_installed_manual(agent, "file-manual")
path = args.get("file_path", "")
if not path:
return {"status": "error", "message": "file_path is required"}
if "old_string" not in args:
return {"status": "error", "message": "old_string is required"}
if "new_string" not in args:
return {"status": "error", "message": "new_string is required"}
path = resolve_workdir_path(agent, path)
old = args.get("old_string", "")
new = args.get("new_string", "")
old = args["old_string"]
new = args["new_string"]
replace_all = args.get("replace_all", False)
try:
content = agent._file_io.read(path)
Expand Down
3 changes: 3 additions & 0 deletions src/lingtai/tools/email/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@


from lingtai.kernel.notifications import register_generic_dismiss_guard
from .._manual import load_installed_manual

register_generic_dismiss_guard(
"email",
Expand Down Expand Up @@ -83,6 +84,8 @@ def handle(agent, args: dict) -> dict:
calls handle() before boot() in a test harness), return a clear error.
"""
action = args.get("action")
if action == "manual":
return load_installed_manual(agent, "email")
if action == "unread":
return {
"status": "error",
Expand Down
5 changes: 3 additions & 2 deletions src/lingtai/tools/email/schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@


def get_description(lang: str = "en") -> str:
return 'LingTai email protocol within your .lingtai/ network — NOT real internet email (for Gmail/Outlook use the imap tool). Addresses are bare paths under .lingtai/ with no @ signs (e.g. human for the operator). Reply discipline: always reply on the channel the message arrived on; prefer reply over send. Never reply via text output — that is your private diary, not a comms channel. Always address people by sender_nickname if set, else sender_name. For detailed usage, see the email-manual skill.'
return "LingTai email protocol within your .lingtai/ network — NOT real internet email (for Gmail/Outlook use the imap tool). Addresses are bare paths under .lingtai/ with no @ signs (e.g. human for the operator). Reply discipline: always reply on the channel the message arrived on; prefer reply over send. Never reply via text output — that is your private diary, not a comms channel. Always address people by sender_nickname if set, else sender_name. Call email(action='manual') to return the installed email-manual skill."


def get_schema(lang: str = "en") -> dict:
Expand All @@ -18,8 +18,9 @@ def get_schema(lang: str = "en") -> dict:
"send", "check", "read", "dismiss", "reply", "reply_all",
"search", "archive", "delete",
"contacts", "add_contact", "remove_contact", "edit_contact",
"manual",
],
"description": 'send: send with optional cc/bcc (requires address, message; message body max 50,000 chars because unread bodies are injected in full into persistent notifications). check: list mailbox with preview of each email (up to 500 chars). read: fetch inbox emails by ID list (email_id=[id1, id2, ...]) AND marks each as read; ordinary unread content is already injected in notification_persistent.email, so prefer dismiss when you only need to clear handled mail. dismiss: same read-state effect as read but returns no bodies — preferred after handling content visible in persistent notification. reply: reply to email (requires email_id, message). reply_all: reply to all recipients. search: regex search mailbox. archive/delete: move/remove from inbox or archive. contacts/add_contact/remove_contact/edit_contact manage contacts.',
"description": 'send: send with optional cc/bcc (requires address, message; message body max 50,000 chars because unread bodies are injected in full into persistent notifications). check: list mailbox with preview of each email (up to 500 chars). read: fetch inbox emails by ID list (email_id=[id1, id2, ...]) AND marks each as read; ordinary unread content is already injected in notification_persistent.email, so prefer dismiss when you only need to clear handled mail. dismiss: same read-state effect as read but returns no bodies — preferred after handling content visible in persistent notification. reply: reply to email (requires email_id, message). reply_all: reply to all recipients. search: regex search mailbox. archive/delete: move/remove from inbox or archive. contacts/add_contact/remove_contact/edit_contact manage contacts. manual returns the installed email-manual skill without reading or changing mailbox state.',
},
"address": {
"oneOf": [
Expand Down
10 changes: 7 additions & 3 deletions src/lingtai/tools/glob/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,24 +7,26 @@
from typing import TYPE_CHECKING

from .._file_paths import resolve_workdir_path
from .._manual import load_installed_manual

if TYPE_CHECKING:
from lingtai.kernel.base_agent import BaseAgent


def get_description(lang: str = "en") -> str:
return "Find files matching a glob pattern. Use '**/' for recursive search (e.g. '**/*.py' finds all Python files). Returns sorted list of matching file paths."
return "Find files matching a glob pattern. Use '**/' for recursive search (e.g. '**/*.py' finds all Python files). Returns sorted list of matching file paths. Call glob(action='manual') to return the installed file-manual skill."


def get_schema(lang: str = "en") -> dict:
return {
"type": "object",
"properties": {
"pattern": {"type": "string", "description": "Glob pattern (e.g., '**/*.py')"},
"action": {"type": "string", "enum": ["manual"], "description": "Use action='manual' to return the installed file-manual skill without searching."},
"pattern": {"type": "string", "description": "Glob pattern (e.g., '**/*.py'). Required for ordinary searches; omit for action='manual'."},
"path": {"type": "string", "description": 'Directory to search in'},
"summary": {"type": "boolean", "description": 'Optional. Default false. When true, this tool runs normally and the raw result is preserved in the durable log (retrievable by tool_call_id), but before the result enters your context it is replaced by an LLM-generated summary driven by your `reasoning` field — so make `reasoning` specific about what to retain. Set true only when the output is expected to be large (>10k chars) and you do NOT need the exact raw text. Leave false when you need exact line/file/diff/stderr text. The summary is non-canonical; if the raw exceeds 500,000 chars no summary is generated and you get a refusal pointing at the preserved raw.', "default": False},
},
"required": ["pattern"],
"required": [],
}


Expand All @@ -33,6 +35,8 @@ def setup(agent: "BaseAgent") -> None:
"""Set up the glob capability on an agent."""

def handle_glob(args: dict) -> dict:
if args.get("action") == "manual":
return load_installed_manual(agent, "file-manual")
pattern = args.get("pattern", "")
if not pattern:
return {"status": "error", "message": "pattern is required"}
Expand Down
Loading
Loading