aicortexpy 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.
- aicortexpy/__init__.py +3 -0
- aicortexpy/__main__.py +5 -0
- aicortexpy/acp_client.py +559 -0
- aicortexpy/acp_conn.py +853 -0
- aicortexpy/acp_errors.py +107 -0
- aicortexpy/agent_env.py +88 -0
- aicortexpy/artifact_ws.py +233 -0
- aicortexpy/artifacts.py +67 -0
- aicortexpy/attachments.py +85 -0
- aicortexpy/auto_commit.py +106 -0
- aicortexpy/capabilities.py +92 -0
- aicortexpy/chat_acp_pool.py +321 -0
- aicortexpy/cli.py +133 -0
- aicortexpy/client.py +438 -0
- aicortexpy/codex_config.py +98 -0
- aicortexpy/config.py +298 -0
- aicortexpy/daemon.py +677 -0
- aicortexpy/data/default_skills/agent-memory/SKILL.md +67 -0
- aicortexpy/data/default_skills/aicorkit-atlassian/SKILL.md +144 -0
- aicortexpy/data/default_skills/aicorkit-graph/SKILL.md +145 -0
- aicortexpy/data/default_skills/aicorkit-outlook/SKILL.md +87 -0
- aicortexpy/data/default_skills/aicorkit-wxmsg/SKILL.md +96 -0
- aicortexpy/data/default_skills/aicortex-basics/SKILL.md +10 -0
- aicortexpy/data/default_skills/aicortex-bridge-cdp/SKILL.md +118 -0
- aicortexpy/data/default_skills/aicortex-cli-ops/SKILL.md +100 -0
- aicortexpy/data/default_skills/aicortex-graph/SKILL.md +30 -0
- aicortexpy/data/default_skills/graphify/SKILL.md +97 -0
- aicortexpy/data/default_skills/question-form/SKILL.md +162 -0
- aicortexpy/data/default_skills/test-report-generator/SKILL.md +179 -0
- aicortexpy/data/default_skills/test-report-generator/scripts/generate_cases.py +146 -0
- aicortexpy/data/default_skills/test-report-generator/scripts/generate_report.py +275 -0
- aicortexpy/design_seed.py +59 -0
- aicortexpy/diskusage.py +209 -0
- aicortexpy/execenv.py +264 -0
- aicortexpy/gc.py +286 -0
- aicortexpy/handoff_bundle.py +65 -0
- aicortexpy/health.py +178 -0
- aicortexpy/local_skills.py +157 -0
- aicortexpy/models.py +385 -0
- aicortexpy/openclaw_config.py +153 -0
- aicortexpy/pinned_workdir.py +60 -0
- aicortexpy/platform_info.py +61 -0
- aicortexpy/poisoned.py +44 -0
- aicortexpy/process.py +168 -0
- aicortexpy/procutil.py +61 -0
- aicortexpy/prompt.py +235 -0
- aicortexpy/repo.py +462 -0
- aicortexpy/runner.py +969 -0
- aicortexpy/runtime_config.py +540 -0
- aicortexpy/sandbox.py +264 -0
- aicortexpy/stream.py +422 -0
- aicortexpy/terminal.py +147 -0
- aicortexpy/tunnel.py +82 -0
- aicortexpy/wakeup.py +222 -0
- aicortexpy-0.1.0.dist-info/METADATA +106 -0
- aicortexpy-0.1.0.dist-info/RECORD +58 -0
- aicortexpy-0.1.0.dist-info/WHEEL +5 -0
- aicortexpy-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"""Go daemon capability inventory vs Python daemon status."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@dataclass(frozen=True)
|
|
9
|
+
class Capability:
|
|
10
|
+
id: str
|
|
11
|
+
name: str
|
|
12
|
+
go_ref: str
|
|
13
|
+
status: str
|
|
14
|
+
python_note: str
|
|
15
|
+
sandbox_note: str = ""
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
CAPABILITIES: list[Capability] = [
|
|
19
|
+
Capability("register", "Register runtimes per workspace", "daemon.go", "done", "OK"),
|
|
20
|
+
Capability("deregister", "Deregister on shutdown", "daemon.go", "done", "OK"),
|
|
21
|
+
Capability("workspace_sync", "Periodic workspace discovery", "workspaceSyncLoop", "done", "30s sync + re-register empty"),
|
|
22
|
+
Capability("runtime_gone", "Recover when server deletes runtime", "handleRuntimeGone", "done", "404 heartbeat → backoff re-register"),
|
|
23
|
+
Capability("identity", "Stable daemon_id / device_name / X-Client-*", "identity.go", "done", "OK"),
|
|
24
|
+
Capability("health", "Local health + shutdown + /repo/checkout", "health.go", "done", "19514 (+ profile offset); allowlist checkout"),
|
|
25
|
+
Capability("background", "Detach start/stop/status", "cmd_daemon_*", "done", "Win/macOS/Linux; --backend python"),
|
|
26
|
+
Capability("poll_claim", "Poll claim tasks", "pollLoop", "done", "OK"),
|
|
27
|
+
Capability("wakeup_ws", "WebSocket wakeup + sidebands", "wakeup.go", "done", "task + tunnel + artifact + terminal mux"),
|
|
28
|
+
Capability("start_complete_fail", "start / progress / complete / fail", "client.go", "done", "OK"),
|
|
29
|
+
Capability("cancel_watch", "Cancel mid-run via status poll", "watchTaskCancellation", "done", "kill process tree"),
|
|
30
|
+
Capability("messages_usage", "Report stream messages + token usage", "ReportTaskMessages/Usage", "done", "claude/cursor/gemini/opencode/copilot + ACP"),
|
|
31
|
+
Capability("prompt_build", "Rich prompt templates", "prompt.go", "done", "templates + reply_instructions"),
|
|
32
|
+
Capability("recover_orphans", "Recover orphans on register", "RecoverOrphans", "done", "OK"),
|
|
33
|
+
Capability("execenv_prepare", "Per-task env + skills", "execenv.Prepare", "done", "prior reuse + agentEnv + handoff/seed"),
|
|
34
|
+
Capability("runtime_meta", "CLAUDE.md/AGENTS.md/GEMINI.md meta skill", "runtime_config.go", "done", "runtime_config.py"),
|
|
35
|
+
Capability("codex_home", "Per-task CODEX_HOME", "codex_home.go", "done", "hydrate + skill strip + multi_agent off"),
|
|
36
|
+
Capability("openclaw_config", "Per-task OpenClaw config wrap", "openclaw_config.go", "done", "fail-closed + full agent entry rewrite"),
|
|
37
|
+
Capability("repo_cache", "Bare cache + worktree checkout", "repocache/", "done", "per-ws bare + lock + worktree + co-authored hook"),
|
|
38
|
+
Capability("pinned_workdir", "Pinned project workdir", "pinned_workdir.go", "done", "projects/<id> stages"),
|
|
39
|
+
Capability("attachments", "Materialize attachments", "materialize_attachments.go", "done", "_incoming/"),
|
|
40
|
+
Capability("auto_commit", "Auto-commit agent changes", "auto_commit.go", "done", "best-effort commit/push"),
|
|
41
|
+
Capability("artifacts", "Artifacts metadata + WS browse/chunk", "artifact/", "done", "scan metadata + wakeup artifact ops"),
|
|
42
|
+
Capability("agent_claude", "Claude stream-json backend", "claude.go", "done", "stream-json + resume + mcp-config"),
|
|
43
|
+
Capability("agent_codex", "Codex exec backend", "codex.go", "done", "codex exec + CODEX_HOME + semantic inactivity"),
|
|
44
|
+
Capability("agent_others", "Other provider CLIs", "pkg/agent", "done", "openclaw/pi/opencode/copilot/cursor/gemini"),
|
|
45
|
+
Capability(
|
|
46
|
+
"chat_acp_pool",
|
|
47
|
+
"ACP connection pool",
|
|
48
|
+
"chat_acp_pool.go",
|
|
49
|
+
"done",
|
|
50
|
+
"Cursor/Kiro chat pool; kiro/hermes/kimi/dsh oneshot ACP",
|
|
51
|
+
),
|
|
52
|
+
Capability(
|
|
53
|
+
"agent_acp_oneshot",
|
|
54
|
+
"ACP one-shot backends",
|
|
55
|
+
"kiro.go/hermes.go/kimi.go/dsh.go",
|
|
56
|
+
"done",
|
|
57
|
+
"JSON-RPC + tool stream + stderr promote + idle watchdog",
|
|
58
|
+
),
|
|
59
|
+
Capability("model_list", "Heartbeat model discovery", "handleModelList", "done", "CLI + ACP discovery + 60s cache"),
|
|
60
|
+
Capability("local_skills", "List/import local skills", "local_skills.go", "done", "local_skills.py"),
|
|
61
|
+
Capability("default_skills", "Bundled default skills", "defaultskills/", "done", "bundled + monorepo sync"),
|
|
62
|
+
Capability("heartbeat", "Runtime heartbeat + actions", "heartbeatLoop", "done", "model/skills; skip update"),
|
|
63
|
+
Capability("auto_update", "Self-update CLI binary", "auto_update.go", "n/a", "use pip"),
|
|
64
|
+
Capability("gc", "Workspace GC / diskusage", "gc.go", "done", ".gc_meta.json + active-root guard"),
|
|
65
|
+
Capability("poisoned", "Poisoned-task quarantine", "poisoned.go", "done", "failure_reason classifiers"),
|
|
66
|
+
Capability("terminal_pty", "Remote terminal", "terminal_*.go", "partial", "pipe-shell MVP; aicortex_TERMINAL_PTY"),
|
|
67
|
+
Capability("tunnel_proxy", "Loopback HTTP tunnel", "tunnel/proxy.go", "done", "tunnel.py via wakeup WS"),
|
|
68
|
+
Capability("handoff_bundle", "Design handoff bundle", "handoff_bundle.go", "done", "download+extract zip"),
|
|
69
|
+
Capability("design_seed", "Design example seed", "design.SeedExample*", "done", "bundle copy + marker"),
|
|
70
|
+
Capability("sandbox_agent_native", "Provider-native sandbox", "codex_sandbox.go", "partial", "Codex config; Claude flags thin"),
|
|
71
|
+
Capability("sandbox_wrapper", "Optional OS wrapper", "sandbox-runtime", "partial", "bwrap/Seatbelt optional; Win n/a"),
|
|
72
|
+
]
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def by_status() -> dict[str, list[Capability]]:
|
|
76
|
+
out: dict[str, list[Capability]] = {}
|
|
77
|
+
for c in CAPABILITIES:
|
|
78
|
+
out.setdefault(c.status, []).append(c)
|
|
79
|
+
return out
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def format_table() -> str:
|
|
83
|
+
order = ["done", "partial", "planned", "hard", "defer", "n/a"]
|
|
84
|
+
lines = [f"{'STATUS':<8} {'ID':<22} {'NAME'}", "-" * 72]
|
|
85
|
+
grouped = by_status()
|
|
86
|
+
for st in order:
|
|
87
|
+
for c in grouped.get(st, []):
|
|
88
|
+
lines.append(f"{c.status:<8} {c.id:<22} {c.name}")
|
|
89
|
+
counts = {st: len(grouped.get(st, [])) for st in order}
|
|
90
|
+
lines.append("-" * 72)
|
|
91
|
+
lines.append("counts: " + ", ".join(f"{st}={counts[st]}" for st in order if counts[st]))
|
|
92
|
+
return "\n".join(lines)
|
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
"""Pooled Cursor/Kiro ACP connections for multi-turn chat sessions."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
import threading
|
|
7
|
+
import time
|
|
8
|
+
from dataclasses import dataclass, field
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
from .acp_conn import (
|
|
12
|
+
ACPChatConn,
|
|
13
|
+
ACPPromptResult,
|
|
14
|
+
CursorACPConn,
|
|
15
|
+
KiroACPConn,
|
|
16
|
+
cursor_acp_supported,
|
|
17
|
+
kiro_acp_supported,
|
|
18
|
+
)
|
|
19
|
+
from .config import AgentEntry, Config
|
|
20
|
+
|
|
21
|
+
log = logging.getLogger("aicortexpy.acp_pool")
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass
|
|
25
|
+
class _PoolEntry:
|
|
26
|
+
key: str
|
|
27
|
+
provider: str
|
|
28
|
+
chat_id: str
|
|
29
|
+
agent_id: str
|
|
30
|
+
work_dir: str
|
|
31
|
+
model: str
|
|
32
|
+
conn: ACPChatConn | None = None
|
|
33
|
+
last_used: float = field(default_factory=time.time)
|
|
34
|
+
turn_lock: threading.Lock = field(default_factory=threading.Lock)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class ChatACPPool:
|
|
38
|
+
def __init__(
|
|
39
|
+
self,
|
|
40
|
+
*,
|
|
41
|
+
cursor_enabled: bool,
|
|
42
|
+
kiro_enabled: bool,
|
|
43
|
+
idle_timeout: float,
|
|
44
|
+
max_entries: int,
|
|
45
|
+
agents: dict[str, AgentEntry],
|
|
46
|
+
) -> None:
|
|
47
|
+
self._agents = agents
|
|
48
|
+
self.idle_timeout = idle_timeout if idle_timeout > 0 else 300.0
|
|
49
|
+
self.max_entries = max_entries if max_entries > 0 else 50
|
|
50
|
+
self._lock = threading.Lock()
|
|
51
|
+
self._entries: dict[str, _PoolEntry] = {}
|
|
52
|
+
|
|
53
|
+
cursor_ok = False
|
|
54
|
+
if cursor_enabled:
|
|
55
|
+
entry = agents.get("cursor")
|
|
56
|
+
cursor_ok = bool(entry) and cursor_acp_supported(entry.path)
|
|
57
|
+
self.cursor_enabled = cursor_ok
|
|
58
|
+
|
|
59
|
+
kiro_ok = False
|
|
60
|
+
if kiro_enabled:
|
|
61
|
+
entry = agents.get("kiro")
|
|
62
|
+
kiro_ok = bool(entry) and kiro_acp_supported(entry.path)
|
|
63
|
+
self.kiro_enabled = kiro_ok
|
|
64
|
+
|
|
65
|
+
if self.cursor_enabled:
|
|
66
|
+
log.info("chat ACP pool: cursor enabled")
|
|
67
|
+
if self.kiro_enabled:
|
|
68
|
+
log.info("chat ACP pool: kiro enabled")
|
|
69
|
+
|
|
70
|
+
@classmethod
|
|
71
|
+
def from_config(cls, cfg: Config) -> ChatACPPool:
|
|
72
|
+
return cls(
|
|
73
|
+
cursor_enabled=cfg.cursor_acp_enabled,
|
|
74
|
+
kiro_enabled=cfg.kiro_acp_enabled,
|
|
75
|
+
idle_timeout=cfg.chat_acp_idle_timeout,
|
|
76
|
+
max_entries=cfg.chat_acp_max_connections,
|
|
77
|
+
agents=cfg.agents,
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
def enabled_for(self, provider: str) -> bool:
|
|
81
|
+
if provider == "cursor":
|
|
82
|
+
return self.cursor_enabled
|
|
83
|
+
if provider == "kiro":
|
|
84
|
+
return self.kiro_enabled
|
|
85
|
+
return False
|
|
86
|
+
|
|
87
|
+
def run_prompt(
|
|
88
|
+
self,
|
|
89
|
+
*,
|
|
90
|
+
provider: str,
|
|
91
|
+
chat_session_id: str,
|
|
92
|
+
agent_id: str,
|
|
93
|
+
work_dir: str,
|
|
94
|
+
model: str,
|
|
95
|
+
prompt: str,
|
|
96
|
+
resume_session_id: str,
|
|
97
|
+
custom_args: list[str],
|
|
98
|
+
timeout: float,
|
|
99
|
+
env: dict[str, str] | None = None,
|
|
100
|
+
cancel_event: threading.Event | None = None,
|
|
101
|
+
on_message: Any | None = None,
|
|
102
|
+
idle_watchdog: float = 0,
|
|
103
|
+
system_prompt: str = "",
|
|
104
|
+
) -> ACPPromptResult:
|
|
105
|
+
key = pool_key(provider, chat_session_id, agent_id, work_dir)
|
|
106
|
+
entry = self._get_or_create(key, provider, chat_session_id, agent_id, work_dir, model)
|
|
107
|
+
with entry.turn_lock:
|
|
108
|
+
if entry.conn is None or entry.model != model:
|
|
109
|
+
if entry.conn is not None:
|
|
110
|
+
try:
|
|
111
|
+
entry.conn.close()
|
|
112
|
+
except Exception:
|
|
113
|
+
pass
|
|
114
|
+
entry.conn = None
|
|
115
|
+
try:
|
|
116
|
+
entry.conn = self._open_conn(
|
|
117
|
+
provider,
|
|
118
|
+
work_dir=work_dir,
|
|
119
|
+
model=model,
|
|
120
|
+
resume_session_id=resume_session_id,
|
|
121
|
+
custom_args=custom_args,
|
|
122
|
+
timeout=timeout,
|
|
123
|
+
env=env,
|
|
124
|
+
)
|
|
125
|
+
entry.model = model
|
|
126
|
+
log.info("%s chat acp pool miss chat=%s", provider, chat_session_id[:8])
|
|
127
|
+
except Exception as e:
|
|
128
|
+
self.remove_entry(key)
|
|
129
|
+
return ACPPromptResult(
|
|
130
|
+
status="failed",
|
|
131
|
+
output="",
|
|
132
|
+
error=f"{provider} chat acp open failed: {e}",
|
|
133
|
+
)
|
|
134
|
+
else:
|
|
135
|
+
log.info("%s chat acp pool hit chat=%s", provider, chat_session_id[:8])
|
|
136
|
+
|
|
137
|
+
entry.last_used = time.time()
|
|
138
|
+
assert entry.conn is not None
|
|
139
|
+
|
|
140
|
+
result = entry.conn.run_prompt(
|
|
141
|
+
prompt,
|
|
142
|
+
timeout=timeout or 20 * 60,
|
|
143
|
+
system_prompt=system_prompt,
|
|
144
|
+
on_message=on_message,
|
|
145
|
+
idle_watchdog=idle_watchdog,
|
|
146
|
+
cancel_event=cancel_event,
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
if not result.session_id and entry.conn.session_id():
|
|
150
|
+
result.session_id = entry.conn.session_id()
|
|
151
|
+
|
|
152
|
+
if _should_evict(result):
|
|
153
|
+
log.info(
|
|
154
|
+
"%s chat acp pool evicting after interrupt status=%s",
|
|
155
|
+
provider,
|
|
156
|
+
result.status,
|
|
157
|
+
)
|
|
158
|
+
self.remove_entry(key)
|
|
159
|
+
elif result.status == "failed" and resume_session_id and not result.session_id:
|
|
160
|
+
log.warning("%s chat acp resume failed, evicting pool entry", provider)
|
|
161
|
+
self.remove_entry(key)
|
|
162
|
+
else:
|
|
163
|
+
entry.last_used = time.time()
|
|
164
|
+
return result
|
|
165
|
+
|
|
166
|
+
def _open_conn(
|
|
167
|
+
self,
|
|
168
|
+
provider: str,
|
|
169
|
+
*,
|
|
170
|
+
work_dir: str,
|
|
171
|
+
model: str,
|
|
172
|
+
resume_session_id: str,
|
|
173
|
+
custom_args: list[str],
|
|
174
|
+
timeout: float,
|
|
175
|
+
env: dict[str, str] | None,
|
|
176
|
+
) -> ACPChatConn:
|
|
177
|
+
entry = self._agents.get(provider)
|
|
178
|
+
if not entry:
|
|
179
|
+
raise RuntimeError(f"no agent configured for {provider}")
|
|
180
|
+
if provider == "cursor":
|
|
181
|
+
return CursorACPConn.open(
|
|
182
|
+
entry.path,
|
|
183
|
+
cwd=work_dir,
|
|
184
|
+
model=model or entry.model,
|
|
185
|
+
resume_session_id="", # never load for cursor chat
|
|
186
|
+
custom_args=custom_args,
|
|
187
|
+
timeout=timeout,
|
|
188
|
+
env=env,
|
|
189
|
+
)
|
|
190
|
+
if provider == "kiro":
|
|
191
|
+
return KiroACPConn.open(
|
|
192
|
+
entry.path,
|
|
193
|
+
cwd=work_dir,
|
|
194
|
+
model=model or entry.model,
|
|
195
|
+
resume_session_id=resume_session_id,
|
|
196
|
+
custom_args=custom_args,
|
|
197
|
+
timeout=timeout,
|
|
198
|
+
env=env,
|
|
199
|
+
)
|
|
200
|
+
raise RuntimeError(f"chat acp pool: unsupported provider {provider!r}")
|
|
201
|
+
|
|
202
|
+
def _get_or_create(
|
|
203
|
+
self,
|
|
204
|
+
key: str,
|
|
205
|
+
provider: str,
|
|
206
|
+
chat_id: str,
|
|
207
|
+
agent_id: str,
|
|
208
|
+
work_dir: str,
|
|
209
|
+
model: str,
|
|
210
|
+
) -> _PoolEntry:
|
|
211
|
+
stale: list[ACPChatConn] = []
|
|
212
|
+
with self._lock:
|
|
213
|
+
stale.extend(self._take_idle_locked(time.time()))
|
|
214
|
+
if key in self._entries:
|
|
215
|
+
e = self._entries[key]
|
|
216
|
+
else:
|
|
217
|
+
if len(self._entries) >= self.max_entries:
|
|
218
|
+
stale.extend(self._take_oldest_locked())
|
|
219
|
+
e = _PoolEntry(
|
|
220
|
+
key=key,
|
|
221
|
+
provider=provider,
|
|
222
|
+
chat_id=chat_id,
|
|
223
|
+
agent_id=agent_id,
|
|
224
|
+
work_dir=work_dir,
|
|
225
|
+
model=model,
|
|
226
|
+
)
|
|
227
|
+
self._entries[key] = e
|
|
228
|
+
_close_conns(stale)
|
|
229
|
+
return e
|
|
230
|
+
|
|
231
|
+
def remove_entry(self, key: str) -> None:
|
|
232
|
+
with self._lock:
|
|
233
|
+
e = self._entries.pop(key, None)
|
|
234
|
+
conn = e.conn if e else None
|
|
235
|
+
if e:
|
|
236
|
+
e.conn = None
|
|
237
|
+
if conn is not None:
|
|
238
|
+
try:
|
|
239
|
+
conn.close()
|
|
240
|
+
except Exception:
|
|
241
|
+
pass
|
|
242
|
+
|
|
243
|
+
def evict_chat_session(self, chat_session_id: str) -> None:
|
|
244
|
+
stale: list[ACPChatConn] = []
|
|
245
|
+
with self._lock:
|
|
246
|
+
for key, e in list(self._entries.items()):
|
|
247
|
+
if e.chat_id != chat_session_id:
|
|
248
|
+
continue
|
|
249
|
+
if e.conn is not None:
|
|
250
|
+
stale.append(e.conn)
|
|
251
|
+
e.conn = None
|
|
252
|
+
del self._entries[key]
|
|
253
|
+
_close_conns(stale)
|
|
254
|
+
|
|
255
|
+
def close_all(self) -> None:
|
|
256
|
+
with self._lock:
|
|
257
|
+
stale = [e.conn for e in self._entries.values() if e.conn is not None]
|
|
258
|
+
for e in self._entries.values():
|
|
259
|
+
e.conn = None
|
|
260
|
+
self._entries.clear()
|
|
261
|
+
_close_conns(stale)
|
|
262
|
+
|
|
263
|
+
def _take_idle_locked(self, now: float) -> list[ACPChatConn]:
|
|
264
|
+
stale: list[ACPChatConn] = []
|
|
265
|
+
for key, e in list(self._entries.items()):
|
|
266
|
+
if now - e.last_used <= self.idle_timeout:
|
|
267
|
+
continue
|
|
268
|
+
if e.conn is not None:
|
|
269
|
+
stale.append(e.conn)
|
|
270
|
+
e.conn = None
|
|
271
|
+
del self._entries[key]
|
|
272
|
+
log.info(
|
|
273
|
+
"chat acp pool evicted idle connection provider=%s chat=%s",
|
|
274
|
+
e.provider,
|
|
275
|
+
e.chat_id[:8],
|
|
276
|
+
)
|
|
277
|
+
return stale
|
|
278
|
+
|
|
279
|
+
def _take_oldest_locked(self) -> list[ACPChatConn]:
|
|
280
|
+
if not self._entries:
|
|
281
|
+
return []
|
|
282
|
+
oldest_key = min(self._entries, key=lambda k: self._entries[k].last_used)
|
|
283
|
+
e = self._entries.pop(oldest_key)
|
|
284
|
+
log.info(
|
|
285
|
+
"chat acp pool evicted oldest connection provider=%s chat=%s",
|
|
286
|
+
e.provider,
|
|
287
|
+
e.chat_id[:8],
|
|
288
|
+
)
|
|
289
|
+
if e.conn is None:
|
|
290
|
+
return []
|
|
291
|
+
conn = e.conn
|
|
292
|
+
e.conn = None
|
|
293
|
+
return [conn]
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
def pool_key(provider: str, chat_session_id: str, agent_id: str, work_dir: str) -> str:
|
|
297
|
+
return "\x00".join([provider, chat_session_id, agent_id, work_dir])
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
def agent_id_from_task(task: Any) -> str:
|
|
301
|
+
agent = getattr(task, "agent", None) or {}
|
|
302
|
+
if isinstance(agent, dict):
|
|
303
|
+
return str(agent.get("id") or "")
|
|
304
|
+
return ""
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
def skip_cursor_chat_session_resume(provider: str, chat_session_id: str) -> bool:
|
|
308
|
+
"""Cursor must not session/load a completed chat ACP session."""
|
|
309
|
+
return provider == "cursor" and bool(str(chat_session_id or "").strip())
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
def _should_evict(result: ACPPromptResult) -> bool:
|
|
313
|
+
return result.status in ("aborted", "cancelled", "timeout", "idle_watchdog")
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
def _close_conns(conns: list[ACPChatConn]) -> None:
|
|
317
|
+
for c in conns:
|
|
318
|
+
try:
|
|
319
|
+
c.close()
|
|
320
|
+
except Exception:
|
|
321
|
+
pass
|
aicortexpy/cli.py
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
"""CLI entry: python -m aicortexpy <command>."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import logging
|
|
7
|
+
import sys
|
|
8
|
+
|
|
9
|
+
from . import __version__
|
|
10
|
+
from .capabilities import format_table
|
|
11
|
+
from .config import load_config
|
|
12
|
+
from .daemon import Daemon
|
|
13
|
+
from .platform_info import platform_label
|
|
14
|
+
from .process import log_path, spawn_background, status_text, stop_daemon
|
|
15
|
+
from .sandbox import describe_default_strategy
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _setup_logging(verbose: bool = False) -> None:
|
|
19
|
+
level = logging.DEBUG if verbose else logging.INFO
|
|
20
|
+
logging.basicConfig(
|
|
21
|
+
level=level,
|
|
22
|
+
format="%(asctime)s %(levelname)s %(name)s: %(message)s",
|
|
23
|
+
datefmt="%H:%M:%S",
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
28
|
+
p = argparse.ArgumentParser(
|
|
29
|
+
prog="python -m aicortexpy",
|
|
30
|
+
description="AICortex Python local agent runtime daemon (MVP)",
|
|
31
|
+
)
|
|
32
|
+
p.add_argument("--version", action="version", version=f"aicortexpy {__version__}")
|
|
33
|
+
p.add_argument("--profile", default="", help="CLI profile name (default profile if empty)")
|
|
34
|
+
p.add_argument("-v", "--verbose", action="store_true")
|
|
35
|
+
|
|
36
|
+
sub = p.add_subparsers(dest="command", required=True)
|
|
37
|
+
|
|
38
|
+
start = sub.add_parser("start", help="Start daemon in background")
|
|
39
|
+
start.add_argument(
|
|
40
|
+
"--foreground",
|
|
41
|
+
action="store_true",
|
|
42
|
+
help="Run in the current terminal instead of detaching",
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
sub.add_parser("run", help="Run daemon in foreground (used by start / service)")
|
|
46
|
+
sub.add_parser("stop", help="Stop the running daemon")
|
|
47
|
+
sub.add_parser("status", help="Show daemon status")
|
|
48
|
+
sub.add_parser("restart", help="Restart daemon")
|
|
49
|
+
sub.add_parser(
|
|
50
|
+
"capabilities",
|
|
51
|
+
help="List Go daemon capabilities vs Python implementation status",
|
|
52
|
+
)
|
|
53
|
+
sub.add_parser(
|
|
54
|
+
"sandbox-info",
|
|
55
|
+
help="Show host OS sandbox strategy (Win/macOS/Linux)",
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
du = sub.add_parser(
|
|
59
|
+
"disk-usage",
|
|
60
|
+
help="Scan workspaces root disk usage (read-only)",
|
|
61
|
+
)
|
|
62
|
+
du.add_argument("--json", action="store_true", help="Emit JSON report")
|
|
63
|
+
du.add_argument("--top", type=int, default=20, help="Show top N task dirs")
|
|
64
|
+
|
|
65
|
+
return p
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def main(argv: list[str] | None = None) -> int:
|
|
69
|
+
parser = build_parser()
|
|
70
|
+
args = parser.parse_args(argv)
|
|
71
|
+
_setup_logging(args.verbose)
|
|
72
|
+
|
|
73
|
+
if args.command == "capabilities":
|
|
74
|
+
print(format_table())
|
|
75
|
+
return 0
|
|
76
|
+
|
|
77
|
+
if args.command == "sandbox-info":
|
|
78
|
+
print(f"platform: {platform_label()}")
|
|
79
|
+
print(describe_default_strategy())
|
|
80
|
+
return 0
|
|
81
|
+
|
|
82
|
+
if args.command == "disk-usage":
|
|
83
|
+
import json
|
|
84
|
+
|
|
85
|
+
from .diskusage import format_report_table, report_to_dict, scan_disk_usage
|
|
86
|
+
|
|
87
|
+
cfg = load_config(args.profile, require_agents=False)
|
|
88
|
+
report = scan_disk_usage(cfg.workspaces_root)
|
|
89
|
+
if args.json:
|
|
90
|
+
print(json.dumps(report_to_dict(report), ensure_ascii=False, indent=2))
|
|
91
|
+
else:
|
|
92
|
+
print(format_report_table(report, top=args.top))
|
|
93
|
+
return 0
|
|
94
|
+
|
|
95
|
+
if args.command == "status":
|
|
96
|
+
cfg = load_config(args.profile, require_agents=False)
|
|
97
|
+
print(status_text(cfg))
|
|
98
|
+
return 0
|
|
99
|
+
|
|
100
|
+
if args.command == "stop":
|
|
101
|
+
cfg = load_config(args.profile, require_agents=False)
|
|
102
|
+
print(stop_daemon(cfg))
|
|
103
|
+
return 0
|
|
104
|
+
|
|
105
|
+
if args.command == "restart":
|
|
106
|
+
cfg = load_config(args.profile, require_agents=False)
|
|
107
|
+
print(stop_daemon(cfg))
|
|
108
|
+
cfg = load_config(args.profile, require_agents=True)
|
|
109
|
+
pid = spawn_background(cfg)
|
|
110
|
+
print(f"restarted pid={pid}")
|
|
111
|
+
print(f"logs: {log_path(cfg)}")
|
|
112
|
+
return 0
|
|
113
|
+
|
|
114
|
+
if args.command == "start":
|
|
115
|
+
cfg = load_config(args.profile, require_agents=True)
|
|
116
|
+
if args.foreground:
|
|
117
|
+
return Daemon(cfg).run()
|
|
118
|
+
pid = spawn_background(cfg)
|
|
119
|
+
print(f"started pid={pid}")
|
|
120
|
+
print(f"health: http://127.0.0.1:{cfg.health_port}/health")
|
|
121
|
+
print(f"logs: {log_path(cfg)}")
|
|
122
|
+
return 0
|
|
123
|
+
|
|
124
|
+
if args.command == "run":
|
|
125
|
+
cfg = load_config(args.profile, require_agents=True)
|
|
126
|
+
return Daemon(cfg).run()
|
|
127
|
+
|
|
128
|
+
parser.print_help()
|
|
129
|
+
return 2
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
if __name__ == "__main__":
|
|
133
|
+
sys.exit(main())
|