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
52 changes: 52 additions & 0 deletions .github/workflows/shell-windows-pr.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
name: Shell PowerShell PR contract

on:
pull_request:
paths:
- ".github/workflows/shell-windows-pr.yml"
- "src/lingtai/adapters/shell*.py"
- "src/lingtai/adapters/windows/**"
- "src/lingtai/tools/bash/**"
- "src/lingtai/tools/registry.py"
- "tests/test_shell_pr1_contract.py"
- "tests/test_shell_windows_native.py"
workflow_dispatch:

permissions:
contents: read

jobs:
native-windows-shell:
name: Native Windows / PowerShell 7
runs-on: windows-latest
timeout-minutes: 15

steps:
- name: Check out repository
uses: actions/checkout@v4

- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.12"

- name: Verify PowerShell 7
shell: pwsh
run: |
if ($PSVersionTable.PSVersion.Major -lt 7) {
throw "PowerShell 7 is required"
}
pwsh -NoLogo -NoProfile -NonInteractive -Command '$PSVersionTable.PSVersion.ToString()'

- name: Install focused test dependencies
shell: pwsh
run: |
python -m pip install --disable-pip-version-check pytest
python -m pip install --disable-pip-version-check --no-deps -e .

- name: Run shell PR1 contract
shell: pwsh
env:
PYTHONUTF8: "1"
run: |
python -m pytest -q tests/test_shell_pr1_contract.py tests/test_shell_windows_native.py
10 changes: 5 additions & 5 deletions docs/references/claude-code-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ CustomAgent(Agent) — host's wrapper (subclass with domain logic)
| Tier | What | How added |
|------|------|-----------|
| **Intrinsics** | Kernel services (mail, system, psyche, soul). Mail provides a disk-backed mailbox: send, check, read, search, delete. Self-send (to own address) creates persistent notes that survive context compaction. System provides runtime inspection (`show`), synchronization (`nap` — timed pause), lifecycle (`refresh` — reload MCP, reset session), self-sleep (`sleep`), and karma/nirvana-gated actions (`lull`, `interrupt`, `suspend`, `cpr`, `nirvana`). Psyche provides pad (`edit`/`load` on `system/pad.md`), context management (`molt` for self-compaction with briefing), and naming (`name`/`set` — set true name once, `nickname` — mutable). `context_forget` is internal only (system-forced wipe/recovery). Covenant is a protected prompt section (no tool access). Capabilities can upgrade intrinsics via `override_intrinsic()`. | Built-in, always present |
| **Capabilities** | Composable capabilities (file [read/write/edit/glob/grep], knowledge, skills, bash, avatar, daemon, mcp, vision, web_search) | Declared at construction via `capabilities=` on Agent |
| **Capabilities** | Composable capabilities (file [read/write/edit/glob/grep], knowledge, skills, shell, avatar, daemon, mcp, vision, web_search) | Declared at construction via `capabilities=` on Agent |
| **MCP tools** | Domain tools from external MCP servers | Connected via `Agent.connect_mcp()` using `MCPClient` from `services/mcp.py`, or `add_tool()` in subclass constructors |

