aicortexpy 0.2.2__tar.gz → 0.2.3__tar.gz
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-0.2.2/aicortexpy.egg-info → aicortexpy-0.2.3}/PKG-INFO +1 -1
- {aicortexpy-0.2.2 → aicortexpy-0.2.3}/aicortexpy/__init__.py +1 -1
- {aicortexpy-0.2.2 → aicortexpy-0.2.3}/aicortexpy/acp_conn.py +13 -8
- {aicortexpy-0.2.2 → aicortexpy-0.2.3}/aicortexpy/acp_errors.py +23 -0
- aicortexpy-0.2.3/aicortexpy/app_workdir.py +72 -0
- {aicortexpy-0.2.2 → aicortexpy-0.2.3}/aicortexpy/artifact_ws.py +5 -1
- {aicortexpy-0.2.2 → aicortexpy-0.2.3}/aicortexpy/artifacts.py +6 -0
- {aicortexpy-0.2.2 → aicortexpy-0.2.3}/aicortexpy/auto_commit.py +3 -1
- {aicortexpy-0.2.2 → aicortexpy-0.2.3}/aicortexpy/client.py +116 -1
- {aicortexpy-0.2.2 → aicortexpy-0.2.3}/aicortexpy/commands/agent_cmd.py +5 -0
- aicortexpy-0.2.3/aicortexpy/commands/graph_cmd.py +204 -0
- {aicortexpy-0.2.2 → aicortexpy-0.2.3}/aicortexpy/commands/memory_cmd.py +1 -1
- {aicortexpy-0.2.2 → aicortexpy-0.2.3}/aicortexpy/config.py +3 -1
- {aicortexpy-0.2.2 → aicortexpy-0.2.3}/aicortexpy/daemon.py +26 -9
- aicortexpy-0.2.3/aicortexpy/data/default_skills/aicortex-graph/SKILL.md +46 -0
- {aicortexpy-0.2.2 → aicortexpy-0.2.3}/aicortexpy/execenv.py +13 -1
- {aicortexpy-0.2.2 → aicortexpy-0.2.3}/aicortexpy/models.py +12 -3
- {aicortexpy-0.2.2 → aicortexpy-0.2.3}/aicortexpy/openclaw_config.py +3 -1
- {aicortexpy-0.2.2 → aicortexpy-0.2.3}/aicortexpy/process.py +16 -3
- aicortexpy-0.2.3/aicortexpy/procutil.py +113 -0
- {aicortexpy-0.2.2 → aicortexpy-0.2.3}/aicortexpy/repo.py +13 -12
- {aicortexpy-0.2.2 → aicortexpy-0.2.3}/aicortexpy/runner.py +8 -5
- {aicortexpy-0.2.2 → aicortexpy-0.2.3}/aicortexpy/runtime_config.py +73 -1
- {aicortexpy-0.2.2 → aicortexpy-0.2.3}/aicortexpy/terminal.py +3 -0
- {aicortexpy-0.2.2 → aicortexpy-0.2.3/aicortexpy.egg-info}/PKG-INFO +1 -1
- {aicortexpy-0.2.2 → aicortexpy-0.2.3}/aicortexpy.egg-info/SOURCES.txt +3 -1
- {aicortexpy-0.2.2 → aicortexpy-0.2.3}/pyproject.toml +1 -1
- aicortexpy-0.2.3/tests/test_app_workdir.py +59 -0
- aicortexpy-0.2.2/aicortexpy/commands/graph_cmd.py +0 -69
- aicortexpy-0.2.2/aicortexpy/data/default_skills/aicortex-graph/SKILL.md +0 -30
- aicortexpy-0.2.2/aicortexpy/procutil.py +0 -61
- {aicortexpy-0.2.2 → aicortexpy-0.2.3}/MANIFEST.in +0 -0
- {aicortexpy-0.2.2 → aicortexpy-0.2.3}/README.md +0 -0
- {aicortexpy-0.2.2 → aicortexpy-0.2.3}/aicortex/__init__.py +0 -0
- {aicortexpy-0.2.2 → aicortexpy-0.2.3}/aicortex/__main__.py +0 -0
- {aicortexpy-0.2.2 → aicortexpy-0.2.3}/aicortexpy/__main__.py +0 -0
- {aicortexpy-0.2.2 → aicortexpy-0.2.3}/aicortexpy/acp_client.py +0 -0
- {aicortexpy-0.2.2 → aicortexpy-0.2.3}/aicortexpy/agent_env.py +0 -0
- {aicortexpy-0.2.2 → aicortexpy-0.2.3}/aicortexpy/attachments.py +0 -0
- {aicortexpy-0.2.2 → aicortexpy-0.2.3}/aicortexpy/capabilities.py +0 -0
- {aicortexpy-0.2.2 → aicortexpy-0.2.3}/aicortexpy/chat_acp_pool.py +0 -0
- {aicortexpy-0.2.2 → aicortexpy-0.2.3}/aicortexpy/cli.py +0 -0
- {aicortexpy-0.2.2 → aicortexpy-0.2.3}/aicortexpy/cli_context.py +0 -0
- {aicortexpy-0.2.2 → aicortexpy-0.2.3}/aicortexpy/codex_config.py +0 -0
- {aicortexpy-0.2.2 → aicortexpy-0.2.3}/aicortexpy/commands/__init__.py +0 -0
- {aicortexpy-0.2.2 → aicortexpy-0.2.3}/aicortexpy/commands/attachment_cmd.py +0 -0
- {aicortexpy-0.2.2 → aicortexpy-0.2.3}/aicortexpy/commands/auth_cmd.py +0 -0
- {aicortexpy-0.2.2 → aicortexpy-0.2.3}/aicortexpy/commands/autopilot_cmd.py +0 -0
- {aicortexpy-0.2.2 → aicortexpy-0.2.3}/aicortexpy/commands/config_cmd.py +0 -0
- {aicortexpy-0.2.2 → aicortexpy-0.2.3}/aicortexpy/commands/daemon_cmd.py +0 -0
- {aicortexpy-0.2.2 → aicortexpy-0.2.3}/aicortexpy/commands/issue_cmd.py +0 -0
- {aicortexpy-0.2.2 → aicortexpy-0.2.3}/aicortexpy/commands/label_cmd.py +0 -0
- {aicortexpy-0.2.2 → aicortexpy-0.2.3}/aicortexpy/commands/meeting_cmd.py +0 -0
- {aicortexpy-0.2.2 → aicortexpy-0.2.3}/aicortexpy/commands/project_cmd.py +0 -0
- {aicortexpy-0.2.2 → aicortexpy-0.2.3}/aicortexpy/commands/repo_cmd.py +0 -0
- {aicortexpy-0.2.2 → aicortexpy-0.2.3}/aicortexpy/commands/runtime_cmd.py +0 -0
- {aicortexpy-0.2.2 → aicortexpy-0.2.3}/aicortexpy/commands/setup_cmd.py +0 -0
- {aicortexpy-0.2.2 → aicortexpy-0.2.3}/aicortexpy/commands/skill_cmd.py +0 -0
- {aicortexpy-0.2.2 → aicortexpy-0.2.3}/aicortexpy/commands/squad_cmd.py +0 -0
- {aicortexpy-0.2.2 → aicortexpy-0.2.3}/aicortexpy/commands/update_cmd.py +0 -0
- {aicortexpy-0.2.2 → aicortexpy-0.2.3}/aicortexpy/commands/version_cmd.py +0 -0
- {aicortexpy-0.2.2 → aicortexpy-0.2.3}/aicortexpy/commands/workspace_cmd.py +0 -0
- {aicortexpy-0.2.2 → aicortexpy-0.2.3}/aicortexpy/data/default_skills/agent-memory/SKILL.md +0 -0
- {aicortexpy-0.2.2 → aicortexpy-0.2.3}/aicortexpy/data/default_skills/aicorkit-atlassian/SKILL.md +0 -0
- {aicortexpy-0.2.2 → aicortexpy-0.2.3}/aicortexpy/data/default_skills/aicorkit-graph/SKILL.md +0 -0
- {aicortexpy-0.2.2 → aicortexpy-0.2.3}/aicortexpy/data/default_skills/aicorkit-outlook/SKILL.md +0 -0
- {aicortexpy-0.2.2 → aicortexpy-0.2.3}/aicortexpy/data/default_skills/aicorkit-wxmsg/SKILL.md +0 -0
- {aicortexpy-0.2.2 → aicortexpy-0.2.3}/aicortexpy/data/default_skills/aicortex-basics/SKILL.md +0 -0
- {aicortexpy-0.2.2 → aicortexpy-0.2.3}/aicortexpy/data/default_skills/aicortex-bridge-cdp/SKILL.md +0 -0
- {aicortexpy-0.2.2 → aicortexpy-0.2.3}/aicortexpy/data/default_skills/aicortex-cli-ops/SKILL.md +0 -0
- {aicortexpy-0.2.2 → aicortexpy-0.2.3}/aicortexpy/data/default_skills/graphify/SKILL.md +0 -0
- {aicortexpy-0.2.2 → aicortexpy-0.2.3}/aicortexpy/data/default_skills/question-form/SKILL.md +0 -0
- {aicortexpy-0.2.2 → aicortexpy-0.2.3}/aicortexpy/data/default_skills/test-report-generator/SKILL.md +0 -0
- {aicortexpy-0.2.2 → aicortexpy-0.2.3}/aicortexpy/data/default_skills/test-report-generator/scripts/generate_cases.py +0 -0
- {aicortexpy-0.2.2 → aicortexpy-0.2.3}/aicortexpy/data/default_skills/test-report-generator/scripts/generate_report.py +0 -0
- {aicortexpy-0.2.2 → aicortexpy-0.2.3}/aicortexpy/design_seed.py +0 -0
- {aicortexpy-0.2.2 → aicortexpy-0.2.3}/aicortexpy/diskusage.py +0 -0
- {aicortexpy-0.2.2 → aicortexpy-0.2.3}/aicortexpy/gc.py +0 -0
- {aicortexpy-0.2.2 → aicortexpy-0.2.3}/aicortexpy/handoff_bundle.py +0 -0
- {aicortexpy-0.2.2 → aicortexpy-0.2.3}/aicortexpy/health.py +0 -0
- {aicortexpy-0.2.2 → aicortexpy-0.2.3}/aicortexpy/id_resolve.py +0 -0
- {aicortexpy-0.2.2 → aicortexpy-0.2.3}/aicortexpy/local_skills.py +0 -0
- {aicortexpy-0.2.2 → aicortexpy-0.2.3}/aicortexpy/oauth_login.py +0 -0
- {aicortexpy-0.2.2 → aicortexpy-0.2.3}/aicortexpy/output.py +0 -0
- {aicortexpy-0.2.2 → aicortexpy-0.2.3}/aicortexpy/permission.py +0 -0
- {aicortexpy-0.2.2 → aicortexpy-0.2.3}/aicortexpy/pinned_workdir.py +0 -0
- {aicortexpy-0.2.2 → aicortexpy-0.2.3}/aicortexpy/platform_client.py +0 -0
- {aicortexpy-0.2.2 → aicortexpy-0.2.3}/aicortexpy/platform_info.py +0 -0
- {aicortexpy-0.2.2 → aicortexpy-0.2.3}/aicortexpy/poisoned.py +0 -0
- {aicortexpy-0.2.2 → aicortexpy-0.2.3}/aicortexpy/prompt.py +0 -0
- {aicortexpy-0.2.2 → aicortexpy-0.2.3}/aicortexpy/sandbox.py +0 -0
- {aicortexpy-0.2.2 → aicortexpy-0.2.3}/aicortexpy/security.py +0 -0
- {aicortexpy-0.2.2 → aicortexpy-0.2.3}/aicortexpy/stream.py +0 -0
- {aicortexpy-0.2.2 → aicortexpy-0.2.3}/aicortexpy/tunnel.py +0 -0
- {aicortexpy-0.2.2 → aicortexpy-0.2.3}/aicortexpy/wakeup.py +0 -0
- {aicortexpy-0.2.2 → aicortexpy-0.2.3}/aicortexpy.egg-info/dependency_links.txt +0 -0
- {aicortexpy-0.2.2 → aicortexpy-0.2.3}/aicortexpy.egg-info/entry_points.txt +0 -0
- {aicortexpy-0.2.2 → aicortexpy-0.2.3}/aicortexpy.egg-info/requires.txt +0 -0
- {aicortexpy-0.2.2 → aicortexpy-0.2.3}/aicortexpy.egg-info/top_level.txt +0 -0
- {aicortexpy-0.2.2 → aicortexpy-0.2.3}/scripts/aicortex +0 -0
- {aicortexpy-0.2.2 → aicortexpy-0.2.3}/scripts/aicortex.cmd +0 -0
- {aicortexpy-0.2.2 → aicortexpy-0.2.3}/setup.cfg +0 -0
|
@@ -23,7 +23,11 @@ from .acp_client import (
|
|
|
23
23
|
kiro_tool_name,
|
|
24
24
|
resolve_resumed_session_id,
|
|
25
25
|
)
|
|
26
|
-
from .acp_errors import
|
|
26
|
+
from .acp_errors import (
|
|
27
|
+
ACPProviderErrorSniffer,
|
|
28
|
+
enrich_kiro_acp_error,
|
|
29
|
+
promote_acp_result_on_provider_error,
|
|
30
|
+
)
|
|
27
31
|
from .procutil import kill_process_tree, popen_kwargs_for_killable_group
|
|
28
32
|
from .security import SecurityPolicy, build_cursor_acp_global_flags, default_security_policy
|
|
29
33
|
|
|
@@ -73,15 +77,16 @@ def kiro_acp_supported(executable: str) -> bool:
|
|
|
73
77
|
|
|
74
78
|
|
|
75
79
|
def _acp_help_ok(executable: str, *, extra_before: list[str]) -> bool:
|
|
80
|
+
from .procutil import run_no_window
|
|
81
|
+
|
|
76
82
|
path = shutil.which(executable) or executable
|
|
77
83
|
args = [path, *extra_before]
|
|
78
84
|
try:
|
|
79
|
-
proc =
|
|
85
|
+
proc = run_no_window(
|
|
80
86
|
args,
|
|
81
87
|
capture_output=True,
|
|
82
88
|
timeout=5,
|
|
83
89
|
check=False,
|
|
84
|
-
**_hide_window(),
|
|
85
90
|
)
|
|
86
91
|
return proc.returncode == 0
|
|
87
92
|
except (OSError, subprocess.TimeoutExpired):
|
|
@@ -89,11 +94,9 @@ def _acp_help_ok(executable: str, *, extra_before: list[str]) -> bool:
|
|
|
89
94
|
|
|
90
95
|
|
|
91
96
|
def _hide_window() -> dict:
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
startup.dwFlags |= subprocess.STARTF_USESHOWWINDOW
|
|
96
|
-
return {"startupinfo": startup}
|
|
97
|
+
from .procutil import popen_kwargs_hidden_console
|
|
98
|
+
|
|
99
|
+
return popen_kwargs_hidden_console()
|
|
97
100
|
|
|
98
101
|
|
|
99
102
|
def choose_cursor_invocation(executable: str, args: list[str]) -> tuple[str, list[str]]:
|
|
@@ -344,6 +347,8 @@ class _BaseACPConn:
|
|
|
344
347
|
status, error = promote_acp_result_on_provider_error(
|
|
345
348
|
status, error, output, self._sniffer
|
|
346
349
|
)
|
|
350
|
+
if self.provider == "kiro" and status == "failed" and error:
|
|
351
|
+
error = enrich_kiro_acp_error(error)
|
|
347
352
|
|
|
348
353
|
usage = None
|
|
349
354
|
with client._usage_lock:
|
|
@@ -19,6 +19,10 @@ _ACP_TERMINAL_ERROR_RE = re.compile(
|
|
|
19
19
|
_ACP_AGENT_OUTPUT_TERMINAL_RE = re.compile(r"API call failed after \d+ retr(?:y|ies)")
|
|
20
20
|
_ACP_MAX_ERROR_LINES = 8
|
|
21
21
|
|
|
22
|
+
_KIRO_EXPLICIT_AUTH_RE = re.compile(r"no token|not logged in", re.IGNORECASE)
|
|
23
|
+
_KIRO_DISPATCH_RE = re.compile(r"dispatch failure", re.IGNORECASE)
|
|
24
|
+
_KIRO_LOGIN_HINT_RE = re.compile(r"kiro-cli login", re.IGNORECASE)
|
|
25
|
+
|
|
22
26
|
|
|
23
27
|
class ACPProviderErrorSniffer:
|
|
24
28
|
def __init__(self, provider: str) -> None:
|
|
@@ -105,3 +109,22 @@ def promote_acp_result_on_provider_error(
|
|
|
105
109
|
if msg:
|
|
106
110
|
return "failed", msg
|
|
107
111
|
return final_status, final_error
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def enrich_kiro_acp_error(error: str) -> str:
|
|
115
|
+
"""Append login/retry hints from Kiro ACP error text alone (no extra CLI probes)."""
|
|
116
|
+
if not error or _KIRO_LOGIN_HINT_RE.search(error):
|
|
117
|
+
return error
|
|
118
|
+
if _KIRO_EXPLICIT_AUTH_RE.search(error):
|
|
119
|
+
return (
|
|
120
|
+
"kiro-cli is not logged in. Run `kiro-cli login`, then retry. "
|
|
121
|
+
f"({error})"
|
|
122
|
+
)
|
|
123
|
+
if _KIRO_DISPATCH_RE.search(error):
|
|
124
|
+
# Kiro often surfaces logout/network problems only as "dispatch failure".
|
|
125
|
+
return (
|
|
126
|
+
f"{error} "
|
|
127
|
+
"(common causes: kiro-cli not logged in - run `kiro-cli login`; "
|
|
128
|
+
"or transient AWS Q network error - retry)"
|
|
129
|
+
)
|
|
130
|
+
return error
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"""Pinned app template / instance workdirs (port of Go daemon/app_workdir.go)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from .client import Task
|
|
10
|
+
|
|
11
|
+
log = logging.getLogger("aicortexpy.app_workdir")
|
|
12
|
+
|
|
13
|
+
_DEFAULT_SEED = b"<!DOCTYPE html><html><body><h1>App</h1></body></html>\n"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def app_uses_pinned_workdir(task: Task) -> bool:
|
|
17
|
+
return bool(task.app_id.strip()) and task.app_mode in ("edit", "run")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def prepare_app_workdir(workspaces_root: Path, task: Task) -> Path:
|
|
21
|
+
"""Create/reuse app template or instance dir; seed index.html from task.app_html."""
|
|
22
|
+
ws = (task.workspace_id or "").strip()
|
|
23
|
+
app_id = task.app_id.strip()
|
|
24
|
+
if not ws or not app_id:
|
|
25
|
+
raise ValueError("missing app workdir identity")
|
|
26
|
+
|
|
27
|
+
base = workspaces_root / ws / "apps" / app_id
|
|
28
|
+
mode = task.app_mode
|
|
29
|
+
if mode == "edit":
|
|
30
|
+
directory = base / "template"
|
|
31
|
+
elif mode == "run":
|
|
32
|
+
if task.app_workdir_mode == "shared":
|
|
33
|
+
directory = base / "instances" / "shared"
|
|
34
|
+
else:
|
|
35
|
+
user_id = task.app_user_id.strip() or "anonymous"
|
|
36
|
+
directory = base / "instances" / "users" / user_id
|
|
37
|
+
else:
|
|
38
|
+
raise ValueError(f"unknown app mode {mode!r}")
|
|
39
|
+
|
|
40
|
+
directory.mkdir(parents=True, exist_ok=True)
|
|
41
|
+
index_path = directory / "index.html"
|
|
42
|
+
seed_text = task.app_html.strip()
|
|
43
|
+
seed = seed_text.encode("utf-8") if seed_text else _DEFAULT_SEED
|
|
44
|
+
need_write = (not index_path.is_file()) or index_path.stat().st_size == 0
|
|
45
|
+
if not need_write and seed_text:
|
|
46
|
+
# Server AppHTML is authoritative at task start; refresh stale disk
|
|
47
|
+
# copies (e.g. first-run default seed) so complete_task won't clobber DB.
|
|
48
|
+
try:
|
|
49
|
+
existing = index_path.read_text(encoding="utf-8")
|
|
50
|
+
except OSError:
|
|
51
|
+
existing = ""
|
|
52
|
+
if existing.strip() != seed_text:
|
|
53
|
+
need_write = True
|
|
54
|
+
if need_write:
|
|
55
|
+
index_path.write_bytes(seed)
|
|
56
|
+
log.info("seeded app index.html at %s mode=%s", index_path, mode)
|
|
57
|
+
return directory
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def read_app_index_html(work_dir: str | Path) -> str:
|
|
61
|
+
path = Path(work_dir) / "index.html"
|
|
62
|
+
return path.read_text(encoding="utf-8")
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def is_app_workdir_path(path: Path) -> bool:
|
|
66
|
+
"""True when path sits under .../apps/<id>/(template|instances)/..."""
|
|
67
|
+
parts = path.resolve().parts
|
|
68
|
+
try:
|
|
69
|
+
i = parts.index("apps")
|
|
70
|
+
except ValueError:
|
|
71
|
+
return False
|
|
72
|
+
return i + 1 < len(parts)
|
|
@@ -110,10 +110,14 @@ class ArtifactBrowser:
|
|
|
110
110
|
st = child.stat()
|
|
111
111
|
except OSError:
|
|
112
112
|
continue
|
|
113
|
+
child_rel = (
|
|
114
|
+
f"{rel.rstrip('/')}/{child.name}".lstrip("/") if rel else child.name
|
|
115
|
+
)
|
|
116
|
+
# Protocol (Go ArtifactListEntry) expects `path`, not `rel_path`.
|
|
113
117
|
entries.append(
|
|
114
118
|
{
|
|
115
119
|
"name": child.name,
|
|
116
|
-
"
|
|
120
|
+
"path": child_rel,
|
|
117
121
|
"is_dir": child.is_dir(),
|
|
118
122
|
"size": st.st_size if child.is_file() else 0,
|
|
119
123
|
"mtime": int(st.st_mtime),
|
|
@@ -64,4 +64,10 @@ def report_issue_artifacts(
|
|
|
64
64
|
try:
|
|
65
65
|
report(items)
|
|
66
66
|
except Exception as e:
|
|
67
|
+
# Issue preview is feature-flagged off by default (404 "not found").
|
|
68
|
+
# Chat/app workdirs often have index.html that is not an issue artifact.
|
|
69
|
+
status = getattr(e, "status", None)
|
|
70
|
+
if status in (400, 404):
|
|
71
|
+
log.debug("report artifacts skipped: %s", e)
|
|
72
|
+
return
|
|
67
73
|
log.warning("report artifacts failed: %s", e)
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import json
|
|
6
|
+
import logging
|
|
6
7
|
import ssl
|
|
7
8
|
import urllib.error
|
|
8
9
|
import urllib.request
|
|
@@ -12,6 +13,7 @@ from typing import Any
|
|
|
12
13
|
from .platform_info import host_os
|
|
13
14
|
|
|
14
15
|
MAX_ATTACHMENT_BYTES = 100 << 20
|
|
16
|
+
log = logging.getLogger("aicortexpy")
|
|
15
17
|
|
|
16
18
|
|
|
17
19
|
class APIError(Exception):
|
|
@@ -31,6 +33,29 @@ def is_not_found(err: Exception, needle: str) -> bool:
|
|
|
31
33
|
)
|
|
32
34
|
|
|
33
35
|
|
|
36
|
+
def is_connection_reset(err: BaseException) -> bool:
|
|
37
|
+
"""True when the peer closed the TCP socket mid-request (e.g. WinError 10054)."""
|
|
38
|
+
if isinstance(err, ConnectionResetError):
|
|
39
|
+
return True
|
|
40
|
+
if isinstance(err, BrokenPipeError):
|
|
41
|
+
return True
|
|
42
|
+
if isinstance(err, OSError):
|
|
43
|
+
# Windows: 10054 (reset), 10053 (local abort); POSIX: ECONNRESET=104 / macOS 54
|
|
44
|
+
if getattr(err, "winerror", None) in (10054, 10053):
|
|
45
|
+
return True
|
|
46
|
+
if getattr(err, "errno", None) in (104, 54):
|
|
47
|
+
return True
|
|
48
|
+
# urllib wraps the socket error in URLError.reason
|
|
49
|
+
reason = getattr(err, "reason", None)
|
|
50
|
+
if isinstance(reason, BaseException) and reason is not err:
|
|
51
|
+
return is_connection_reset(reason)
|
|
52
|
+
cause = getattr(err, "__cause__", None) or getattr(err, "__context__", None)
|
|
53
|
+
if isinstance(cause, BaseException) and cause is not err:
|
|
54
|
+
return is_connection_reset(cause)
|
|
55
|
+
msg = str(err)
|
|
56
|
+
return "10054" in msg or "10053" in msg
|
|
57
|
+
|
|
58
|
+
|
|
34
59
|
@dataclass
|
|
35
60
|
class Task:
|
|
36
61
|
raw: dict[str, Any]
|
|
@@ -177,6 +202,38 @@ class Task:
|
|
|
177
202
|
def session_kind(self) -> str:
|
|
178
203
|
return self._s("session_kind")
|
|
179
204
|
|
|
205
|
+
@property
|
|
206
|
+
def app_id(self) -> str:
|
|
207
|
+
return self._s("app_id")
|
|
208
|
+
|
|
209
|
+
@property
|
|
210
|
+
def app_mode(self) -> str:
|
|
211
|
+
return self._s("app_mode")
|
|
212
|
+
|
|
213
|
+
@property
|
|
214
|
+
def app_name(self) -> str:
|
|
215
|
+
return self._s("app_name")
|
|
216
|
+
|
|
217
|
+
@property
|
|
218
|
+
def app_description(self) -> str:
|
|
219
|
+
return self._s("app_description")
|
|
220
|
+
|
|
221
|
+
@property
|
|
222
|
+
def app_spec(self) -> str:
|
|
223
|
+
return self._s("app_spec")
|
|
224
|
+
|
|
225
|
+
@property
|
|
226
|
+
def app_workdir_mode(self) -> str:
|
|
227
|
+
return self._s("app_workdir_mode")
|
|
228
|
+
|
|
229
|
+
@property
|
|
230
|
+
def app_html(self) -> str:
|
|
231
|
+
return self._s("app_html")
|
|
232
|
+
|
|
233
|
+
@property
|
|
234
|
+
def app_user_id(self) -> str:
|
|
235
|
+
return self._s("app_user_id")
|
|
236
|
+
|
|
180
237
|
@property
|
|
181
238
|
def squad_id(self) -> str:
|
|
182
239
|
return self._s("squad_id")
|
|
@@ -238,6 +295,9 @@ class DaemonClient:
|
|
|
238
295
|
"Authorization": f"Bearer {self.token}",
|
|
239
296
|
"X-Client-Platform": "daemon",
|
|
240
297
|
"X-Client-OS": host_os(),
|
|
298
|
+
# Avoid Windows urllib keep-alive reuse of half-closed sockets
|
|
299
|
+
# (WinError 10054 mid-claim after the server already dispatched).
|
|
300
|
+
"Connection": "close",
|
|
241
301
|
}
|
|
242
302
|
if self.version:
|
|
243
303
|
headers["X-Client-Version"] = self.version
|
|
@@ -293,7 +353,32 @@ class DaemonClient:
|
|
|
293
353
|
return data if isinstance(data, dict) else {}
|
|
294
354
|
|
|
295
355
|
def claim_task(self, runtime_id: str) -> Task | None:
|
|
296
|
-
|
|
356
|
+
path = f"/api/daemon/runtimes/{runtime_id}/tasks/claim"
|
|
357
|
+
try:
|
|
358
|
+
data = self.post(path, {})
|
|
359
|
+
except Exception as e:
|
|
360
|
+
# Server may have already marked the task dispatched before the
|
|
361
|
+
# response was reset. Retry once: ClaimTaskForRuntime redelivers
|
|
362
|
+
# unstarted dispatched rows. Only fail orphans if retry also dies
|
|
363
|
+
# — otherwise we kill healthy work that just lost the HTTP body.
|
|
364
|
+
if not is_connection_reset(e):
|
|
365
|
+
raise
|
|
366
|
+
log.warning(
|
|
367
|
+
"claim %s connection reset; retrying once for redelivery",
|
|
368
|
+
runtime_id[:8],
|
|
369
|
+
)
|
|
370
|
+
try:
|
|
371
|
+
data = self.post(path, {})
|
|
372
|
+
except Exception as e2:
|
|
373
|
+
if is_connection_reset(e2):
|
|
374
|
+
n = self.fail_orphaned_dispatched(runtime_id, reason=str(e2))
|
|
375
|
+
if n:
|
|
376
|
+
log.warning(
|
|
377
|
+
"claim %s retry reset; failed %d orphaned dispatched task(s)",
|
|
378
|
+
runtime_id[:8],
|
|
379
|
+
n,
|
|
380
|
+
)
|
|
381
|
+
raise
|
|
297
382
|
if not isinstance(data, dict):
|
|
298
383
|
return None
|
|
299
384
|
task = data.get("task")
|
|
@@ -301,6 +386,33 @@ class DaemonClient:
|
|
|
301
386
|
return None
|
|
302
387
|
return Task(task)
|
|
303
388
|
|
|
389
|
+
def list_pending_tasks(self, runtime_id: str) -> list[dict[str, Any]]:
|
|
390
|
+
data = self.get(f"/api/daemon/runtimes/{runtime_id}/tasks/pending")
|
|
391
|
+
return [x for x in data if isinstance(x, dict)] if isinstance(data, list) else []
|
|
392
|
+
|
|
393
|
+
def fail_orphaned_dispatched(self, runtime_id: str, *, reason: str = "") -> int:
|
|
394
|
+
"""Fail dispatched tasks left behind when a claim response was lost."""
|
|
395
|
+
try:
|
|
396
|
+
pending = self.list_pending_tasks(runtime_id)
|
|
397
|
+
except Exception:
|
|
398
|
+
return 0
|
|
399
|
+
failed = 0
|
|
400
|
+
msg = "claim response lost (connection reset); please retry"
|
|
401
|
+
if reason:
|
|
402
|
+
msg = f"{msg}: {reason}"
|
|
403
|
+
for row in pending:
|
|
404
|
+
if str(row.get("status") or "") != "dispatched":
|
|
405
|
+
continue
|
|
406
|
+
tid = str(row.get("id") or "")
|
|
407
|
+
if not tid:
|
|
408
|
+
continue
|
|
409
|
+
try:
|
|
410
|
+
self.fail_task(tid, msg, failure_reason="runtime_recovery")
|
|
411
|
+
failed += 1
|
|
412
|
+
except Exception:
|
|
413
|
+
continue
|
|
414
|
+
return failed
|
|
415
|
+
|
|
304
416
|
def start_task(self, task_id: str) -> None:
|
|
305
417
|
self.post(f"/api/daemon/tasks/{task_id}/start", {})
|
|
306
418
|
|
|
@@ -318,6 +430,7 @@ class DaemonClient:
|
|
|
318
430
|
work_dir: str = "",
|
|
319
431
|
session_id: str = "",
|
|
320
432
|
branch_name: str = "",
|
|
433
|
+
app_html: str = "",
|
|
321
434
|
) -> None:
|
|
322
435
|
body: dict[str, Any] = {"output": output}
|
|
323
436
|
if work_dir:
|
|
@@ -326,6 +439,8 @@ class DaemonClient:
|
|
|
326
439
|
body["session_id"] = session_id
|
|
327
440
|
if branch_name:
|
|
328
441
|
body["branch_name"] = branch_name
|
|
442
|
+
if app_html.strip():
|
|
443
|
+
body["app_html"] = app_html
|
|
329
444
|
self.post(f"/api/daemon/tasks/{task_id}/complete", body)
|
|
330
445
|
|
|
331
446
|
def fail_task(
|
|
@@ -28,6 +28,7 @@ def add_parser(sub: argparse._SubParsersAction) -> None:
|
|
|
28
28
|
create.add_argument("--name", required=True)
|
|
29
29
|
create.add_argument("--instructions", default="")
|
|
30
30
|
create.add_argument("--from-template", default="")
|
|
31
|
+
create.add_argument("--runtime-id", default="", help="Runtime id to execute this agent")
|
|
31
32
|
create.add_argument("--output", choices=["json", "table"], default="json")
|
|
32
33
|
update = asub.add_parser("update", help="Update agent")
|
|
33
34
|
update.add_argument("agent_id")
|
|
@@ -79,9 +80,13 @@ def run(ns: argparse.Namespace) -> int:
|
|
|
79
80
|
if ns.agent_command == "create":
|
|
80
81
|
if ns.from_template:
|
|
81
82
|
body = {"name": ns.name, "template": ns.from_template, "workspace_id": client.workspace_id}
|
|
83
|
+
if ns.runtime_id:
|
|
84
|
+
body["runtime_id"] = ns.runtime_id
|
|
82
85
|
print_json(client.post("/api/agents/from-template", body))
|
|
83
86
|
else:
|
|
84
87
|
body = {"name": ns.name, "workspace_id": client.workspace_id}
|
|
88
|
+
if ns.runtime_id:
|
|
89
|
+
body["runtime_id"] = ns.runtime_id
|
|
85
90
|
if ns.instructions:
|
|
86
91
|
body["instructions"] = ns.instructions
|
|
87
92
|
print_json(client.post("/api/agents", body))
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
"""graph / knowledge commands (aligned with Go `aicortex graph`)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import os
|
|
7
|
+
import sys
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
from ..cli_context import require_client
|
|
11
|
+
from ..output import print_json
|
|
12
|
+
from ..platform_client import PlatformError
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def add_parser(sub: argparse._SubParsersAction) -> None:
|
|
16
|
+
p = sub.add_parser("graph", help="Workspace Knowledge search/write/ensure")
|
|
17
|
+
gsub = p.add_subparsers(dest="graph_command", required=True)
|
|
18
|
+
|
|
19
|
+
cols = gsub.add_parser("collections", help="List Knowledge collections bound to this Agent")
|
|
20
|
+
cols.add_argument("--agent-id", default="", help="Override AICORTEX_AGENT_ID")
|
|
21
|
+
cols.add_argument("--output", choices=["json", "table"], default="json")
|
|
22
|
+
|
|
23
|
+
ensure = gsub.add_parser(
|
|
24
|
+
"ensure",
|
|
25
|
+
help="Create a Knowledge collection if missing and bind it to this Agent",
|
|
26
|
+
)
|
|
27
|
+
ensure.add_argument("--slug", required=True, help="Collection slug, e.g. APP_DATA_<app_id>")
|
|
28
|
+
ensure.add_argument("--name", default="", help="Display name (defaults to slug)")
|
|
29
|
+
ensure.add_argument("--description", default="")
|
|
30
|
+
ensure.add_argument(
|
|
31
|
+
"--visibility",
|
|
32
|
+
default="workspace",
|
|
33
|
+
choices=["private", "workspace", "restricted", "global"],
|
|
34
|
+
)
|
|
35
|
+
ensure.add_argument("--agent-id", default="", help="Override AICORTEX_AGENT_ID")
|
|
36
|
+
ensure.add_argument("--output", choices=["json", "table"], default="json")
|
|
37
|
+
|
|
38
|
+
search = gsub.add_parser("search")
|
|
39
|
+
search.add_argument("query", nargs="?", default="")
|
|
40
|
+
search.add_argument("--q", default="", help="Query (alternative to positional)")
|
|
41
|
+
search.add_argument("--collection", "--collection-id", dest="collection", default="")
|
|
42
|
+
search.add_argument("--mode", default="auto")
|
|
43
|
+
search.add_argument("-n", type=int, default=10)
|
|
44
|
+
search.add_argument("--output", choices=["json", "table"], default="json")
|
|
45
|
+
|
|
46
|
+
walk = gsub.add_parser("walk")
|
|
47
|
+
walk.add_argument("query", nargs="?", default="")
|
|
48
|
+
walk.add_argument("--q", default="")
|
|
49
|
+
walk.add_argument("--collection", "--collection-id", dest="collection", default="")
|
|
50
|
+
walk.add_argument("-n", type=int, default=5)
|
|
51
|
+
walk.add_argument("--output", choices=["json", "table"], default="json")
|
|
52
|
+
|
|
53
|
+
fetch = gsub.add_parser("fetch")
|
|
54
|
+
fetch.add_argument("--chunk-ids", required=True, help="comma-separated")
|
|
55
|
+
fetch.add_argument("--output", choices=["json", "table"], default="json")
|
|
56
|
+
|
|
57
|
+
write = gsub.add_parser("write")
|
|
58
|
+
write.add_argument("--collection", "--collection-id", dest="collection", default="")
|
|
59
|
+
write.add_argument("--title", required=True)
|
|
60
|
+
write.add_argument("--body", default="")
|
|
61
|
+
write.add_argument("--body-file", default="")
|
|
62
|
+
write.add_argument("--content", default="", help="Alias for --body (legacy)")
|
|
63
|
+
write.add_argument("--agent-id", default="", help="Override AICORTEX_AGENT_ID for binding lookup")
|
|
64
|
+
write.add_argument("--output", choices=["json", "table"], default="json")
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _agent_id(ns: argparse.Namespace) -> str:
|
|
68
|
+
return (
|
|
69
|
+
str(getattr(ns, "agent_id", "") or "").strip()
|
|
70
|
+
or os.environ.get("AICORTEX_AGENT_ID", "").strip()
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _query(ns: argparse.Namespace) -> str:
|
|
75
|
+
return (str(getattr(ns, "query", "") or "").strip() or str(getattr(ns, "q", "") or "").strip())
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _resolve_body(ns: argparse.Namespace) -> str:
|
|
79
|
+
body = str(getattr(ns, "body", "") or "").strip()
|
|
80
|
+
if body:
|
|
81
|
+
return body
|
|
82
|
+
content = str(getattr(ns, "content", "") or "").strip()
|
|
83
|
+
if content:
|
|
84
|
+
return content
|
|
85
|
+
path = str(getattr(ns, "body_file", "") or "").strip()
|
|
86
|
+
if path:
|
|
87
|
+
return Path(path).read_text(encoding="utf-8")
|
|
88
|
+
return ""
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _load_bound_collections(client, agent_id: str) -> list[dict]:
|
|
92
|
+
if agent_id:
|
|
93
|
+
rows = client.get(f"/api/agents/{agent_id}/knowledge")
|
|
94
|
+
else:
|
|
95
|
+
rows = client.get("/api/knowledge/collections")
|
|
96
|
+
if not isinstance(rows, list):
|
|
97
|
+
return []
|
|
98
|
+
out = []
|
|
99
|
+
for c in rows:
|
|
100
|
+
if not isinstance(c, dict):
|
|
101
|
+
continue
|
|
102
|
+
# Skip Agent Memory collections (owner_agent_id set)
|
|
103
|
+
if str(c.get("owner_agent_id") or "").strip():
|
|
104
|
+
continue
|
|
105
|
+
out.append(c)
|
|
106
|
+
return out
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _match_collection(cols: list[dict], key: str) -> dict:
|
|
110
|
+
key = (key or "").strip()
|
|
111
|
+
if not key:
|
|
112
|
+
if len(cols) == 1:
|
|
113
|
+
return cols[0]
|
|
114
|
+
if not cols:
|
|
115
|
+
raise ValueError(
|
|
116
|
+
"no bound Knowledge collections; run: aicortex graph ensure --slug <slug> --name <name>"
|
|
117
|
+
)
|
|
118
|
+
raise ValueError("--collection is required when more than one library is bound")
|
|
119
|
+
kl = key.lower()
|
|
120
|
+
for c in cols:
|
|
121
|
+
if str(c.get("id") or "") == key or str(c.get("slug") or "").lower() == kl:
|
|
122
|
+
return c
|
|
123
|
+
raise ValueError(
|
|
124
|
+
f'collection "{key}" is not bound to this Agent; '
|
|
125
|
+
f"run: aicortex graph ensure --slug {key} --name {key}"
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def run(ns: argparse.Namespace) -> int:
|
|
130
|
+
client = require_client(ns)
|
|
131
|
+
try:
|
|
132
|
+
if ns.graph_command == "collections":
|
|
133
|
+
cols = _load_bound_collections(client, _agent_id(ns))
|
|
134
|
+
rows = [{"id": c.get("id"), "slug": c.get("slug"), "name": c.get("name")} for c in cols]
|
|
135
|
+
print_json({"count": len(rows), "collections": rows})
|
|
136
|
+
return 0
|
|
137
|
+
|
|
138
|
+
if ns.graph_command == "ensure":
|
|
139
|
+
agent_id = _agent_id(ns)
|
|
140
|
+
if not agent_id:
|
|
141
|
+
raise ValueError(
|
|
142
|
+
"AICORTEX_AGENT_ID is required (set automatically during agent runs)"
|
|
143
|
+
)
|
|
144
|
+
slug = str(ns.slug).strip()
|
|
145
|
+
name = str(ns.name or "").strip() or slug
|
|
146
|
+
body = {
|
|
147
|
+
"slug": slug,
|
|
148
|
+
"name": name,
|
|
149
|
+
"description": str(ns.description or ""),
|
|
150
|
+
"visibility": str(ns.visibility or "workspace"),
|
|
151
|
+
}
|
|
152
|
+
print_json(client.post(f"/api/agents/{agent_id}/knowledge/ensure", body))
|
|
153
|
+
return 0
|
|
154
|
+
|
|
155
|
+
if ns.graph_command == "search":
|
|
156
|
+
q = _query(ns)
|
|
157
|
+
if not q:
|
|
158
|
+
raise ValueError("query is required")
|
|
159
|
+
body: dict = {"query": q, "mode": ns.mode, "limit": ns.n}
|
|
160
|
+
if ns.collection:
|
|
161
|
+
body["collection"] = ns.collection
|
|
162
|
+
print_json(client.post("/api/knowledge/search", body))
|
|
163
|
+
return 0
|
|
164
|
+
|
|
165
|
+
if ns.graph_command == "walk":
|
|
166
|
+
q = _query(ns)
|
|
167
|
+
if not q:
|
|
168
|
+
raise ValueError("query is required")
|
|
169
|
+
body = {"query": q, "limit": ns.n}
|
|
170
|
+
if ns.collection:
|
|
171
|
+
body["collection"] = ns.collection
|
|
172
|
+
print_json(client.post("/api/knowledge/walk", body))
|
|
173
|
+
return 0
|
|
174
|
+
|
|
175
|
+
if ns.graph_command == "fetch":
|
|
176
|
+
ids = [x.strip() for x in ns.chunk_ids.split(",") if x.strip()]
|
|
177
|
+
if not ids:
|
|
178
|
+
raise ValueError("--chunk-ids is required")
|
|
179
|
+
print_json(client.post("/api/knowledge/fetch", {"chunk_ids": ids}))
|
|
180
|
+
return 0
|
|
181
|
+
|
|
182
|
+
if ns.graph_command == "write":
|
|
183
|
+
body_text = _resolve_body(ns)
|
|
184
|
+
if not body_text.strip():
|
|
185
|
+
raise ValueError("--body, --body-file, or --content is required")
|
|
186
|
+
cols = _load_bound_collections(client, _agent_id(ns))
|
|
187
|
+
col = _match_collection(cols, ns.collection)
|
|
188
|
+
print_json(
|
|
189
|
+
client.post(
|
|
190
|
+
f"/api/knowledge/collections/{col['id']}/documents",
|
|
191
|
+
{
|
|
192
|
+
"title": ns.title,
|
|
193
|
+
"body": body_text,
|
|
194
|
+
"source_id": "aicortex-graph",
|
|
195
|
+
"source_kind": "document",
|
|
196
|
+
"origin": "aicortex",
|
|
197
|
+
},
|
|
198
|
+
)
|
|
199
|
+
)
|
|
200
|
+
return 0
|
|
201
|
+
except (PlatformError, ValueError, OSError) as e:
|
|
202
|
+
print(str(e), file=sys.stderr)
|
|
203
|
+
return 1
|
|
204
|
+
return 2
|
|
@@ -37,7 +37,7 @@ def run(ns: argparse.Namespace) -> int:
|
|
|
37
37
|
if ns.memory_command in {"ensure", "show"}:
|
|
38
38
|
print_json(client.get(f"/api/agents/{ns.agent_id}/memory"))
|
|
39
39
|
return 0
|
|
40
|
-
body = {"title": ns.title, "
|
|
40
|
+
body = {"title": ns.title, "body": ns.content, "append": ns.memory_command == "append"}
|
|
41
41
|
print_json(client.post(f"/api/knowledge/collections/{ns.collection_id}/documents", body))
|
|
42
42
|
return 0
|
|
43
43
|
except PlatformError as e:
|
|
@@ -332,9 +332,11 @@ def detect_agent_version(executable: str) -> str:
|
|
|
332
332
|
import re
|
|
333
333
|
import subprocess
|
|
334
334
|
|
|
335
|
+
from .procutil import run_no_window
|
|
336
|
+
|
|
335
337
|
for args in ([executable, "--version"], [executable, "version"], [executable, "-V"]):
|
|
336
338
|
try:
|
|
337
|
-
proc =
|
|
339
|
+
proc = run_no_window(
|
|
338
340
|
args,
|
|
339
341
|
capture_output=True,
|
|
340
342
|
text=True,
|