mindcode 0.2.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.
- mindcode/__init__.py +5 -0
- mindcode/__main__.py +4 -0
- mindcode/__pycache__/__init__.cpython-313.pyc +0 -0
- mindcode/__pycache__/__main__.cpython-313.pyc +0 -0
- mindcode/__pycache__/_version.cpython-313.pyc +0 -0
- mindcode/__pycache__/config.cpython-313.pyc +0 -0
- mindcode/__pycache__/policy.cpython-313.pyc +0 -0
- mindcode/__pycache__/remote.cpython-313.pyc +0 -0
- mindcode/__pycache__/render.cpython-313.pyc +0 -0
- mindcode/__pycache__/runtime.cpython-313.pyc +0 -0
- mindcode/__pycache__/tasking.cpython-313.pyc +0 -0
- mindcode/__pycache__/terminal_core.cpython-313.pyc +0 -0
- mindcode/_version.py +11 -0
- mindcode/approval.py +105 -0
- mindcode/bench/__init__.py +19 -0
- mindcode/bench/__pycache__/__init__.cpython-313.pyc +0 -0
- mindcode/bench/__pycache__/fake_tools.cpython-313.pyc +0 -0
- mindcode/bench/__pycache__/jsonutil.cpython-313.pyc +0 -0
- mindcode/bench/__pycache__/paths.cpython-313.pyc +0 -0
- mindcode/bench/__pycache__/predictions.cpython-313.pyc +0 -0
- mindcode/bench/__pycache__/report.cpython-313.pyc +0 -0
- mindcode/bench/__pycache__/runner.cpython-313.pyc +0 -0
- mindcode/bench/__pycache__/schema.cpython-313.pyc +0 -0
- mindcode/bench/__pycache__/trace.cpython-313.pyc +0 -0
- mindcode/bench/__pycache__/workspace.cpython-313.pyc +0 -0
- mindcode/bench/adapters/__init__.py +22 -0
- mindcode/bench/adapters/__pycache__/__init__.cpython-313.pyc +0 -0
- mindcode/bench/adapters/__pycache__/base.cpython-313.pyc +0 -0
- mindcode/bench/adapters/__pycache__/mini_bfcl.cpython-313.pyc +0 -0
- mindcode/bench/adapters/__pycache__/mini_gaia.cpython-313.pyc +0 -0
- mindcode/bench/adapters/__pycache__/mini_terminal.cpython-313.pyc +0 -0
- mindcode/bench/adapters/__pycache__/swe_bench.cpython-313.pyc +0 -0
- mindcode/bench/adapters/__pycache__/terminal_bench.cpython-313.pyc +0 -0
- mindcode/bench/adapters/base.py +45 -0
- mindcode/bench/adapters/mini_bfcl.py +18 -0
- mindcode/bench/adapters/mini_gaia.py +18 -0
- mindcode/bench/adapters/mini_terminal.py +18 -0
- mindcode/bench/adapters/swe_bench.py +86 -0
- mindcode/bench/adapters/terminal_bench.py +107 -0
- mindcode/bench/fake_tools.py +73 -0
- mindcode/bench/jsonutil.py +20 -0
- mindcode/bench/paths.py +10 -0
- mindcode/bench/predictions.py +65 -0
- mindcode/bench/report.py +48 -0
- mindcode/bench/runner.py +250 -0
- mindcode/bench/schema.py +78 -0
- mindcode/bench/scorers/__pycache__/base.cpython-313.pyc +0 -0
- mindcode/bench/scorers/__pycache__/composite.cpython-313.pyc +0 -0
- mindcode/bench/scorers/__pycache__/exact.cpython-313.pyc +0 -0
- mindcode/bench/scorers/__pycache__/json_call.cpython-313.pyc +0 -0
- mindcode/bench/scorers/__pycache__/swe.cpython-313.pyc +0 -0
- mindcode/bench/scorers/__pycache__/terminal.cpython-313.pyc +0 -0
- mindcode/bench/scorers/__pycache__/terminal_bench.cpython-313.pyc +0 -0
- mindcode/bench/scorers/base.py +22 -0
- mindcode/bench/scorers/composite.py +57 -0
- mindcode/bench/scorers/exact.py +30 -0
- mindcode/bench/scorers/json_call.py +44 -0
- mindcode/bench/scorers/swe.py +29 -0
- mindcode/bench/scorers/terminal.py +43 -0
- mindcode/bench/scorers/terminal_bench.py +25 -0
- mindcode/bench/trace.py +34 -0
- mindcode/bench/workspace.py +127 -0
- mindcode/cli/__init__.py +112 -0
- mindcode/cli/__pycache__/__init__.cpython-313.pyc +0 -0
- mindcode/cli/__pycache__/_shared.cpython-313.pyc +0 -0
- mindcode/cli/_shared.py +43 -0
- mindcode/cli/commands/__init__.py +1 -0
- mindcode/cli/commands/__pycache__/__init__.cpython-313.pyc +0 -0
- mindcode/cli/commands/__pycache__/bench.cpython-313.pyc +0 -0
- mindcode/cli/commands/__pycache__/chat.cpython-313.pyc +0 -0
- mindcode/cli/commands/__pycache__/config_cmd.cpython-313.pyc +0 -0
- mindcode/cli/commands/__pycache__/remote.cpython-313.pyc +0 -0
- mindcode/cli/commands/__pycache__/shell.cpython-313.pyc +0 -0
- mindcode/cli/commands/__pycache__/status.cpython-313.pyc +0 -0
- mindcode/cli/commands/__pycache__/task.cpython-313.pyc +0 -0
- mindcode/cli/commands/__pycache__/terminal.cpython-313.pyc +0 -0
- mindcode/cli/commands/bench.py +365 -0
- mindcode/cli/commands/chat.py +86 -0
- mindcode/cli/commands/config_cmd.py +197 -0
- mindcode/cli/commands/remote.py +146 -0
- mindcode/cli/commands/shell.py +108 -0
- mindcode/cli/commands/status.py +37 -0
- mindcode/cli/commands/task.py +417 -0
- mindcode/cli/commands/terminal.py +124 -0
- mindcode/cli/shell/__init__.py +5 -0
- mindcode/cli/shell/__pycache__/__init__.cpython-313.pyc +0 -0
- mindcode/cli/shell/__pycache__/completion.cpython-313.pyc +0 -0
- mindcode/cli/shell/__pycache__/menu.cpython-313.pyc +0 -0
- mindcode/cli/shell/__pycache__/repl.cpython-313.pyc +0 -0
- mindcode/cli/shell/__pycache__/slash.cpython-313.pyc +0 -0
- mindcode/cli/shell/__pycache__/startup.cpython-313.pyc +0 -0
- mindcode/cli/shell/completion.py +72 -0
- mindcode/cli/shell/menu.py +88 -0
- mindcode/cli/shell/repl.py +597 -0
- mindcode/cli/shell/slash.py +480 -0
- mindcode/cli/shell/startup.py +55 -0
- mindcode/cli/shell/tui.py +897 -0
- mindcode/config.py +173 -0
- mindcode/mcp.py +205 -0
- mindcode/policy.py +173 -0
- mindcode/remote.py +216 -0
- mindcode/render.py +458 -0
- mindcode/runtime.py +541 -0
- mindcode/skills.py +154 -0
- mindcode/subagents.py +43 -0
- mindcode/tasking.py +281 -0
- mindcode/terminal/__init__.py +1 -0
- mindcode/terminal/__main__.py +17 -0
- mindcode/terminal/__pycache__/__init__.cpython-313.pyc +0 -0
- mindcode/terminal/__pycache__/__main__.cpython-313.pyc +0 -0
- mindcode/terminal_core.py +264 -0
- mindcode-0.2.0.dist-info/METADATA +244 -0
- mindcode-0.2.0.dist-info/RECORD +116 -0
- mindcode-0.2.0.dist-info/WHEEL +5 -0
- mindcode-0.2.0.dist-info/entry_points.txt +2 -0
- mindcode-0.2.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
import typer
|
|
7
|
+
from rich.table import Table
|
|
8
|
+
|
|
9
|
+
from ...remote import (
|
|
10
|
+
RemoteConfig,
|
|
11
|
+
RemoteExecutor,
|
|
12
|
+
RemoteStore,
|
|
13
|
+
SSHCommandBuilder,
|
|
14
|
+
mask_env,
|
|
15
|
+
)
|
|
16
|
+
from .._shared import console
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
app = typer.Typer(help="管理和执行远程 SSH 目标")
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@app.command("add")
|
|
23
|
+
def add_remote(
|
|
24
|
+
name: str = typer.Argument(..., help="remote 名称"),
|
|
25
|
+
host: str = typer.Option(..., "--host", help="SSH host,允许 user@host"),
|
|
26
|
+
workdir: str = typer.Option(..., "--workdir", help="远程默认工作目录"),
|
|
27
|
+
port: int = typer.Option(22, "--port", help="SSH 端口"),
|
|
28
|
+
identity_file: str | None = typer.Option(
|
|
29
|
+
None,
|
|
30
|
+
"--identity-file",
|
|
31
|
+
help="SSH 私钥路径",
|
|
32
|
+
),
|
|
33
|
+
env: list[str] | None = typer.Option(
|
|
34
|
+
None,
|
|
35
|
+
"--env",
|
|
36
|
+
help="环境变量,格式 KEY=VALUE,可重复",
|
|
37
|
+
),
|
|
38
|
+
) -> None:
|
|
39
|
+
try:
|
|
40
|
+
config = RemoteConfig(
|
|
41
|
+
name=name,
|
|
42
|
+
host=host,
|
|
43
|
+
port=port,
|
|
44
|
+
workdir=workdir,
|
|
45
|
+
identity_file=str(Path(identity_file).expanduser()) if identity_file else None,
|
|
46
|
+
env=_parse_env(env or []),
|
|
47
|
+
)
|
|
48
|
+
RemoteStore().add(config)
|
|
49
|
+
except ValueError as exc:
|
|
50
|
+
console.print(f"[red]remote 配置错误:[/red] {exc}")
|
|
51
|
+
raise typer.Exit(2)
|
|
52
|
+
console.print(f"remote added: {name}")
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@app.command("list")
|
|
56
|
+
def list_remotes() -> None:
|
|
57
|
+
table = Table("name", "host", "port", "workdir")
|
|
58
|
+
for config in RemoteStore().load_all().values():
|
|
59
|
+
table.add_row(config.name, config.host, str(config.port), config.workdir)
|
|
60
|
+
console.print(table)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@app.command("show")
|
|
64
|
+
def show_remote(name: str) -> None:
|
|
65
|
+
try:
|
|
66
|
+
config = RemoteStore().get(name)
|
|
67
|
+
except KeyError:
|
|
68
|
+
console.print(f"[red]remote 不存在:[/red] {name}")
|
|
69
|
+
raise typer.Exit(2)
|
|
70
|
+
payload = {
|
|
71
|
+
"name": config.name,
|
|
72
|
+
"host": config.host,
|
|
73
|
+
"port": config.port,
|
|
74
|
+
"workdir": config.workdir,
|
|
75
|
+
"identity_file": config.identity_file,
|
|
76
|
+
"env": mask_env(config.env),
|
|
77
|
+
}
|
|
78
|
+
console.print(json.dumps(payload, ensure_ascii=False, indent=2))
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
@app.command("remove")
|
|
82
|
+
def remove_remote(name: str) -> None:
|
|
83
|
+
if not RemoteStore().remove(name):
|
|
84
|
+
console.print(f"[red]remote 不存在:[/red] {name}")
|
|
85
|
+
raise typer.Exit(2)
|
|
86
|
+
console.print(f"remote removed: {name}")
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
@app.command("exec")
|
|
90
|
+
def exec_remote(
|
|
91
|
+
name: str = typer.Argument(..., help="remote 名称"),
|
|
92
|
+
command: str = typer.Argument(..., help="远程 shell 命令"),
|
|
93
|
+
timeout: float | None = typer.Option(None, "--timeout", help="超时秒数"),
|
|
94
|
+
) -> None:
|
|
95
|
+
try:
|
|
96
|
+
config = RemoteStore().get(name)
|
|
97
|
+
except KeyError:
|
|
98
|
+
console.print(f"[red]remote 不存在:[/red] {name}")
|
|
99
|
+
raise typer.Exit(2)
|
|
100
|
+
result = RemoteExecutor().execute(config, command, timeout_s=timeout)
|
|
101
|
+
if result.stdout:
|
|
102
|
+
console.print(result.stdout, end="")
|
|
103
|
+
if result.stderr:
|
|
104
|
+
console.print(result.stderr, end="", style="red")
|
|
105
|
+
if result.exit_code != 0:
|
|
106
|
+
raise typer.Exit(result.exit_code)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
@app.command("tail")
|
|
110
|
+
def tail_remote(
|
|
111
|
+
name: str = typer.Argument(..., help="remote 名称"),
|
|
112
|
+
path: str = typer.Argument(..., help="远程日志路径"),
|
|
113
|
+
lines: int = typer.Option(80, "--lines", "-n", help="显示最近行数"),
|
|
114
|
+
follow: bool = typer.Option(False, "--follow", "-f", help="持续追踪"),
|
|
115
|
+
timeout: float | None = typer.Option(None, "--timeout", help="超时秒数"),
|
|
116
|
+
) -> None:
|
|
117
|
+
try:
|
|
118
|
+
config = RemoteStore().get(name)
|
|
119
|
+
command = SSHCommandBuilder().build_tail_command(
|
|
120
|
+
path,
|
|
121
|
+
lines=lines,
|
|
122
|
+
follow=follow,
|
|
123
|
+
)
|
|
124
|
+
except KeyError:
|
|
125
|
+
console.print(f"[red]remote 不存在:[/red] {name}")
|
|
126
|
+
raise typer.Exit(2)
|
|
127
|
+
except ValueError as exc:
|
|
128
|
+
console.print(f"[red]tail 参数错误:[/red] {exc}")
|
|
129
|
+
raise typer.Exit(2)
|
|
130
|
+
result = RemoteExecutor().execute(config, command, timeout_s=timeout)
|
|
131
|
+
if result.stdout:
|
|
132
|
+
console.print(result.stdout, end="")
|
|
133
|
+
if result.stderr:
|
|
134
|
+
console.print(result.stderr, end="", style="red")
|
|
135
|
+
if result.exit_code != 0:
|
|
136
|
+
raise typer.Exit(result.exit_code)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def _parse_env(items: list[str]) -> dict[str, str]:
|
|
140
|
+
parsed: dict[str, str] = {}
|
|
141
|
+
for item in items:
|
|
142
|
+
if "=" not in item:
|
|
143
|
+
raise ValueError(f"--env must be KEY=VALUE: {item}")
|
|
144
|
+
key, value = item.split("=", 1)
|
|
145
|
+
parsed[key] = value
|
|
146
|
+
return parsed
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
"""`mindcode shell` 子命令 —— 交互式 REPL 入口。"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
import typer
|
|
8
|
+
|
|
9
|
+
from ...policy import ApprovalMode
|
|
10
|
+
from .._shared import console
|
|
11
|
+
from ..shell import run_shell
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
_SHELL_APPROVAL_MODES = {
|
|
15
|
+
"ask_user": ApprovalMode.ASK_USER,
|
|
16
|
+
"full_accept": ApprovalMode.FULL_ACCEPT,
|
|
17
|
+
# Short spellings kept for existing shell scripts and sessions.
|
|
18
|
+
"ask": ApprovalMode.ASK_USER,
|
|
19
|
+
"full": ApprovalMode.FULL_ACCEPT,
|
|
20
|
+
"auto": ApprovalMode.FULL_ACCEPT,
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def shell(
|
|
25
|
+
prompt: str = typer.Argument(
|
|
26
|
+
None, help="一次性 prompt;省略进入交互模式"
|
|
27
|
+
),
|
|
28
|
+
workspace: str = typer.Option(
|
|
29
|
+
".", "-w", "--workspace", help="workspace 根目录"
|
|
30
|
+
),
|
|
31
|
+
approve_mode: str = typer.Option(
|
|
32
|
+
ApprovalMode.ASK_USER.value,
|
|
33
|
+
"-a",
|
|
34
|
+
"--approve-mode",
|
|
35
|
+
case_sensitive=False,
|
|
36
|
+
help="ask_user=逐项询问 / full_accept=全部接受",
|
|
37
|
+
),
|
|
38
|
+
provider: str = typer.Option(
|
|
39
|
+
None, "-p", "--provider", help="覆盖 default_provider"
|
|
40
|
+
),
|
|
41
|
+
model: str = typer.Option(
|
|
42
|
+
None, "-m", "--model", help="覆盖 default_model"
|
|
43
|
+
),
|
|
44
|
+
resume: str = typer.Option(
|
|
45
|
+
None, "-r", "--resume", help="恢复指定 session_id"
|
|
46
|
+
),
|
|
47
|
+
session: str = typer.Option(
|
|
48
|
+
None, "-s", "--session", help="指定新 session_id"
|
|
49
|
+
),
|
|
50
|
+
no_stream: bool = typer.Option(
|
|
51
|
+
False, "--no-stream", help="关闭流式输出"
|
|
52
|
+
),
|
|
53
|
+
subagents: bool = typer.Option(
|
|
54
|
+
False,
|
|
55
|
+
"--subagents",
|
|
56
|
+
help="使用 master/coder 委派模式(await_result)",
|
|
57
|
+
),
|
|
58
|
+
max_steps: int | None = typer.Option(
|
|
59
|
+
None, "--max-steps", help="最大 step 数(默认 50)"
|
|
60
|
+
),
|
|
61
|
+
step_timeout: float | None = typer.Option(
|
|
62
|
+
None, "--step-timeout", help="单步超时秒数,0 表示禁用(默认 120)"
|
|
63
|
+
),
|
|
64
|
+
total_timeout: float | None = typer.Option(
|
|
65
|
+
None, "--total-timeout", help="总超时秒数,0 表示禁用(默认 600)"
|
|
66
|
+
),
|
|
67
|
+
) -> None:
|
|
68
|
+
"""启动交互式 shell(默认子命令)。"""
|
|
69
|
+
ws = Path(workspace).expanduser().resolve()
|
|
70
|
+
if not ws.is_dir():
|
|
71
|
+
console.print(
|
|
72
|
+
f"[red]workspace 不存在或不是目录:[/red] {ws}"
|
|
73
|
+
)
|
|
74
|
+
raise typer.Exit(2)
|
|
75
|
+
|
|
76
|
+
selected_approve_mode = _SHELL_APPROVAL_MODES.get(
|
|
77
|
+
approve_mode.lower()
|
|
78
|
+
)
|
|
79
|
+
if selected_approve_mode is None:
|
|
80
|
+
console.print(
|
|
81
|
+
"[red]无效 approve mode:[/red] "
|
|
82
|
+
f"{approve_mode} [dim](ask_user / full_accept)[/dim]"
|
|
83
|
+
)
|
|
84
|
+
raise typer.Exit(2)
|
|
85
|
+
|
|
86
|
+
try:
|
|
87
|
+
rc = run_shell(
|
|
88
|
+
ws,
|
|
89
|
+
approve_mode=selected_approve_mode,
|
|
90
|
+
provider_name=provider,
|
|
91
|
+
model=model,
|
|
92
|
+
session_id=session,
|
|
93
|
+
resume=resume,
|
|
94
|
+
stream=not no_stream,
|
|
95
|
+
subagents=subagents,
|
|
96
|
+
one_shot_prompt=prompt,
|
|
97
|
+
max_steps=max_steps if max_steps is not None else 50,
|
|
98
|
+
step_timeout_s=None
|
|
99
|
+
if step_timeout == 0
|
|
100
|
+
else (step_timeout if step_timeout is not None else 300.0),
|
|
101
|
+
total_timeout_s=None
|
|
102
|
+
if total_timeout == 0
|
|
103
|
+
else (total_timeout if total_timeout is not None else 1800.0),
|
|
104
|
+
)
|
|
105
|
+
except ValueError as exc:
|
|
106
|
+
console.print(f"[red]配置错误:[/red] {exc}")
|
|
107
|
+
raise typer.Exit(2)
|
|
108
|
+
raise typer.Exit(rc)
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"""`mindcode status` 子命令 —— 全局状态。"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from rich.table import Table
|
|
6
|
+
|
|
7
|
+
from ..._version import __logo__, __version__
|
|
8
|
+
from .._shared import console, find_config_file
|
|
9
|
+
|
|
10
|
+
from ...config import load_config
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def status() -> None:
|
|
14
|
+
"""显示 mindcode 全局状态。"""
|
|
15
|
+
console.print(__logo__)
|
|
16
|
+
console.print(f"mindcode [dim]v{__version__}[/dim]\n")
|
|
17
|
+
|
|
18
|
+
config_file = find_config_file()
|
|
19
|
+
table = Table(show_header=False, box=None, padding=(0, 2))
|
|
20
|
+
table.add_column(style="bold cyan", width=14)
|
|
21
|
+
table.add_column()
|
|
22
|
+
table.add_row("Config:", f"{config_file} [green]✓[/green]")
|
|
23
|
+
|
|
24
|
+
config = load_config(config_file)
|
|
25
|
+
table.add_row("Default:", config.default_provider)
|
|
26
|
+
table.add_row("Providers:", ", ".join(config.providers))
|
|
27
|
+
|
|
28
|
+
for name, p in config.providers.items():
|
|
29
|
+
key_status = (
|
|
30
|
+
"[green]✓[/green]" if p.api_key else "[red]✗[/red]"
|
|
31
|
+
)
|
|
32
|
+
table.add_row(
|
|
33
|
+
f" {name}:",
|
|
34
|
+
f"{key_status} key [dim]{p.default_model}[/dim]",
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
console.print(table)
|
|
@@ -0,0 +1,417 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
import signal
|
|
7
|
+
import subprocess
|
|
8
|
+
import sys
|
|
9
|
+
import time
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
import typer
|
|
13
|
+
from mindagent.core import (
|
|
14
|
+
LocalConversationStore,
|
|
15
|
+
LocalTaskLedgerStore,
|
|
16
|
+
LocalTaskMemoryStore,
|
|
17
|
+
RunOutcome,
|
|
18
|
+
SessionEnvironment,
|
|
19
|
+
SessionPolicy,
|
|
20
|
+
TaskCoordinator,
|
|
21
|
+
)
|
|
22
|
+
from mindagent.tools import MemoryReadTool
|
|
23
|
+
from rich.table import Table
|
|
24
|
+
|
|
25
|
+
from ...config import load_config
|
|
26
|
+
from ...policy import ApprovalMode
|
|
27
|
+
from ...runtime import build_runtime
|
|
28
|
+
from ...tasking import TaskEventRecorder, TaskStatus, TaskStore
|
|
29
|
+
from .._shared import console
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
app = typer.Typer(help="运行和追踪 mindcode 任务")
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@app.command("run")
|
|
36
|
+
def run_task(
|
|
37
|
+
goal: str = typer.Argument(..., help="任务目标"),
|
|
38
|
+
workspace: str = typer.Option(".", "-w", "--workspace", help="workspace 根目录"),
|
|
39
|
+
provider: str | None = typer.Option(None, "-p", "--provider", help="覆盖 provider"),
|
|
40
|
+
model: str | None = typer.Option(None, "--model", help="覆盖 model"),
|
|
41
|
+
approve_mode: ApprovalMode = typer.Option(
|
|
42
|
+
ApprovalMode.AUTO.value,
|
|
43
|
+
"--approve-mode",
|
|
44
|
+
case_sensitive=False,
|
|
45
|
+
help="审批模式",
|
|
46
|
+
),
|
|
47
|
+
max_steps: int = typer.Option(50, "--max-steps", help="最大 step 数"),
|
|
48
|
+
step_timeout: float | None = typer.Option(
|
|
49
|
+
300.0,
|
|
50
|
+
"--step-timeout",
|
|
51
|
+
help="单步超时秒数",
|
|
52
|
+
),
|
|
53
|
+
total_timeout: float | None = typer.Option(
|
|
54
|
+
1800.0,
|
|
55
|
+
"--total-timeout",
|
|
56
|
+
help="总超时秒数",
|
|
57
|
+
),
|
|
58
|
+
allow_dangerous: bool = typer.Option(
|
|
59
|
+
False,
|
|
60
|
+
"--allow-dangerous",
|
|
61
|
+
help="允许 dangerous action",
|
|
62
|
+
),
|
|
63
|
+
background: bool = typer.Option(
|
|
64
|
+
False,
|
|
65
|
+
"--background",
|
|
66
|
+
"-b",
|
|
67
|
+
help="后台运行 task",
|
|
68
|
+
),
|
|
69
|
+
) -> None:
|
|
70
|
+
ws = Path(workspace).expanduser().resolve()
|
|
71
|
+
if not ws.is_dir():
|
|
72
|
+
console.print(f"[red]workspace 不存在或不是目录:[/red] {ws}")
|
|
73
|
+
raise typer.Exit(2)
|
|
74
|
+
|
|
75
|
+
config = load_config()
|
|
76
|
+
provider_config = config.get_provider(provider)
|
|
77
|
+
model_name = provider_config.resolve_model(model)
|
|
78
|
+
store = TaskStore()
|
|
79
|
+
record = store.create(
|
|
80
|
+
goal=goal,
|
|
81
|
+
workspace=ws,
|
|
82
|
+
provider=provider_config.name,
|
|
83
|
+
model=model_name,
|
|
84
|
+
approve_mode=ApprovalMode(approve_mode).value,
|
|
85
|
+
background=background,
|
|
86
|
+
max_steps=max_steps,
|
|
87
|
+
total_timeout_s=total_timeout,
|
|
88
|
+
allow_dangerous=allow_dangerous,
|
|
89
|
+
)
|
|
90
|
+
console.print(f"task_id={record.task_id}")
|
|
91
|
+
|
|
92
|
+
if background:
|
|
93
|
+
_start_background_worker(store, record.task_id)
|
|
94
|
+
return
|
|
95
|
+
|
|
96
|
+
try:
|
|
97
|
+
ok = asyncio.run(
|
|
98
|
+
_run_task(
|
|
99
|
+
store=store,
|
|
100
|
+
task_id=record.task_id,
|
|
101
|
+
goal=goal,
|
|
102
|
+
workspace=ws,
|
|
103
|
+
provider_name=provider_config.name,
|
|
104
|
+
model=model_name,
|
|
105
|
+
approve_mode=ApprovalMode(approve_mode),
|
|
106
|
+
max_steps=max_steps,
|
|
107
|
+
step_timeout=step_timeout,
|
|
108
|
+
total_timeout=total_timeout,
|
|
109
|
+
allow_dangerous=allow_dangerous,
|
|
110
|
+
)
|
|
111
|
+
)
|
|
112
|
+
if not ok:
|
|
113
|
+
raise typer.Exit(1)
|
|
114
|
+
except ValueError as exc:
|
|
115
|
+
console.print(f"[red]配置错误:[/red] {exc}")
|
|
116
|
+
raise typer.Exit(2)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _start_background_worker(store: TaskStore, task_id: str) -> None:
|
|
120
|
+
task_dir = store.task_dir(task_id)
|
|
121
|
+
task_dir.mkdir(parents=True, exist_ok=True)
|
|
122
|
+
(task_dir / "events.jsonl").touch(exist_ok=True)
|
|
123
|
+
log_path = task_dir / "worker.log"
|
|
124
|
+
cmd = [sys.executable, "-m", "mindcode", "task", "_worker", task_id]
|
|
125
|
+
with log_path.open("ab") as log:
|
|
126
|
+
process = subprocess.Popen(
|
|
127
|
+
cmd,
|
|
128
|
+
stdout=log,
|
|
129
|
+
stderr=subprocess.STDOUT,
|
|
130
|
+
cwd=Path.cwd(),
|
|
131
|
+
env=os.environ.copy(),
|
|
132
|
+
)
|
|
133
|
+
record = store.load(task_id)
|
|
134
|
+
record.pid = process.pid
|
|
135
|
+
store.save(record)
|
|
136
|
+
console.print(f"background pid={process.pid}")
|
|
137
|
+
console.print(f"events={task_dir / 'events.jsonl'}")
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
@app.command("_worker", hidden=True)
|
|
141
|
+
def task_worker(task_id: str) -> None:
|
|
142
|
+
store = TaskStore()
|
|
143
|
+
record = store.load(task_id)
|
|
144
|
+
ok = asyncio.run(
|
|
145
|
+
_run_task(
|
|
146
|
+
store=store,
|
|
147
|
+
task_id=record.task_id,
|
|
148
|
+
goal=record.goal,
|
|
149
|
+
workspace=Path(record.workspace),
|
|
150
|
+
provider_name=record.provider,
|
|
151
|
+
model=record.model or "",
|
|
152
|
+
approve_mode=ApprovalMode(record.approve_mode),
|
|
153
|
+
max_steps=record.max_steps,
|
|
154
|
+
step_timeout=getattr(record, "step_timeout_s", 300.0),
|
|
155
|
+
total_timeout=record.total_timeout_s,
|
|
156
|
+
allow_dangerous=record.allow_dangerous,
|
|
157
|
+
)
|
|
158
|
+
)
|
|
159
|
+
if not ok:
|
|
160
|
+
raise typer.Exit(1)
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
@app.command("resume")
|
|
164
|
+
def resume_task(
|
|
165
|
+
task_id: str = typer.Argument(..., help="要恢复的 task_id"),
|
|
166
|
+
message: str | None = typer.Option(
|
|
167
|
+
None,
|
|
168
|
+
"--message",
|
|
169
|
+
"-m",
|
|
170
|
+
help="恢复时追加给主 Agent 的消息",
|
|
171
|
+
),
|
|
172
|
+
) -> None:
|
|
173
|
+
store = TaskStore()
|
|
174
|
+
try:
|
|
175
|
+
record = store.load(task_id)
|
|
176
|
+
except FileNotFoundError:
|
|
177
|
+
console.print(f"[red]task 不存在:[/red] {task_id}")
|
|
178
|
+
raise typer.Exit(2)
|
|
179
|
+
try:
|
|
180
|
+
ok = asyncio.run(
|
|
181
|
+
_run_task(
|
|
182
|
+
store=store,
|
|
183
|
+
task_id=record.task_id,
|
|
184
|
+
goal=record.goal,
|
|
185
|
+
workspace=Path(record.workspace),
|
|
186
|
+
provider_name=record.provider,
|
|
187
|
+
model=record.model or "",
|
|
188
|
+
approve_mode=ApprovalMode(record.approve_mode),
|
|
189
|
+
max_steps=record.max_steps,
|
|
190
|
+
step_timeout=getattr(record, "step_timeout_s", 300.0),
|
|
191
|
+
total_timeout=record.total_timeout_s,
|
|
192
|
+
allow_dangerous=record.allow_dangerous,
|
|
193
|
+
resume=True,
|
|
194
|
+
user_input=message,
|
|
195
|
+
)
|
|
196
|
+
)
|
|
197
|
+
if not ok:
|
|
198
|
+
raise typer.Exit(1)
|
|
199
|
+
except ValueError as exc:
|
|
200
|
+
console.print(f"[red]无法恢复:[/red] {exc}")
|
|
201
|
+
raise typer.Exit(2)
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
async def _run_task(
|
|
205
|
+
*,
|
|
206
|
+
store: TaskStore,
|
|
207
|
+
task_id: str,
|
|
208
|
+
goal: str,
|
|
209
|
+
workspace: Path,
|
|
210
|
+
provider_name: str,
|
|
211
|
+
model: str,
|
|
212
|
+
approve_mode: ApprovalMode,
|
|
213
|
+
max_steps: int,
|
|
214
|
+
total_timeout: float | None,
|
|
215
|
+
allow_dangerous: bool,
|
|
216
|
+
step_timeout: float | None = 300.0,
|
|
217
|
+
resume: bool = False,
|
|
218
|
+
user_input: str | None = None,
|
|
219
|
+
) -> bool:
|
|
220
|
+
record = store.load(task_id)
|
|
221
|
+
record.status = TaskStatus.RUNNING.value
|
|
222
|
+
record.started_at = time.time()
|
|
223
|
+
record.completed_at = None
|
|
224
|
+
record.error = None
|
|
225
|
+
record.pid = os.getpid()
|
|
226
|
+
store.save(record)
|
|
227
|
+
|
|
228
|
+
recorder = TaskEventRecorder(store, task_id)
|
|
229
|
+
memory_store = LocalTaskMemoryStore(store.task_dir(task_id))
|
|
230
|
+
runtime = None
|
|
231
|
+
try:
|
|
232
|
+
config = load_config()
|
|
233
|
+
provider_config = config.get_provider(provider_name)
|
|
234
|
+
runtime = build_runtime(
|
|
235
|
+
workspace,
|
|
236
|
+
provider_config=provider_config,
|
|
237
|
+
approve_mode=approve_mode,
|
|
238
|
+
model=model,
|
|
239
|
+
stream=False,
|
|
240
|
+
event_handler=recorder,
|
|
241
|
+
max_steps=max_steps,
|
|
242
|
+
step_timeout_s=step_timeout,
|
|
243
|
+
total_timeout_s=total_timeout,
|
|
244
|
+
extra_tools=[MemoryReadTool(memory_store)],
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
environment = SessionEnvironment(
|
|
248
|
+
workspace_root=workspace,
|
|
249
|
+
working_directory=workspace,
|
|
250
|
+
metadata={"mindcode_task_id": task_id},
|
|
251
|
+
)
|
|
252
|
+
policy = SessionPolicy(
|
|
253
|
+
allowed_tools=frozenset(
|
|
254
|
+
{
|
|
255
|
+
"file_read",
|
|
256
|
+
"file_write",
|
|
257
|
+
"file_edit",
|
|
258
|
+
"exec_command",
|
|
259
|
+
"memory_read",
|
|
260
|
+
}
|
|
261
|
+
),
|
|
262
|
+
allow_write=True,
|
|
263
|
+
allow_dangerous=allow_dangerous,
|
|
264
|
+
max_parallel_actions=1,
|
|
265
|
+
)
|
|
266
|
+
session = runtime.create_session(
|
|
267
|
+
session_id=task_id,
|
|
268
|
+
environment=environment,
|
|
269
|
+
policy=policy,
|
|
270
|
+
conversation_store=LocalConversationStore(
|
|
271
|
+
store.task_dir(task_id) / "conversation"
|
|
272
|
+
),
|
|
273
|
+
)
|
|
274
|
+
coordinator = TaskCoordinator(
|
|
275
|
+
session,
|
|
276
|
+
LocalTaskLedgerStore(store.root),
|
|
277
|
+
memory_store,
|
|
278
|
+
task_id=task_id,
|
|
279
|
+
)
|
|
280
|
+
if resume:
|
|
281
|
+
result = await coordinator.resume(user_input=user_input)
|
|
282
|
+
else:
|
|
283
|
+
result = await coordinator.run(goal)
|
|
284
|
+
except Exception as exc:
|
|
285
|
+
record = store.load(task_id)
|
|
286
|
+
record.completed_at = time.time()
|
|
287
|
+
record.status = TaskStatus.FAILED.value
|
|
288
|
+
record.pid = None
|
|
289
|
+
record.error = f"{type(exc).__name__}: {exc}"
|
|
290
|
+
record.metrics = {
|
|
291
|
+
"tool_calls": recorder.tool_calls,
|
|
292
|
+
"agent_events": recorder.agent_events,
|
|
293
|
+
}
|
|
294
|
+
store.save(record)
|
|
295
|
+
store.write_summary(record)
|
|
296
|
+
console.print(f"[red]failed[/red] {record.error}")
|
|
297
|
+
return False
|
|
298
|
+
finally:
|
|
299
|
+
if runtime is not None:
|
|
300
|
+
await runtime.close(close_dependencies=True)
|
|
301
|
+
|
|
302
|
+
record = store.load(task_id)
|
|
303
|
+
record.completed_at = time.time()
|
|
304
|
+
record.pid = None
|
|
305
|
+
record.run_id = result.run_id
|
|
306
|
+
record.final_answer = result.final_answer
|
|
307
|
+
record.error = result.error
|
|
308
|
+
record.status = {
|
|
309
|
+
RunOutcome.FINAL: TaskStatus.COMPLETED.value,
|
|
310
|
+
RunOutcome.PARTIAL: TaskStatus.PARTIAL.value,
|
|
311
|
+
RunOutcome.ERROR: TaskStatus.FAILED.value,
|
|
312
|
+
RunOutcome.CANCELLED: TaskStatus.CANCELLED.value,
|
|
313
|
+
}[result.outcome]
|
|
314
|
+
record.metrics = {
|
|
315
|
+
"steps": result.steps,
|
|
316
|
+
"tool_calls": recorder.tool_calls,
|
|
317
|
+
"agent_events": recorder.agent_events,
|
|
318
|
+
"run_outcome": result.outcome.value,
|
|
319
|
+
}
|
|
320
|
+
store.save(record)
|
|
321
|
+
store.write_summary(record)
|
|
322
|
+
if record.error:
|
|
323
|
+
console.print(f"[red]failed[/red] {record.error}")
|
|
324
|
+
return False
|
|
325
|
+
console.print(f"[green]completed[/green] {task_id}")
|
|
326
|
+
if record.final_answer:
|
|
327
|
+
console.print(record.final_answer)
|
|
328
|
+
return True
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
@app.command("list")
|
|
332
|
+
def list_tasks() -> None:
|
|
333
|
+
store = TaskStore()
|
|
334
|
+
table = Table("task_id", "status", "workspace", "goal")
|
|
335
|
+
for record in store.list():
|
|
336
|
+
table.add_row(
|
|
337
|
+
record.task_id,
|
|
338
|
+
record.status,
|
|
339
|
+
record.workspace,
|
|
340
|
+
_shorten(record.goal, 60),
|
|
341
|
+
)
|
|
342
|
+
console.print(table)
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
@app.command("status")
|
|
346
|
+
def task_status(task_id: str) -> None:
|
|
347
|
+
store = TaskStore()
|
|
348
|
+
try:
|
|
349
|
+
record = store.refresh(store.load(task_id))
|
|
350
|
+
except FileNotFoundError:
|
|
351
|
+
console.print(f"[red]task 不存在:[/red] {task_id}")
|
|
352
|
+
raise typer.Exit(2)
|
|
353
|
+
console.print(json.dumps(record.__dict__, ensure_ascii=False, indent=2))
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
@app.command("tail")
|
|
357
|
+
def task_tail(
|
|
358
|
+
task_id: str,
|
|
359
|
+
lines: int = typer.Option(30, "--lines", "-n", help="显示最近事件行数"),
|
|
360
|
+
follow: bool = typer.Option(False, "--follow", "-f", help="持续追踪事件"),
|
|
361
|
+
) -> None:
|
|
362
|
+
store = TaskStore()
|
|
363
|
+
path = store.task_dir(task_id) / "events.jsonl"
|
|
364
|
+
if not path.exists():
|
|
365
|
+
console.print(f"[red]events 不存在:[/red] {task_id}")
|
|
366
|
+
raise typer.Exit(2)
|
|
367
|
+
rows = path.read_text(encoding="utf-8").splitlines()[-lines:]
|
|
368
|
+
for row in rows:
|
|
369
|
+
_print_event_row(row)
|
|
370
|
+
if not follow:
|
|
371
|
+
return
|
|
372
|
+
offset = path.stat().st_size
|
|
373
|
+
while True:
|
|
374
|
+
time.sleep(1)
|
|
375
|
+
with path.open(encoding="utf-8") as file:
|
|
376
|
+
file.seek(offset)
|
|
377
|
+
for row in file:
|
|
378
|
+
_print_event_row(row)
|
|
379
|
+
offset = file.tell()
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
@app.command("cancel")
|
|
383
|
+
def task_cancel(task_id: str) -> None:
|
|
384
|
+
store = TaskStore()
|
|
385
|
+
try:
|
|
386
|
+
record = store.load(task_id)
|
|
387
|
+
except FileNotFoundError:
|
|
388
|
+
console.print(f"[red]task 不存在:[/red] {task_id}")
|
|
389
|
+
raise typer.Exit(2)
|
|
390
|
+
cancellable = {TaskStatus.CREATED.value, TaskStatus.RUNNING.value}
|
|
391
|
+
if record.pid is None or record.status not in cancellable:
|
|
392
|
+
console.print(f"[yellow]task 当前不可取消:[/yellow] {record.status}")
|
|
393
|
+
raise typer.Exit(1)
|
|
394
|
+
try:
|
|
395
|
+
os.kill(record.pid, signal.SIGTERM)
|
|
396
|
+
except ProcessLookupError:
|
|
397
|
+
pass
|
|
398
|
+
record.status = TaskStatus.CANCELLED.value
|
|
399
|
+
record.completed_at = time.time()
|
|
400
|
+
record.error = "cancelled by user"
|
|
401
|
+
store.save(record)
|
|
402
|
+
store.write_summary(record)
|
|
403
|
+
console.print(f"cancelled {task_id}")
|
|
404
|
+
|
|
405
|
+
|
|
406
|
+
def _print_event_row(row: str) -> None:
|
|
407
|
+
item = json.loads(row)
|
|
408
|
+
console.print(
|
|
409
|
+
f"{item['event_type']} {item['state']} "
|
|
410
|
+
f"{_shorten(str(item.get('payload', {})), 120)}"
|
|
411
|
+
)
|
|
412
|
+
|
|
413
|
+
|
|
414
|
+
def _shorten(text: str, limit: int) -> str:
|
|
415
|
+
if len(text) <= limit:
|
|
416
|
+
return text
|
|
417
|
+
return text[: limit - 3] + "..."
|