agent-cli-lib 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,48 @@
1
+ """agent-cli-lib: agent CLI 基座 (opencode / pi / hermes) 的统一封装."""
2
+
3
+ from agent_cli_lib.api import (
4
+ AgentResult,
5
+ AgentRunner,
6
+ SessionInfo,
7
+ export_session,
8
+ load_session,
9
+ resolve_prompt,
10
+ resume,
11
+ run,
12
+ )
13
+ from agent_cli_lib.core.runner import get_backend
14
+ from agent_cli_lib.errors import (
15
+ AcpProtocolError,
16
+ AgentCliError,
17
+ BackendLaunchError,
18
+ BackendNotFoundError,
19
+ BackendRunError,
20
+ PromptError,
21
+ RemovedAPIError,
22
+ RunCancelledError,
23
+ SessionError,
24
+ )
25
+
26
+ __version__ = "0.1.0"
27
+
28
+ __all__ = [
29
+ "AcpProtocolError",
30
+ "AgentCliError",
31
+ "AgentResult",
32
+ "AgentRunner",
33
+ "BackendLaunchError",
34
+ "BackendNotFoundError",
35
+ "BackendRunError",
36
+ "PromptError",
37
+ "RemovedAPIError",
38
+ "RunCancelledError",
39
+ "SessionError",
40
+ "SessionInfo",
41
+ "__version__",
42
+ "export_session",
43
+ "get_backend",
44
+ "load_session",
45
+ "resolve_prompt",
46
+ "resume",
47
+ "run",
48
+ ]
@@ -0,0 +1,25 @@
1
+ """API 签名层(公开门面):全部公开方法的签名、docstring 使用提示、废弃/移除注解。
2
+
3
+ 约定:本层只聚合不实现——每个签名一行委托 core/backends,细节按职能归位到成员模块。
4
+ 标记用法:from agent_cli_lib.api import deprecated, removed
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from agent_cli_lib.api.decorators import deprecated, removed
10
+ from agent_cli_lib.api.prompt import resolve_prompt
11
+ from agent_cli_lib.api.runner import AgentResult, AgentRunner, export_session, resume, run
12
+ from agent_cli_lib.api.session import SessionInfo, load_session
13
+
14
+ __all__ = [
15
+ "AgentResult",
16
+ "AgentRunner",
17
+ "SessionInfo",
18
+ "deprecated",
19
+ "export_session",
20
+ "load_session",
21
+ "removed",
22
+ "resolve_prompt",
23
+ "resume",
24
+ "run",
25
+ ]
@@ -0,0 +1,37 @@
1
+ """API 注解工具:废弃/移除标记(签名层的核心能力)。
2
+
3
+ - deprecated:typing_extensions 的 PEP 702 实现(Python 3.13 起进标准库 warnings.deprecated)。
4
+ 一个装饰器三合一:静态诊断(ty/pyright/pylance 报 Deprecated、IDE 划删除线)、
5
+ 运行时发 DeprecationWarning、消息本身承载迁移/用法提示。
6
+ - removed:签名保留但调用即抛 RemovedAPIError,hint 指向替代用法(彻底丢弃档)。
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import functools
12
+ from collections.abc import Callable
13
+ from typing import NoReturn, TypeVar, cast
14
+
15
+ from typing_extensions import deprecated
16
+
17
+ from agent_cli_lib.errors import RemovedAPIError
18
+
19
+ __all__ = ["deprecated", "removed"]
20
+
21
+ F = TypeVar("F", bound=Callable[..., object])
22
+
23
+
24
+ def removed(hint: str) -> Callable[[F], F]:
25
+ """标记 API 已彻底移除:签名保留(静态可见),调用即抛 RemovedAPIError。
26
+
27
+ hint 写替代用法,例如:@removed("改用 run(prompt, model=...),见 docs/api.md")
28
+ """
29
+
30
+ def decorator(func: F) -> F:
31
+ @functools.wraps(func)
32
+ def wrapper(*args: object, **kwargs: object) -> NoReturn:
33
+ raise RemovedAPIError(getattr(func, "__qualname__", str(func)), hint)
34
+
35
+ return cast("F", wrapper)
36
+
37
+ return decorator
@@ -0,0 +1,10 @@
1
+ """prompt 门面(签名层):resolve_prompt 的公开出口。
2
+
3
+ 实现在 core.prompt(文件/字符串判别、大小上限等细节收纳在那里),本层只聚合。
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ from agent_cli_lib.core.prompt import resolve_prompt
9
+
10
+ __all__ = ["resolve_prompt"]
@@ -0,0 +1,85 @@
1
+ """运行门面(签名层):公开签名 + 使用提示,一行委托 core(聚合层约定,不碰实现细节)。
2
+
3
+ 需要跨多次调用 cancel() 或复用会话上下文时,直接用 AgentRunner 实例。
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ from collections.abc import Callable
9
+
10
+ from acp import SessionNotification
11
+
12
+ from agent_cli_lib.core.runner import AgentResult, AgentRunner
13
+ from agent_cli_lib.core.session import SessionInfo
14
+
15
+ __all__ = ["AgentResult", "AgentRunner", "export_session", "resume", "run"]
16
+
17
+
18
+ def run(
19
+ prompt: str,
20
+ *,
21
+ backend: str = "opencode",
22
+ model: str | None = None,
23
+ workdir: str | None = None,
24
+ export_to: str | None = None,
25
+ transport: str = "auto",
26
+ on_event: Callable[[SessionNotification], None] | None = None,
27
+ ) -> AgentResult:
28
+ """一次性执行 agent 任务,无需管理 runner 生命周期。
29
+
30
+ export_to:任务完成后把会话导出文件落到该路径(相对路径按调用方进程 cwd 解析,
31
+ 父目录自动创建)。导出失败不抛异常,降级为 AgentResult.export_error;
32
+ 成功时 AgentResult.export_path 为落盘绝对路径。各后端导出物可用性:
33
+ opencode 可 `opencode import` 恢复;pi 的 jsonl 可 `pi --session <path>` 恢复;
34
+ hermes 的 jsonl 仅归档(无 import,恢复只能 session id)。
35
+
36
+ Examples:
37
+ >>> from agent_cli_lib.api import run
38
+ >>> result = run("修复登录 bug", backend="opencode", workdir="/my/project",
39
+ ... export_to="/tmp/sess.json")
40
+ >>> result.session_id, result.output, result.export_path
41
+ """
42
+ return AgentRunner(backend=backend, workdir=workdir, transport=transport).run(
43
+ prompt, model=model, export_to=export_to, on_event=on_event
44
+ )
45
+
46
+
47
+ def resume(
48
+ session: str,
49
+ *,
50
+ backend: str = "opencode",
51
+ model: str | None = None,
52
+ prompt: str = "",
53
+ workdir: str | None = None,
54
+ transport: str = "auto",
55
+ on_event: Callable[[SessionNotification], None] | None = None,
56
+ ) -> AgentResult:
57
+ """按 session id 或 session 文件路径恢复会话。
58
+
59
+ prompt 必填性:ACP 路径(opencode 默认路由)所有 backend 必须带 prompt;
60
+ hermes CLI 路径必须带 prompt;pi CLI 路径可省略。
61
+
62
+ Examples:
63
+ >>> from agent_cli_lib.api import resume
64
+ >>> result = resume("sess-abc", prompt="继续优化", backend="pi")
65
+ """
66
+ return AgentRunner(backend=backend, workdir=workdir, transport=transport).resume(
67
+ session, model=model, prompt=prompt, on_event=on_event
68
+ )
69
+
70
+
71
+ def export_session(
72
+ session_id: str,
73
+ target: str,
74
+ *,
75
+ backend: str = "opencode",
76
+ workdir: str | None = None,
77
+ ) -> SessionInfo:
78
+ """调用后端原生导出命令,把会话数据落到 target 文件。
79
+
80
+ Examples:
81
+ >>> from agent_cli_lib.api import export_session
82
+ >>> info = export_session("sess-abc", "out.json", backend="opencode")
83
+ >>> info.file_path
84
+ """
85
+ return AgentRunner(backend=backend, workdir=workdir).export_session(session_id, target)
@@ -0,0 +1,10 @@
1
+ """会话门面(签名层):SessionInfo 与 load_session 的公开出口。
2
+
3
+ 实现在 core.session(路径判别/校验细节收纳在那里),本层只聚合。
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ from agent_cli_lib.core.session import SessionInfo, load_session
9
+
10
+ __all__ = ["SessionInfo", "load_session"]
@@ -0,0 +1 @@
1
+ """agent CLI 后端适配层 (opencode / pi / hermes)."""
@@ -0,0 +1,84 @@
1
+ """后端统一协议。
2
+
3
+ 每个 agent CLI 基座实现为一个小型充血模型:自己负责
4
+ - 构造命令行 argv(把统一的 prompt/session/workdir 翻译成自己的 flag)
5
+ - 解析进程输出(提取最终文本 + session id,含各家 stdout/stderr 的格式差异)
6
+ - 落盘会话导出文件(把会话以原生导出格式写到 target)
7
+
8
+ AgentRunner 只调用 build_argv / parse_output / export_session_file,不关心任何 backend 细节。
9
+ 传输层扩展(v8 设计):`acp` 属性声明 ACP 可用性(None = 整体走 CLI 子进程路径);
10
+ `session_exists` 是 CLI 路径 resume 的廉价存在性探测(ACP 路径不需要,协议错误自带语义)。
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ from dataclasses import dataclass
16
+ from typing import NamedTuple, Protocol
17
+
18
+
19
+ @dataclass
20
+ class RunRequest:
21
+ """一次运行的统一输入(backend 把它翻译成自己的 argv / ACP 请求)。"""
22
+
23
+ prompt: str
24
+ workdir: str | None = None
25
+ model: str | None = None
26
+ session: str | None = None
27
+
28
+
29
+ @dataclass
30
+ class RunOutput:
31
+ """一次运行的统一输出(从进程 stdout/stderr 或 ACP 响应解析出来)。"""
32
+
33
+ text: str
34
+ session_id: str = ""
35
+
36
+
37
+ class AcpSpec(NamedTuple):
38
+ """backend 的 ACP 能力声明(None 表示无 ACP / 整体走 CLI)。
39
+
40
+ - command:启动 ACP agent 的完整命令(含已解析的 binary 路径)
41
+ - cwd_bound:resume 是否协议级绑定会话原目录(opencode v2 实测 True)。
42
+ 说明:跨目录 cwd 错误的"可读翻译"由 wire 层按协议消息内容无条件实现
43
+ (不依赖本字段);本字段是 spec 数据,供测试断言与未来前置校验使用
44
+ """
45
+
46
+ command: tuple[str, ...]
47
+ cwd_bound: bool = False
48
+
49
+
50
+ class Backend(Protocol):
51
+ """agent CLI 基座协议(name/binary 为只读成员:类属性或 property 均可满足)。"""
52
+
53
+ @property
54
+ def name(self) -> str: ...
55
+
56
+ @property
57
+ def binary(self) -> str: ...
58
+
59
+ def build_argv(self, req: RunRequest) -> list[str]:
60
+ """把统一请求翻译成该 CLI 的命令行参数列表(不含二进制本身)。"""
61
+ ...
62
+
63
+ def parse_output(self, stdout: str, stderr: str) -> RunOutput:
64
+ """从进程 stdout/stderr 提取最终文本与 session id(各家格式差异收纳于此)。"""
65
+ ...
66
+
67
+ def export_session_file(self, session_id: str, target: str) -> None:
68
+ """把会话以该 CLI 的原生导出格式写到 target 文件(target 为已存在的绝对路径,
69
+ 父目录已由调用方创建);失败抛 AgentCliError。
70
+ """
71
+ ...
72
+
73
+ def session_exists(self, session_id: str) -> bool:
74
+ """廉价探测会话是否存在(CLI 路径 resume 前置校验用)。
75
+
76
+ 默认恒 True(多数 CLI 对未知 id 自己报错,探测是浪费);存在
77
+ "未知 id 静默新建"语义的 backend(opencode v2 CLI 兜底)真实探测。
78
+ """
79
+ ...
80
+
81
+ @property
82
+ def acp(self) -> AcpSpec | None:
83
+ """ACP 能力声明;None = 该 backend 不走 ACP 传输(pi / hermes 0.19.0)。"""
84
+ ...
@@ -0,0 +1,5 @@
1
+ """hermes 后端适配(占位骨架,待实现)."""
2
+
3
+ from agent_cli_lib.backends.hermes.backend import HermesBackend
4
+
5
+ __all__ = ["HermesBackend"]
@@ -0,0 +1,85 @@
1
+ """hermes CLI 封装实现。
2
+
3
+ 关键映射(实测 0.19.0):
4
+ - 一次性运行:hermes chat -q <msg> -Q(-Q 非交互退出)
5
+ - 工作目录:通过 subprocess cwd
6
+ - 恢复会话:--resume <session_id>
7
+ - session id:只在 stderr 尾部以 "session_id: <id>" 行出现,stdout 即正文
8
+ - 导出:hermes sessions export --format jsonl(CLI 自己写 target);导出物是归档,
9
+ hermes 无 import 命令,恢复只能 --resume <session_id>
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import re
15
+ import subprocess
16
+ from pathlib import Path
17
+
18
+ from agent_cli_lib.backends.base import AcpSpec, RunOutput, RunRequest
19
+ from agent_cli_lib.errors import ProcessResult, SessionError, which_or_raise
20
+
21
+ # hermes 把 session id 打在 stderr 尾部,形如 "session_id: 20261104_061903_4b2d3b"
22
+ _SESSION_LINE_RE = re.compile(r"^session(?:_id)?\s*[:=]\s*([A-Za-z0-9_\-]+)", re.IGNORECASE)
23
+
24
+
25
+ class HermesBackend:
26
+ @property
27
+ def acp(self) -> AcpSpec | None:
28
+ """hermes 0.19.0 ACP resume broken,整体走 CLI 子进程路径。"""
29
+ return None
30
+
31
+ def session_exists(self, session_id: str) -> bool:
32
+ """CLI 对未知 id 自己报错,无需探测。"""
33
+ return True
34
+
35
+ name = "hermes"
36
+ binary = "hermes"
37
+
38
+ def build_argv(self, req: RunRequest) -> list[str]:
39
+ # hermes 非 interactive 只有单查询模式;无 prompt 会退回交互式 REPL 挂起,直接拒绝
40
+ if not req.prompt.strip():
41
+ raise SessionError("hermes 恢复会话需要提供续写 prompt(其 CLI 无非交互续写模式)")
42
+ argv: list[str] = ["chat", f"--query={req.prompt}", "-Q"]
43
+ if req.model:
44
+ argv += ["-m", req.model]
45
+ if req.session:
46
+ argv += ["--resume", req.session]
47
+ return argv
48
+
49
+ def parse_output(self, stdout: str, stderr: str) -> RunOutput:
50
+ session_id = ""
51
+ for line in stderr.splitlines():
52
+ m = _SESSION_LINE_RE.match(line.strip())
53
+ if m:
54
+ session_id = m.group(1)
55
+ break
56
+ # stdout 即正文(reasoning 框等由 CLI 自行渲染,不在此过滤)
57
+ return RunOutput(text=stdout.strip(), session_id=session_id)
58
+
59
+ def export_session_file(self, session_id: str, target: str) -> None:
60
+ """跑原生 sessions export(jsonl),由 hermes 自己写 target。"""
61
+ if not session_id:
62
+ raise SessionError("导出需要有效的 session_id")
63
+ binary = which_or_raise(self.binary, self.name)
64
+ argv = [
65
+ binary,
66
+ "sessions",
67
+ "export",
68
+ "--session-id",
69
+ session_id,
70
+ "--format",
71
+ "jsonl",
72
+ target,
73
+ ]
74
+ proc = subprocess.run(
75
+ argv,
76
+ capture_output=True,
77
+ text=True,
78
+ encoding="utf-8",
79
+ errors="replace",
80
+ check=False,
81
+ )
82
+ if proc.returncode != 0:
83
+ # CLI 自己写 target,非零退出时内容不可信,不留下半截文件
84
+ Path(target).unlink(missing_ok=True)
85
+ ProcessResult(self.name, proc.returncode, proc.stdout, proc.stderr).raise_for_status()
@@ -0,0 +1,10 @@
1
+ """opencode 后端聚合包(v8 设计):版本分派 + CLI 兜底。"""
2
+
3
+ from agent_cli_lib.backends.opencode.backend import (
4
+ Opencode2Backend,
5
+ OpencodeBackend,
6
+ OpencodeCliImpl,
7
+ )
8
+ from agent_cli_lib.backends.opencode.specs import V1, V2
9
+
10
+ __all__ = ["V1", "V2", "Opencode2Backend", "OpencodeBackend", "OpencodeCliImpl"]