### Key Modules
Expand All @@ -149,7 +149,7 @@ CustomAgent(Agent) — host's wrapper (subclass with domain logic)
- **`lingtai.kernel.llm.service`** — `LLMService`. Adapter registry + factory, session registry, one-shot generation gateway, context compaction orchestration. Adapters register via `LLMService.register_adapter()`. Decoupled from config files — uses injected `key_resolver` and `provider_defaults`.
- **`src/lingtai/llm/interface_converters.py`** — Bidirectional converters between `ChatInterface` and provider-specific formats (Anthropic, OpenAI, Gemini).
- **`tools` (the five mandatory intrinsics)** — Each file exports `get_schema(lang)`, `get_description(lang)`, and `handle(agent, args)`. All five mandatory intrinsics (email, system, psyche, soul, notification) — now consolidated into the top-level `tools/` package and injected via `BaseAgent(intrinsics=lingtai.tools.registry.INTRINSICS)` — have self-contained handler logic — they receive the agent as an explicit parameter. Mail intrinsic provides a disk-backed mailbox with 5 actions: `send` (fire-and-forget with optional `delay` in seconds — all sends go through outbox → mailman thread → sent pipeline), `check` (list inbox with unread flags), `read` (by ID, non-destructive), `search` (regex), `delete`. Every send writes to `mailbox/outbox/`, spawns a daemon `_mailman` thread that sleeps for the delay, dispatches (filesystem write or self-send), then moves to `mailbox/sent/` with `sent_at` and `status`. Returns `{"status": "sent", "to": addr, "delay": N}` — the agent doesn't know dispatch outcome. `outbox/` (transient) and `sent/` (audit trail) are not exposed to the agent. Messages persist in `mailbox/inbox/{uuid}/message.json` — delivery is a filesystem write to the recipient's inbox directory. System intrinsic provides runtime inspection (`show`), synchronization (`nap` — timed pause, wakes on mail), lifecycle (`refresh` — reload MCP, reset session), self-sleep (`sleep`), and karma/nirvana-gated actions: `lull` (put other to sleep), `interrupt` (cancel other's turn), `suspend` (force other to SUSPENDED), `cpr` (resuscitate SUSPENDED agent), `nirvana` (permanently destroy). Psyche intrinsic provides pad (`edit`/`load` on `system/pad.md`), context management (`molt` for self-compaction with briefing), and naming (`name`/`set` — set true name once, `nickname` — mutable). `context_forget` is internal only (called by system-forced recovery). Covenant is injected at construction as a protected prompt section (no tool access).
- **`src/lingtai/tools/`** — Built-in capability modules export `setup(agent, **kwargs)`. The core default floor is read, write, edit, glob, grep (file I/O — also available as the `"file"` group), knowledge, skills, bash, avatar, daemon, and mcp; optional registry entries include vision and web_search. Durable private knowledge lives in the `knowledge` capability; the skill catalog lives in `skills`. Avatar (分身) spawns `Agent` peers with `name` and `type`; `type="deep"` copies character, pad, and knowledge, while the default shallow spawn starts from `init.json` only. Reasoning is sent as the first message.
- **`src/lingtai/tools/`** — Built-in capability modules export `setup(agent, **kwargs)`. The core default floor is read, write, edit, glob, grep (file I/O — also available as the `"file"` group), knowledge, skills, shell, avatar, daemon, and mcp; optional registry entries include vision and web_search. Durable private knowledge lives in the `knowledge` capability; the skill catalog lives in `skills`. Avatar (分身) spawns `Agent` peers with `name` and `type`; `type="deep"` copies character, pad, and knowledge, while the default shallow spawn starts from `init.json` only. Reasoning is sent as the first message.
- **`src/lingtai/tools/daemon/__init__.py`** — Daemon (神識) subagent capability. `DaemonManager` dispatches ephemeral `ChatSession` tool loops in threads via `ThreadPoolExecutor`. Each emanation gets a curated tool surface (parent capability handlers plus task-scoped MCP registrations, minus blacklist). Results persist in per-run daemon folders and terminal completion/failure is surfaced as compact system notifications. Actions: emanate (分), list (观), ask (问), check (察), reclaim (收). Configurable: `max_emanations`, `max_turns`, `timeout`.
- **`src/lingtai/cli.py`** — CLI entrypoint (`lingtai-agent run <working_dir>`). Reads `init.json` manifest, creates LLMService + Agent, starts the agent loop. Also `lingtai cpr <dir>` for resuscitating suspended agents.
- **`src/lingtai/network.py`** — `AgentNetwork` class. Three-layer topology discovery: avatar edges (from `delegates/ledger.jsonl`), contact edges (from `mailbox/contacts.json`), mail edges (from `mailbox/inbox/` + `mailbox/sent/`). Returns `AgentNode` and edge objects.
Expand All @@ -176,7 +176,7 @@ CustomAgent(Agent) — host's wrapper (subclass with domain logic)
| `grep` | `capabilities=["grep"]` | Search file contents by regex via FileIOService |
| `psyche` | intrinsic | Identity, pad, context molt, and naming. |
| `knowledge` | `capabilities=["knowledge"]` | Private durable knowledge across molts. Former durable-memory names `library` and `codex` are removed. |
| `bash` | `capabilities={"bash": {"policy_file": "p.json"}}` or `{"bash": {"yolo": True}}` | Shell command execution with policy |
| `shell` | `capabilities={"shell": {"policy_file": "p.json"}}` or `{"shell": {"yolo": True}}` | Shell command execution with the active dialect |
| `avatar` | `capabilities=["avatar"]` | Spawn avatar (分身) as fully independent detached process. Two params: `name` (required, true name) and `type` ('shallow' default — copies init.json only, 投胎; 'deep' — copies character/pad/knowledge too, 二重身). Each avatar gets its own working dir + `lingtai-agent run` process. Survives parent death. Reasoning = starting prompt in init.json. |
| `email` | `capabilities=["email"]` | Upgrades mail intrinsic with reply/reply_all, CC/BCC, contacts, sent/archive folders, archive (inbox→archive), delete (inbox/archive), delayed send (`delay`), private mode, and scheduled recurring sends (`schedule` sub-object with create/cancel/list). Routes dispatch through outbox → `_mailman(skip_sent=True)`. Sets `_mailbox_name="email box"`, `_mailbox_tool="email"`. Delegates inbox ops to mail intrinsic helpers. |
| `vision` | `capabilities=["vision"]` or `{"vision": {"vision_service": svc}}` | Image understanding (LLM multimodal or dedicated VisionService) |
Expand All @@ -195,11 +195,11 @@ CustomAgent(Agent) — host's wrapper (subclass with domain logic)
# Layer 2: Agent with capabilities
agent = Agent(
service=svc, agent_name="alice", working_dir="/agents/alice",
capabilities=["file", "vision", "web_search", "bash"], # "file" expands to read/write/edit/glob/grep
capabilities=["file", "vision", "web_search", "shell"], # "file" expands to read/write/edit/glob/grep
)
agent = Agent(
service=svc, agent_name="bob", working_dir="/agents/bob",
capabilities={"bash": {"policy_file": "p.json"}}, # dict form (with kwargs)
capabilities={"shell": {"policy_file": "p.json"}}, # dict form (with kwargs)
)

# Layer 3: Custom agent subclass
Expand Down
4 changes: 3 additions & 1 deletion src/lingtai/ANATOMY.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ related_files:
- src/lingtai/__main__.py
- src/lingtai/agent.py
- src/lingtai/adapters/posix/ANATOMY.md
- src/lingtai/adapters/bash.py
- src/lingtai/adapters/shell.py
- src/lingtai/adapters/shell_process.py
- src/lingtai/adapters/shell_state_lock.py
- src/lingtai/adapters/refresh_watcher.py
- src/lingtai/adapters/lifecycle_clock.py
- src/lingtai/auth/ANATOMY.md
Expand Down
6 changes: 5 additions & 1 deletion src/lingtai/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@
"UnknownToolError": ("lingtai.kernel.types", "UnknownToolError"),
# Tools
"setup_capability": ("lingtai.tools.registry", "setup_capability"),
"ShellManager": ("lingtai.tools.bash", "ShellManager"),
# Retained Python import compatibility; the registered capability/tool is
# only ``shell``.
"BashManager": ("lingtai.tools.bash", "BashManager"),
"AvatarManager": ("lingtai.tools.avatar", "AvatarManager"),
"EmailManager": ("lingtai.tools.email", "EmailManager"),
Expand Down Expand Up @@ -74,7 +77,7 @@
from lingtai.kernel.state import AgentState
from lingtai.kernel.types import UnknownToolError
from lingtai.tools.avatar import AvatarManager
from lingtai.tools.bash import BashManager
from lingtai.tools.bash import BashManager, ShellManager
from lingtai.tools.email import EmailManager
from lingtai.tools.registry import setup_capability

Expand Down Expand Up @@ -138,6 +141,7 @@ def __dir__() -> list[str]:
"UnknownToolError",
# Capabilities
"setup_capability",
"ShellManager",
"BashManager",
"AvatarManager",
"EmailManager",
Expand Down
2 changes: 1 addition & 1 deletion src/lingtai/adapters/bash.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
"""Composition selector for Bash shell-language adapters."""
"""Compatibility selector for the retained internal Bash implementation."""
from __future__ import annotations

import os
Expand Down
10 changes: 3 additions & 7 deletions src/lingtai/adapters/bash_process.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,9 @@
"""Fail-loud Bash asynchronous process composition selector."""
"""Compatibility selector for the retained internal Bash package."""
from __future__ import annotations

import os

from lingtai.tools.bash._async_process import BashAsyncProcessPort
from .posix.bash_process import PosixBashAsyncProcessAdapter
from .shell_process import select_shell_async_process


def select_bash_async_process() -> BashAsyncProcessPort:
if os.name == "posix":
return PosixBashAsyncProcessAdapter()
raise NotImplementedError(f"Bash async process supervision is unsupported on {os.name!r}")
return select_shell_async_process()
10 changes: 3 additions & 7 deletions src/lingtai/adapters/bash_state_lock.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,9 @@
"""Fail-loud Bash async state-lock composition selector."""
"""Compatibility selector for the retained internal Bash package."""
from __future__ import annotations

import os

from lingtai.tools.bash._state_lock import BashStateLockPort
from .posix.bash_state_lock import PosixBashStateLockAdapter
from .shell_state_lock import select_shell_state_lock


def select_bash_state_lock() -> BashStateLockPort:
if os.name == "posix":
return PosixBashStateLockAdapter()
raise NotImplementedError(f"Bash async state locking is unsupported on {os.name!r}")
return select_shell_state_lock()
59 changes: 59 additions & 0 deletions src/lingtai/adapters/shell.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
"""Composition selector for the canonical ``shell`` capability.

The implementation package remains ``lingtai.tools.bash`` for PR1 durable and
packaging compatibility. Platform identity belongs at this outer selector.
"""
from __future__ import annotations

import os
import platform

from lingtai.tools.bash._shell_dialect import ShellDialect
from .posix.bash import PosixBashDialect


def _one_line(value: object) -> str:
"""Normalize host metadata for one-line agent-facing tool prose."""
return " ".join(str(value or "").split())


def describe_host_os() -> str:
"""Return a truthful human-readable host OS name and version."""
system = _one_line(platform.system())
if system == "Darwin":
version = _one_line(platform.mac_ver()[0])
if version:
return f"macOS {version}"
kernel = _one_line(platform.release())
return f"macOS (Darwin kernel {kernel})" if kernel else "macOS"

if system == "Linux":
try:
os_release = platform.freedesktop_os_release()
except OSError:
os_release = {}
pretty_name = _one_line(os_release.get("PRETTY_NAME"))
if pretty_name:
return pretty_name
name = _one_line(os_release.get("NAME")) or "Linux"
version = _one_line(os_release.get("VERSION_ID") or os_release.get("VERSION"))
return f"{name} {version}".strip()

if system == "Windows":
release = _one_line(platform.release())
version = _one_line(platform.version())
label = f"Windows {release}".strip()
return f"{label} ({version})" if version and version != release else label

system = system or _one_line(os.name) or "unknown"
release = _one_line(platform.release())
return f"{system} {release}".strip()


def select_shell_dialect() -> ShellDialect:
if os.name == "posix":
return PosixBashDialect()
if os.name == "nt":
from .windows.powershell import PowerShellDialect
return PowerShellDialect()
raise NotImplementedError(f"shell dialect is unsupported on platform {os.name!r}")
18 changes: 18 additions & 0 deletions src/lingtai/adapters/shell_process.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
"""Composition selector for the canonical shell async process Port."""
from __future__ import annotations

import os

from lingtai.tools.bash._async_process import BashAsyncProcessPort


def select_shell_async_process() -> BashAsyncProcessPort:
if os.name == "posix":
# Keep the POSIX-only process implementation behind platform selection;
# its transitive imports include selectors that are not Windows-safe.
from .posix.bash_process import PosixBashAsyncProcessAdapter
return PosixBashAsyncProcessAdapter()
if os.name == "nt":
from .windows.powershell_process import WindowsShellAsyncProcessAdapter
return WindowsShellAsyncProcessAdapter()
raise NotImplementedError(f"shell async process supervision is unsupported on {os.name!r}")
18 changes: 18 additions & 0 deletions src/lingtai/adapters/shell_state_lock.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
"""Composition selector for the canonical shell state-lock Port."""
from __future__ import annotations

import os

from lingtai.tools.bash._state_lock import BashStateLockPort


def select_shell_state_lock() -> BashStateLockPort:
if os.name == "posix":
# Keep the POSIX-only fcntl import behind platform selection so loading
# the portable selector on Windows reaches its native adapter instead.
from .posix.bash_state_lock import PosixBashStateLockAdapter
return PosixBashStateLockAdapter()
if os.name == "nt":
from .windows.powershell_state_lock import WindowsShellStateLockAdapter
return WindowsShellStateLockAdapter()
raise NotImplementedError(f"shell async state locking is unsupported on {os.name!r}")
1 change: 1 addition & 0 deletions src/lingtai/adapters/windows/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""Native Windows adapters for the canonical shell capability."""
Loading
Loading