aicortexpy 0.2.2__tar.gz → 0.2.4__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.4}/PKG-INFO +1 -1
- {aicortexpy-0.2.2 → aicortexpy-0.2.4}/aicortexpy/__init__.py +1 -1
- {aicortexpy-0.2.2 → aicortexpy-0.2.4}/aicortexpy/acp_conn.py +48 -13
- {aicortexpy-0.2.2 → aicortexpy-0.2.4}/aicortexpy/acp_errors.py +23 -0
- aicortexpy-0.2.4/aicortexpy/app_workdir.py +72 -0
- {aicortexpy-0.2.2 → aicortexpy-0.2.4}/aicortexpy/artifact_ws.py +5 -1
- {aicortexpy-0.2.2 → aicortexpy-0.2.4}/aicortexpy/artifacts.py +6 -0
- {aicortexpy-0.2.2 → aicortexpy-0.2.4}/aicortexpy/auto_commit.py +3 -1
- {aicortexpy-0.2.2 → aicortexpy-0.2.4}/aicortexpy/cli.py +10 -0
- {aicortexpy-0.2.2 → aicortexpy-0.2.4}/aicortexpy/client.py +116 -1
- {aicortexpy-0.2.2 → aicortexpy-0.2.4}/aicortexpy/commands/agent_cmd.py +5 -0
- aicortexpy-0.2.4/aicortexpy/commands/graph_cmd.py +204 -0
- {aicortexpy-0.2.2 → aicortexpy-0.2.4}/aicortexpy/commands/memory_cmd.py +1 -1
- {aicortexpy-0.2.2 → aicortexpy-0.2.4}/aicortexpy/config.py +7 -2
- {aicortexpy-0.2.2 → aicortexpy-0.2.4}/aicortexpy/daemon.py +28 -10
- aicortexpy-0.2.4/aicortexpy/data/default_skills/aicortex-graph/SKILL.md +46 -0
- {aicortexpy-0.2.2 → aicortexpy-0.2.4}/aicortexpy/execenv.py +13 -1
- {aicortexpy-0.2.2 → aicortexpy-0.2.4}/aicortexpy/models.py +12 -3
- {aicortexpy-0.2.2 → aicortexpy-0.2.4}/aicortexpy/openclaw_config.py +3 -1
- {aicortexpy-0.2.2 → aicortexpy-0.2.4}/aicortexpy/process.py +16 -3
- aicortexpy-0.2.4/aicortexpy/procutil.py +113 -0
- {aicortexpy-0.2.2 → aicortexpy-0.2.4}/aicortexpy/repo.py +13 -12
- {aicortexpy-0.2.2 → aicortexpy-0.2.4}/aicortexpy/runner.py +17 -6
- {aicortexpy-0.2.2 → aicortexpy-0.2.4}/aicortexpy/runtime_config.py +73 -1
- {aicortexpy-0.2.2 → aicortexpy-0.2.4}/aicortexpy/terminal.py +3 -0
- {aicortexpy-0.2.2 → aicortexpy-0.2.4/aicortexpy.egg-info}/PKG-INFO +1 -1
- {aicortexpy-0.2.2 → aicortexpy-0.2.4}/aicortexpy.egg-info/SOURCES.txt +3 -1
- {aicortexpy-0.2.2 → aicortexpy-0.2.4}/pyproject.toml +1 -1
- aicortexpy-0.2.4/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.4}/MANIFEST.in +0 -0
- {aicortexpy-0.2.2 → aicortexpy-0.2.4}/README.md +0 -0
- {aicortexpy-0.2.2 → aicortexpy-0.2.4}/aicortex/__init__.py +0 -0
- {aicortexpy-0.2.2 → aicortexpy-0.2.4}/aicortex/__main__.py +0 -0
- {aicortexpy-0.2.2 → aicortexpy-0.2.4}/aicortexpy/__main__.py +0 -0
- {aicortexpy-0.2.2 → aicortexpy-0.2.4}/aicortexpy/acp_client.py +0 -0
- {aicortexpy-0.2.2 → aicortexpy-0.2.4}/aicortexpy/agent_env.py +0 -0
- {aicortexpy-0.2.2 → aicortexpy-0.2.4}/aicortexpy/attachments.py +0 -0
- {aicortexpy-0.2.2 → aicortexpy-0.2.4}/aicortexpy/capabilities.py +0 -0
- {aicortexpy-0.2.2 → aicortexpy-0.2.4}/aicortexpy/chat_acp_pool.py +0 -0
- {aicortexpy-0.2.2 → aicortexpy-0.2.4}/aicortexpy/cli_context.py +0 -0
- {aicortexpy-0.2.2 → aicortexpy-0.2.4}/aicortexpy/codex_config.py +0 -0
- {aicortexpy-0.2.2 → aicortexpy-0.2.4}/aicortexpy/commands/__init__.py +0 -0
- {aicortexpy-0.2.2 → aicortexpy-0.2.4}/aicortexpy/commands/attachment_cmd.py +0 -0
- {aicortexpy-0.2.2 → aicortexpy-0.2.4}/aicortexpy/commands/auth_cmd.py +0 -0
- {aicortexpy-0.2.2 → aicortexpy-0.2.4}/aicortexpy/commands/autopilot_cmd.py +0 -0
- {aicortexpy-0.2.2 → aicortexpy-0.2.4}/aicortexpy/commands/config_cmd.py +0 -0
- {aicortexpy-0.2.2 → aicortexpy-0.2.4}/aicortexpy/commands/daemon_cmd.py +0 -0
- {aicortexpy-0.2.2 → aicortexpy-0.2.4}/aicortexpy/commands/issue_cmd.py +0 -0
- {aicortexpy-0.2.2 → aicortexpy-0.2.4}/aicortexpy/commands/label_cmd.py +0 -0
- {aicortexpy-0.2.2 → aicortexpy-0.2.4}/aicortexpy/commands/meeting_cmd.py +0 -0
- {aicortexpy-0.2.2 → aicortexpy-0.2.4}/aicortexpy/commands/project_cmd.py +0 -0
- {aicortexpy-0.2.2 → aicortexpy-0.2.4}/aicortexpy/commands/repo_cmd.py +0 -0
- {aicortexpy-0.2.2 → aicortexpy-0.2.4}/aicortexpy/commands/runtime_cmd.py +0 -0
- {aicortexpy-0.2.2 → aicortexpy-0.2.4}/aicortexpy/commands/setup_cmd.py +0 -0
- {aicortexpy-0.2.2 → aicortexpy-0.2.4}/aicortexpy/commands/skill_cmd.py +0 -0
- {aicortexpy-0.2.2 → aicortexpy-0.2.4}/aicortexpy/commands/squad_cmd.py +0 -0
- {aicortexpy-0.2.2 → aicortexpy-0.2.4}/aicortexpy/commands/update_cmd.py +0 -0
- {aicortexpy-0.2.2 → aicortexpy-0.2.4}/aicortexpy/commands/version_cmd.py +0 -0
- {aicortexpy-0.2.2 → aicortexpy-0.2.4}/aicortexpy/commands/workspace_cmd.py +0 -0
- {aicortexpy-0.2.2 → aicortexpy-0.2.4}/aicortexpy/data/default_skills/agent-memory/SKILL.md +0 -0
- {aicortexpy-0.2.2 → aicortexpy-0.2.4}/aicortexpy/data/default_skills/aicorkit-atlassian/SKILL.md +0 -0
- {aicortexpy-0.2.2 → aicortexpy-0.2.4}/aicortexpy/data/default_skills/aicorkit-graph/SKILL.md +0 -0
- {aicortexpy-0.2.2 → aicortexpy-0.2.4}/aicortexpy/data/default_skills/aicorkit-outlook/SKILL.md +0 -0
- {aicortexpy-0.2.2 → aicortexpy-0.2.4}/aicortexpy/data/default_skills/aicorkit-wxmsg/SKILL.md +0 -0
- {aicortexpy-0.2.2 → aicortexpy-0.2.4}/aicortexpy/data/default_skills/aicortex-basics/SKILL.md +0 -0
- {aicortexpy-0.2.2 → aicortexpy-0.2.4}/aicortexpy/data/default_skills/aicortex-bridge-cdp/SKILL.md +0 -0
- {aicortexpy-0.2.2 → aicortexpy-0.2.4}/aicortexpy/data/default_skills/aicortex-cli-ops/SKILL.md +0 -0
- {aicortexpy-0.2.2 → aicortexpy-0.2.4}/aicortexpy/data/default_skills/graphify/SKILL.md +0 -0
- {aicortexpy-0.2.2 → aicortexpy-0.2.4}/aicortexpy/data/default_skills/question-form/SKILL.md +0 -0
- {aicortexpy-0.2.2 → aicortexpy-0.2.4}/aicortexpy/data/default_skills/test-report-generator/SKILL.md +0 -0
- {aicortexpy-0.2.2 → aicortexpy-0.2.4}/aicortexpy/data/default_skills/test-report-generator/scripts/generate_cases.py +0 -0
- {aicortexpy-0.2.2 → aicortexpy-0.2.4}/aicortexpy/data/default_skills/test-report-generator/scripts/generate_report.py +0 -0
- {aicortexpy-0.2.2 → aicortexpy-0.2.4}/aicortexpy/design_seed.py +0 -0
- {aicortexpy-0.2.2 → aicortexpy-0.2.4}/aicortexpy/diskusage.py +0 -0
- {aicortexpy-0.2.2 → aicortexpy-0.2.4}/aicortexpy/gc.py +0 -0
- {aicortexpy-0.2.2 → aicortexpy-0.2.4}/aicortexpy/handoff_bundle.py +0 -0
- {aicortexpy-0.2.2 → aicortexpy-0.2.4}/aicortexpy/health.py +0 -0
- {aicortexpy-0.2.2 → aicortexpy-0.2.4}/aicortexpy/id_resolve.py +0 -0
- {aicortexpy-0.2.2 → aicortexpy-0.2.4}/aicortexpy/local_skills.py +0 -0
- {aicortexpy-0.2.2 → aicortexpy-0.2.4}/aicortexpy/oauth_login.py +0 -0
- {aicortexpy-0.2.2 → aicortexpy-0.2.4}/aicortexpy/output.py +0 -0
- {aicortexpy-0.2.2 → aicortexpy-0.2.4}/aicortexpy/permission.py +0 -0
- {aicortexpy-0.2.2 → aicortexpy-0.2.4}/aicortexpy/pinned_workdir.py +0 -0
- {aicortexpy-0.2.2 → aicortexpy-0.2.4}/aicortexpy/platform_client.py +0 -0
- {aicortexpy-0.2.2 → aicortexpy-0.2.4}/aicortexpy/platform_info.py +0 -0
- {aicortexpy-0.2.2 → aicortexpy-0.2.4}/aicortexpy/poisoned.py +0 -0
- {aicortexpy-0.2.2 → aicortexpy-0.2.4}/aicortexpy/prompt.py +0 -0
- {aicortexpy-0.2.2 → aicortexpy-0.2.4}/aicortexpy/sandbox.py +0 -0
- {aicortexpy-0.2.2 → aicortexpy-0.2.4}/aicortexpy/security.py +0 -0
- {aicortexpy-0.2.2 → aicortexpy-0.2.4}/aicortexpy/stream.py +0 -0
- {aicortexpy-0.2.2 → aicortexpy-0.2.4}/aicortexpy/tunnel.py +0 -0
- {aicortexpy-0.2.2 → aicortexpy-0.2.4}/aicortexpy/wakeup.py +0 -0
- {aicortexpy-0.2.2 → aicortexpy-0.2.4}/aicortexpy.egg-info/dependency_links.txt +0 -0
- {aicortexpy-0.2.2 → aicortexpy-0.2.4}/aicortexpy.egg-info/entry_points.txt +0 -0
- {aicortexpy-0.2.2 → aicortexpy-0.2.4}/aicortexpy.egg-info/requires.txt +0 -0
- {aicortexpy-0.2.2 → aicortexpy-0.2.4}/aicortexpy.egg-info/top_level.txt +0 -0
- {aicortexpy-0.2.2 → aicortexpy-0.2.4}/scripts/aicortex +0 -0
- {aicortexpy-0.2.2 → aicortexpy-0.2.4}/scripts/aicortex.cmd +0 -0
- {aicortexpy-0.2.2 → aicortexpy-0.2.4}/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
|
|
|
@@ -33,9 +37,20 @@ HANDSHAKE_TIMEOUT = 120.0
|
|
|
33
37
|
PROMPT_TIMEOUT = 20 * 60.0
|
|
34
38
|
STDOUT_MAX_LINE = 10 * 1024 * 1024 # match Go scanner max token
|
|
35
39
|
|
|
40
|
+
# UI / agent form may send these; ACP session/set_model rejects them (e.g. Cursor -32602).
|
|
41
|
+
_ACP_MODEL_SENTINELS = frozenset({"", "auto", "default", "none"})
|
|
42
|
+
|
|
36
43
|
MessageCB = Callable[[dict[str, Any]], None]
|
|
37
44
|
|
|
38
45
|
|
|
46
|
+
def normalize_acp_model(model: str | None) -> str:
|
|
47
|
+
"""Return a concrete model id for ACP, or '' to keep the agent default."""
|
|
48
|
+
m = (model or "").strip()
|
|
49
|
+
if m.lower() in _ACP_MODEL_SENTINELS:
|
|
50
|
+
return ""
|
|
51
|
+
return m
|
|
52
|
+
|
|
53
|
+
|
|
39
54
|
@dataclass
|
|
40
55
|
class ACPPromptResult:
|
|
41
56
|
status: str
|
|
@@ -73,15 +88,16 @@ def kiro_acp_supported(executable: str) -> bool:
|
|
|
73
88
|
|
|
74
89
|
|
|
75
90
|
def _acp_help_ok(executable: str, *, extra_before: list[str]) -> bool:
|
|
91
|
+
from .procutil import run_no_window
|
|
92
|
+
|
|
76
93
|
path = shutil.which(executable) or executable
|
|
77
94
|
args = [path, *extra_before]
|
|
78
95
|
try:
|
|
79
|
-
proc =
|
|
96
|
+
proc = run_no_window(
|
|
80
97
|
args,
|
|
81
98
|
capture_output=True,
|
|
82
99
|
timeout=5,
|
|
83
100
|
check=False,
|
|
84
|
-
**_hide_window(),
|
|
85
101
|
)
|
|
86
102
|
return proc.returncode == 0
|
|
87
103
|
except (OSError, subprocess.TimeoutExpired):
|
|
@@ -89,11 +105,9 @@ def _acp_help_ok(executable: str, *, extra_before: list[str]) -> bool:
|
|
|
89
105
|
|
|
90
106
|
|
|
91
107
|
def _hide_window() -> dict:
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
startup.dwFlags |= subprocess.STARTF_USESHOWWINDOW
|
|
96
|
-
return {"startupinfo": startup}
|
|
108
|
+
from .procutil import popen_kwargs_hidden_console
|
|
109
|
+
|
|
110
|
+
return popen_kwargs_hidden_console()
|
|
97
111
|
|
|
98
112
|
|
|
99
113
|
def choose_cursor_invocation(executable: str, args: list[str]) -> tuple[str, list[str]]:
|
|
@@ -344,6 +358,8 @@ class _BaseACPConn:
|
|
|
344
358
|
status, error = promote_acp_result_on_provider_error(
|
|
345
359
|
status, error, output, self._sniffer
|
|
346
360
|
)
|
|
361
|
+
if self.provider == "kiro" and status == "failed" and error:
|
|
362
|
+
error = enrich_kiro_acp_error(error)
|
|
347
363
|
|
|
348
364
|
usage = None
|
|
349
365
|
with client._usage_lock:
|
|
@@ -528,26 +544,43 @@ class _BaseACPConn:
|
|
|
528
544
|
raise RuntimeError(f"{self.provider} acp session/new returned no session ID")
|
|
529
545
|
self._session_id = session_id
|
|
530
546
|
self._client.session_id = session_id
|
|
531
|
-
|
|
547
|
+
# UI may send sentinel "auto"/"default"; ACP session/set_model rejects those.
|
|
548
|
+
effective_model = normalize_acp_model(model)
|
|
549
|
+
self._model = effective_model
|
|
532
550
|
log.info("%s acp session ready session_id=%s", self.provider, session_id)
|
|
533
|
-
if not
|
|
551
|
+
if not effective_model:
|
|
552
|
+
if (model or "").strip():
|
|
553
|
+
log.info(
|
|
554
|
+
"%s acp skipping set_model for sentinel model=%r",
|
|
555
|
+
self.provider,
|
|
556
|
+
model,
|
|
557
|
+
)
|
|
534
558
|
return
|
|
535
559
|
try:
|
|
536
560
|
if self.model_via_config_option:
|
|
537
561
|
self._client.request(
|
|
538
562
|
"session/set_config_option",
|
|
539
|
-
{
|
|
563
|
+
{
|
|
564
|
+
"sessionId": session_id,
|
|
565
|
+
"configId": "model",
|
|
566
|
+
"value": effective_model,
|
|
567
|
+
},
|
|
540
568
|
timeout=handshake_timeout,
|
|
541
569
|
)
|
|
542
570
|
else:
|
|
543
571
|
self._client.request(
|
|
544
572
|
"session/set_model",
|
|
545
|
-
{"sessionId": session_id, "modelId":
|
|
573
|
+
{"sessionId": session_id, "modelId": effective_model},
|
|
546
574
|
timeout=handshake_timeout,
|
|
547
575
|
)
|
|
548
576
|
except Exception as e:
|
|
549
577
|
if self.soft_set_model:
|
|
550
|
-
log.
|
|
578
|
+
log.warning(
|
|
579
|
+
"%s acp set_model soft-failed model=%r: %s (continuing with agent default)",
|
|
580
|
+
self.provider,
|
|
581
|
+
effective_model,
|
|
582
|
+
e,
|
|
583
|
+
)
|
|
551
584
|
return
|
|
552
585
|
self.close()
|
|
553
586
|
raise RuntimeError(f"{self.provider} acp set model failed: {e}") from e
|
|
@@ -556,6 +589,8 @@ class _BaseACPConn:
|
|
|
556
589
|
class CursorACPConn(_BaseACPConn):
|
|
557
590
|
provider = "cursor"
|
|
558
591
|
require_set_model = True
|
|
592
|
+
# Prefer pooled chat over hard-failing on an unknown model id.
|
|
593
|
+
soft_set_model = True
|
|
559
594
|
|
|
560
595
|
@classmethod
|
|
561
596
|
def open(
|
|
@@ -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)
|
|
@@ -63,11 +63,21 @@ def _setup_logging(
|
|
|
63
63
|
level = logging.INFO
|
|
64
64
|
else:
|
|
65
65
|
level = logging.WARNING
|
|
66
|
+
# Windows consoles are often GBK; force UTF-8 so agent names like 小政 log cleanly.
|
|
67
|
+
for stream in (sys.stdout, sys.stderr):
|
|
68
|
+
reconf = getattr(stream, "reconfigure", None)
|
|
69
|
+
if callable(reconf):
|
|
70
|
+
try:
|
|
71
|
+
reconf(encoding="utf-8", errors="replace")
|
|
72
|
+
except Exception:
|
|
73
|
+
pass
|
|
66
74
|
logging.basicConfig(
|
|
67
75
|
level=level,
|
|
68
76
|
format="%(asctime)s %(levelname)s %(name)s: %(message)s",
|
|
69
77
|
datefmt="%H:%M:%S",
|
|
70
78
|
force=True,
|
|
79
|
+
encoding="utf-8",
|
|
80
|
+
errors="replace",
|
|
71
81
|
)
|
|
72
82
|
|
|
73
83
|
|
|
@@ -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))
|