aictx 0.3.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.
- aictx/__init__.py +4 -0
- aictx/__main__.py +3 -0
- aictx/_version.py +1 -0
- aictx/adapters.py +200 -0
- aictx/agent_runtime.py +201 -0
- aictx/cli.py +585 -0
- aictx/core_runtime.py +1877 -0
- aictx/global_metrics.py +759 -0
- aictx/middleware.py +401 -0
- aictx/runner_integrations.py +372 -0
- aictx/runtime_compat.py +130 -0
- aictx/runtime_contract.py +229 -0
- aictx/runtime_cost.py +445 -0
- aictx/runtime_failure.py +244 -0
- aictx/runtime_graph.py +339 -0
- aictx/runtime_io.py +101 -0
- aictx/runtime_knowledge.py +1098 -0
- aictx/runtime_launcher.py +108 -0
- aictx/runtime_memory.py +280 -0
- aictx/runtime_metrics.py +125 -0
- aictx/runtime_task_memory.py +167 -0
- aictx/runtime_tasks.py +302 -0
- aictx/runtime_versioning.py +92 -0
- aictx/scaffold.py +224 -0
- aictx/state.py +129 -0
- aictx/templates/context_packet_schema.json +34 -0
- aictx/templates/model_routing.json +47 -0
- aictx/templates/user_preferences.json +74 -0
- aictx-0.3.0.dist-info/METADATA +170 -0
- aictx-0.3.0.dist-info/RECORD +34 -0
- aictx-0.3.0.dist-info/WHEEL +5 -0
- aictx-0.3.0.dist-info/entry_points.txt +2 -0
- aictx-0.3.0.dist-info/licenses/LICENSE +21 -0
- aictx-0.3.0.dist-info/top_level.txt +1 -0
aictx/__init__.py
ADDED
aictx/__main__.py
ADDED
aictx/_version.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.3.0"
|
aictx/adapters.py
ADDED
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import stat
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from .state import ENGINE_HOME, REPO_ADAPTERS_DIR, write_json, read_json
|
|
8
|
+
|
|
9
|
+
GLOBAL_ADAPTERS_DIR = ENGINE_HOME / "adapters"
|
|
10
|
+
GLOBAL_ADAPTERS_REGISTRY_PATH = GLOBAL_ADAPTERS_DIR / "registry.json"
|
|
11
|
+
GLOBAL_ADAPTERS_BIN_DIR = GLOBAL_ADAPTERS_DIR / "bin"
|
|
12
|
+
GLOBAL_ADAPTERS_INSTALL_STATUS_PATH = GLOBAL_ADAPTERS_DIR / "install_status.json"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def adapter_runtime_contract(adapter_id: str) -> dict[str, Any]:
|
|
16
|
+
return {
|
|
17
|
+
"runtime_entrypoint": "aictx internal run-execution",
|
|
18
|
+
"integration_mode": "wrapper",
|
|
19
|
+
"auto_prepare_finalize": True,
|
|
20
|
+
"requires_request_context": True,
|
|
21
|
+
"wrapper_env": [
|
|
22
|
+
"AICTX_REQUEST",
|
|
23
|
+
"AICTX_REPO",
|
|
24
|
+
"AICTX_EXECUTION_ID",
|
|
25
|
+
"AICTX_AGENT_ID",
|
|
26
|
+
"AICTX_TASK_TYPE",
|
|
27
|
+
"AICTX_EXECUTION_MODE",
|
|
28
|
+
"AICTX_VALIDATED_LEARNING",
|
|
29
|
+
],
|
|
30
|
+
"wrapper_script_name": f"aictx-{adapter_id}-auto",
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def adapter_profiles() -> dict[str, dict[str, Any]]:
|
|
35
|
+
return {
|
|
36
|
+
"generic": {
|
|
37
|
+
"adapter_id": "generic",
|
|
38
|
+
"display_name": "Generic multi-LLM runner",
|
|
39
|
+
"family": "multi_llm",
|
|
40
|
+
"middleware_always_on": True,
|
|
41
|
+
"explicit_skill_metadata": False,
|
|
42
|
+
"structured_skill_metadata": True,
|
|
43
|
+
"heuristic_skill_fallback": True,
|
|
44
|
+
"auto_installed": True,
|
|
45
|
+
"runtime_contract": adapter_runtime_contract("generic"),
|
|
46
|
+
},
|
|
47
|
+
"codex": {
|
|
48
|
+
"adapter_id": "codex",
|
|
49
|
+
"display_name": "OpenAI Codex",
|
|
50
|
+
"family": "openai_codex",
|
|
51
|
+
"middleware_always_on": True,
|
|
52
|
+
"explicit_skill_metadata": True,
|
|
53
|
+
"structured_skill_metadata": True,
|
|
54
|
+
"heuristic_skill_fallback": True,
|
|
55
|
+
"expected_skill_metadata_fields": ["skill_id", "skill_name", "skill_path", "source"],
|
|
56
|
+
"auto_installed": True,
|
|
57
|
+
"runtime_contract": adapter_runtime_contract("codex"),
|
|
58
|
+
},
|
|
59
|
+
"claude": {
|
|
60
|
+
"adapter_id": "claude",
|
|
61
|
+
"display_name": "Anthropic Claude",
|
|
62
|
+
"family": "anthropic_claude",
|
|
63
|
+
"middleware_always_on": True,
|
|
64
|
+
"explicit_skill_metadata": True,
|
|
65
|
+
"structured_skill_metadata": True,
|
|
66
|
+
"heuristic_skill_fallback": True,
|
|
67
|
+
"expected_skill_metadata_fields": ["skill_id", "skill_name", "skill_path", "source"],
|
|
68
|
+
"auto_installed": True,
|
|
69
|
+
"runtime_contract": adapter_runtime_contract("claude"),
|
|
70
|
+
},
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def adapter_registry_payload(scope: str) -> dict[str, Any]:
|
|
75
|
+
profiles = adapter_profiles()
|
|
76
|
+
return {
|
|
77
|
+
"version": 1,
|
|
78
|
+
"scope": scope,
|
|
79
|
+
"default_adapter_id": "generic",
|
|
80
|
+
"supported_adapters": sorted(profiles.keys()),
|
|
81
|
+
"middleware_mode": "always_on",
|
|
82
|
+
"skill_detection_contract": {
|
|
83
|
+
"authoritative_signal": "explicit_runner_metadata",
|
|
84
|
+
"structured_fallback": True,
|
|
85
|
+
"heuristic_fallback": True,
|
|
86
|
+
},
|
|
87
|
+
"runtime_contract": {
|
|
88
|
+
"entrypoint": "aictx internal run-execution",
|
|
89
|
+
"integration_mode": "wrapper",
|
|
90
|
+
"supported_runners": sorted(profiles.keys()),
|
|
91
|
+
},
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def write_executable(path: Path, content: str) -> None:
|
|
96
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
97
|
+
path.write_text(content, encoding="utf-8")
|
|
98
|
+
path.chmod(path.stat().st_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def render_wrapper_script(adapter_id: str) -> str:
|
|
102
|
+
return f"""#!/bin/sh
|
|
103
|
+
set -eu
|
|
104
|
+
REQUEST="${{AICTX_REQUEST:-}}"
|
|
105
|
+
if [ -z "$REQUEST" ]; then
|
|
106
|
+
echo "AICTX_REQUEST must be set for {adapter_id} auto wrapper." >&2
|
|
107
|
+
exit 64
|
|
108
|
+
fi
|
|
109
|
+
REPO="${{AICTX_REPO:-.}}"
|
|
110
|
+
EXEC_ID="${{AICTX_EXECUTION_ID:-auto}}"
|
|
111
|
+
AGENT_ID="${{AICTX_AGENT_ID:-{adapter_id}}}"
|
|
112
|
+
TASK_TYPE="${{AICTX_TASK_TYPE:-}}"
|
|
113
|
+
EXEC_MODE="${{AICTX_EXECUTION_MODE:-plain}}"
|
|
114
|
+
VALIDATED="${{AICTX_VALIDATED_LEARNING:-0}}"
|
|
115
|
+
if [ "$VALIDATED" = "1" ] || [ "$VALIDATED" = "true" ]; then
|
|
116
|
+
VALIDATED_FLAG="--validated-learning"
|
|
117
|
+
else
|
|
118
|
+
VALIDATED_FLAG=""
|
|
119
|
+
fi
|
|
120
|
+
if [ -n "$TASK_TYPE" ]; then
|
|
121
|
+
exec aictx internal run-execution --repo "$REPO" --request "$REQUEST" --agent-id "$AGENT_ID" --adapter-id "{adapter_id}" --execution-id "$EXEC_ID" --execution-mode "$EXEC_MODE" $VALIDATED_FLAG --task-type "$TASK_TYPE" -- "$@"
|
|
122
|
+
fi
|
|
123
|
+
exec aictx internal run-execution --repo "$REPO" --request "$REQUEST" --agent-id "$AGENT_ID" --adapter-id "{adapter_id}" --execution-id "$EXEC_ID" --execution-mode "$EXEC_MODE" $VALIDATED_FLAG -- "$@"
|
|
124
|
+
"""
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def install_adapter_wrappers() -> list[Path]:
|
|
128
|
+
created: list[Path] = []
|
|
129
|
+
GLOBAL_ADAPTERS_BIN_DIR.mkdir(parents=True, exist_ok=True)
|
|
130
|
+
for adapter_id in sorted(adapter_profiles().keys()):
|
|
131
|
+
wrapper_name = adapter_runtime_contract(adapter_id)["wrapper_script_name"]
|
|
132
|
+
path = GLOBAL_ADAPTERS_BIN_DIR / wrapper_name
|
|
133
|
+
write_executable(path, render_wrapper_script(adapter_id))
|
|
134
|
+
created.append(path)
|
|
135
|
+
return created
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def adapter_install_status_payload(wrapper_paths: list[Path]) -> dict[str, Any]:
|
|
139
|
+
profiles = adapter_profiles()
|
|
140
|
+
wrappers = {
|
|
141
|
+
adapter_id: str(GLOBAL_ADAPTERS_BIN_DIR / profiles[adapter_id]["runtime_contract"]["wrapper_script_name"])
|
|
142
|
+
for adapter_id in sorted(profiles.keys())
|
|
143
|
+
}
|
|
144
|
+
return {
|
|
145
|
+
"version": 1,
|
|
146
|
+
"engine_home": str(ENGINE_HOME),
|
|
147
|
+
"integration_mode": "wrapper",
|
|
148
|
+
"runtime_entrypoint": "aictx internal run-execution",
|
|
149
|
+
"supported_runners": sorted(profiles.keys()),
|
|
150
|
+
"wrappers": wrappers,
|
|
151
|
+
"artifacts": [str(path) for path in wrapper_paths],
|
|
152
|
+
"status": "wrapper_ready",
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def install_global_adapters() -> list[Path]:
|
|
157
|
+
GLOBAL_ADAPTERS_DIR.mkdir(parents=True, exist_ok=True)
|
|
158
|
+
write_json(GLOBAL_ADAPTERS_REGISTRY_PATH, adapter_registry_payload("global"))
|
|
159
|
+
created = [GLOBAL_ADAPTERS_REGISTRY_PATH]
|
|
160
|
+
for adapter_id, payload in adapter_profiles().items():
|
|
161
|
+
path = GLOBAL_ADAPTERS_DIR / f"{adapter_id}.json"
|
|
162
|
+
write_json(path, payload)
|
|
163
|
+
created.append(path)
|
|
164
|
+
wrapper_paths = install_adapter_wrappers()
|
|
165
|
+
created.extend(wrapper_paths)
|
|
166
|
+
write_json(GLOBAL_ADAPTERS_INSTALL_STATUS_PATH, adapter_install_status_payload(wrapper_paths))
|
|
167
|
+
created.append(GLOBAL_ADAPTERS_INSTALL_STATUS_PATH)
|
|
168
|
+
return created
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def install_repo_adapters(repo: Path) -> list[Path]:
|
|
172
|
+
adapters_dir = repo / REPO_ADAPTERS_DIR
|
|
173
|
+
adapters_dir.mkdir(parents=True, exist_ok=True)
|
|
174
|
+
registry_path = adapters_dir / "registry.json"
|
|
175
|
+
write_json(registry_path, adapter_registry_payload("repo"))
|
|
176
|
+
created = [registry_path]
|
|
177
|
+
for adapter_id, payload in adapter_profiles().items():
|
|
178
|
+
path = adapters_dir / f"{adapter_id}.json"
|
|
179
|
+
write_json(path, payload)
|
|
180
|
+
created.append(path)
|
|
181
|
+
return created
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def resolve_adapter_profile(adapter_id: str | None, agent_id: str | None = None, repo_root: Path | None = None) -> dict[str, Any]:
|
|
185
|
+
requested = str(adapter_id or "").strip().lower()
|
|
186
|
+
agent = str(agent_id or "").strip().lower()
|
|
187
|
+
profiles = adapter_profiles()
|
|
188
|
+
resolved_id = requested if requested in profiles else "generic"
|
|
189
|
+
if resolved_id == "generic":
|
|
190
|
+
if "codex" in requested or "codex" in agent:
|
|
191
|
+
resolved_id = "codex"
|
|
192
|
+
elif "claude" in requested or "claude" in agent:
|
|
193
|
+
resolved_id = "claude"
|
|
194
|
+
if repo_root:
|
|
195
|
+
repo_path = repo_root / REPO_ADAPTERS_DIR / f"{resolved_id}.json"
|
|
196
|
+
if repo_path.exists():
|
|
197
|
+
payload = read_json(repo_path, {})
|
|
198
|
+
if payload:
|
|
199
|
+
return payload
|
|
200
|
+
return dict(profiles[resolved_id])
|
aictx/agent_runtime.py
ADDED
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import shutil
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from .state import ENGINE_HOME, GLOBAL_METRICS_DIR
|
|
7
|
+
|
|
8
|
+
AGENTS_START = "<!-- AICTX:START -->"
|
|
9
|
+
AGENTS_END = "<!-- AICTX:END -->"
|
|
10
|
+
GLOBAL_RUNTIME_PATH = ENGINE_HOME / "agent_runtime.md"
|
|
11
|
+
GLOBAL_RUNTIME_MANIFEST_PATH = ENGINE_HOME / "agent_runtime_manifest.json"
|
|
12
|
+
LOCAL_RUNTIME_PATH = Path('.ai_context_engine') / 'agent_runtime.md'
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def render_agent_runtime(engine_home: Path | None = None) -> str:
|
|
16
|
+
engine_home = engine_home or ENGINE_HOME
|
|
17
|
+
global_metrics_dir = engine_home / ".ai_context_global_metrics"
|
|
18
|
+
return f"""# AI Context Engine agent runtime
|
|
19
|
+
|
|
20
|
+
Use this runtime guide after repository initialization with `aictx init`.
|
|
21
|
+
|
|
22
|
+
## Startup / bootstrap
|
|
23
|
+
- Read `.ai_context_engine/memory/derived_boot_summary.json` first when present.
|
|
24
|
+
- Apply `.ai_context_engine/memory/user_preferences.json` as defaults unless the current prompt overrides them.
|
|
25
|
+
- Repo-local preferences may keep communication mode disabled by default; explicit user requests still override.
|
|
26
|
+
- `aictx` acts as always-on execution middleware for agent runs in initialized repos: run a base prehook before execution and a base posthook after execution.
|
|
27
|
+
- Use the engine as the first low-cost memory layer before deeper repo analysis.
|
|
28
|
+
- Do not hand-edit generated `.ai_context_*` artifacts.
|
|
29
|
+
|
|
30
|
+
## Retrieval order
|
|
31
|
+
1. `.ai_context_engine/memory/derived_boot_summary.json`
|
|
32
|
+
2. `.ai_context_engine/memory/user_preferences.json`
|
|
33
|
+
3. `.ai_context_engine/memory/project_bootstrap.json`
|
|
34
|
+
4. smallest relevant note or structured hit
|
|
35
|
+
5. code/runtime/tests when memory is missing, stale, or insufficient
|
|
36
|
+
|
|
37
|
+
## Packet construction
|
|
38
|
+
- Use packet-building behavior for non-trivial tasks that need compact context.
|
|
39
|
+
- Packet assembly may use retrieval, task memory, failure memory, memory graph, and budget optimization.
|
|
40
|
+
- Routing/model suggestion is available when the task complexity needs it.
|
|
41
|
+
|
|
42
|
+
## Execution middleware
|
|
43
|
+
- Enter the engine for every agent execution in initialized repos, even when no skill is active.
|
|
44
|
+
- Base prehook: bootstrap + prefs + task classification + minimal retrieval + packet decision.
|
|
45
|
+
- Skill-aware enrichment is additive: when explicit skill metadata exists, preserve it and add skill context without changing the always-on entry path.
|
|
46
|
+
- Base posthook: telemetry + validated learning write-back + failure/task-memory updates when relevant.
|
|
47
|
+
- Heuristic skill detection is low-confidence fallback only; do not treat it as authoritative metadata.
|
|
48
|
+
- Repo-local adapter manifests live under `.ai_context_engine/adapters/` and are auto-installed for generic, Codex, and Claude runners.
|
|
49
|
+
|
|
50
|
+
## Communication mode
|
|
51
|
+
- `communication.layer` controls whether the caveman communication layer is active by default: `enabled` or `disabled`.
|
|
52
|
+
- `communication.mode` supports `caveman_lite`, `caveman_full`, and `caveman_ultra`.
|
|
53
|
+
- Precedence order: explicit current-user instruction > `.ai_context_engine/memory/user_preferences.json` > runtime defaults.
|
|
54
|
+
- If `communication.layer=disabled`, use normal style unless the user explicitly asks for caveman mode in the current session.
|
|
55
|
+
- Persisted changes to communication mode must come from runtime/preferences updates, not ad-hoc agent assumptions.
|
|
56
|
+
|
|
57
|
+
## Learning write-back after non-trivial tasks
|
|
58
|
+
- Persist validated learnings after non-trivial tasks.
|
|
59
|
+
- Prefer updating existing notes/rules over duplicating them.
|
|
60
|
+
- Keep generated artifacts derived; write durable knowledge to notes/preferences and regenerate when needed.
|
|
61
|
+
|
|
62
|
+
## Task memory
|
|
63
|
+
- Use task memory to reinforce reusable lessons by task type.
|
|
64
|
+
- `unknown` remains the safe fallback bucket.
|
|
65
|
+
|
|
66
|
+
## Failure memory
|
|
67
|
+
- Use failure memory for repeated breakages, regressions, and troubleshooting patterns.
|
|
68
|
+
- Reinforce occurrences instead of duplicating failure ids.
|
|
69
|
+
|
|
70
|
+
## Memory graph
|
|
71
|
+
- Use memory graph as a bounded connected-context enrichment layer.
|
|
72
|
+
- Graph failure is non-blocking; fallback to normal retrieval layers.
|
|
73
|
+
|
|
74
|
+
## Knowledge / library / mods
|
|
75
|
+
- Activate the knowledge/library workflow only when the user explicitly asks the agent to learn docs, ingest references, or build reusable knowledge.
|
|
76
|
+
- When activated, use the library/mods pipeline for local or remote knowledge ingestion and retrieval.
|
|
77
|
+
|
|
78
|
+
## Savings reports / telemetry / health
|
|
79
|
+
- Repo-local sources of truth:
|
|
80
|
+
- `.ai_context_engine/metrics/weekly_summary.json`
|
|
81
|
+
- `CONTEXT_SAVINGS.md` when present
|
|
82
|
+
- Global cross-project sources of truth:
|
|
83
|
+
- `{global_metrics_dir / 'projects_index.json'}`
|
|
84
|
+
- `{global_metrics_dir / 'telemetry_sources.json'}`
|
|
85
|
+
- `{global_metrics_dir / 'global_context_savings.json'}`
|
|
86
|
+
- `{global_metrics_dir / 'global_token_savings.json'}`
|
|
87
|
+
- `{global_metrics_dir / 'global_latency_metrics.json'}`
|
|
88
|
+
- `{global_metrics_dir / 'system_health_report.json'}`
|
|
89
|
+
- Missing telemetry must be reported as `unknown`, never as zero and never as fabricated estimates.
|
|
90
|
+
|
|
91
|
+
## Cross-project discovery
|
|
92
|
+
- Cross-project discovery must use registered workspace roots or repos.
|
|
93
|
+
- Never assume hardcoded host-specific project paths.
|
|
94
|
+
|
|
95
|
+
## Engine capabilities available after `init`
|
|
96
|
+
- boot / bootstrap
|
|
97
|
+
- query / retrieval
|
|
98
|
+
- packet construction
|
|
99
|
+
- route / model suggestion
|
|
100
|
+
- migrate / rebuild
|
|
101
|
+
- stale detection and compaction analysis
|
|
102
|
+
- task memory
|
|
103
|
+
- failure memory
|
|
104
|
+
- memory graph
|
|
105
|
+
- library / mods / retrieval
|
|
106
|
+
- global metrics and health
|
|
107
|
+
"""
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def render_repo_agents_block() -> str:
|
|
111
|
+
return f"""{AGENTS_START}
|
|
112
|
+
## AI Context Engine
|
|
113
|
+
|
|
114
|
+
This repository is initialized for `aictx`.
|
|
115
|
+
|
|
116
|
+
Agent rules:
|
|
117
|
+
- Read `.ai_context_engine/memory/derived_boot_summary.json` first when present.
|
|
118
|
+
- Use `aictx` as the first low-cost memory layer before deeper repo analysis.
|
|
119
|
+
- Apply `.ai_context_engine/memory/user_preferences.json` as defaults unless the current prompt overrides them.
|
|
120
|
+
- Enter the engine middleware for every execution in initialized repos, not only when a skill is active.
|
|
121
|
+
- Communication mode may be disabled by repo-local preferences; explicit user requests still override for the current session.
|
|
122
|
+
- Persist validated learnings after non-trivial tasks.
|
|
123
|
+
- If the user asks the agent to learn docs, reusable knowledge, or external references, activate the knowledge/library workflow.
|
|
124
|
+
- If the user asks for savings reports or health, use repo-local `.ai_context_engine/metrics/` and global engine telemetry artifacts as the source of truth.
|
|
125
|
+
- Missing telemetry must be reported as `unknown`, never as zero or as an invented estimate.
|
|
126
|
+
- Do not hand-edit generated `.ai_context_*` artifacts.
|
|
127
|
+
|
|
128
|
+
Detailed runtime instructions:
|
|
129
|
+
- `.ai_context_engine/agent_runtime.md`
|
|
130
|
+
{AGENTS_END}
|
|
131
|
+
"""
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def render_workspace_agents_block() -> str:
|
|
135
|
+
return f"""{AGENTS_START}
|
|
136
|
+
## AI Context Engine Workspace
|
|
137
|
+
|
|
138
|
+
Workspace rules:
|
|
139
|
+
- Repositories initialized with `aictx` may expose `.ai_context_*` artifacts.
|
|
140
|
+
- Prefer repo-local bootstrap first, then workspace/global discovery.
|
|
141
|
+
- Cross-project reporting must use registered workspace repos or roots, never hardcoded host paths.
|
|
142
|
+
- For savings and health, prefer generated telemetry artifacts and report missing data as `unknown`.
|
|
143
|
+
{AGENTS_END}
|
|
144
|
+
"""
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def upsert_marked_block(path: Path, block: str) -> None:
|
|
148
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
149
|
+
existing = path.read_text(encoding='utf-8') if path.exists() else ''
|
|
150
|
+
if AGENTS_START in existing and AGENTS_END in existing:
|
|
151
|
+
start = existing.index(AGENTS_START)
|
|
152
|
+
end = existing.index(AGENTS_END) + len(AGENTS_END)
|
|
153
|
+
head = existing[:start].rstrip()
|
|
154
|
+
updated = block.strip() + "\n" if not head else head + "\n\n" + block.strip() + "\n"
|
|
155
|
+
tail = existing[end:].lstrip()
|
|
156
|
+
if tail:
|
|
157
|
+
updated += "\n" + tail
|
|
158
|
+
else:
|
|
159
|
+
updated = existing.rstrip()
|
|
160
|
+
if updated:
|
|
161
|
+
updated += "\n\n"
|
|
162
|
+
updated += block.strip() + "\n"
|
|
163
|
+
path.write_text(updated, encoding='utf-8')
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def install_global_agent_runtime(write_json) -> list[Path]:
|
|
167
|
+
ENGINE_HOME.mkdir(parents=True, exist_ok=True)
|
|
168
|
+
GLOBAL_RUNTIME_PATH.write_text(render_agent_runtime(), encoding='utf-8')
|
|
169
|
+
write_json(
|
|
170
|
+
GLOBAL_RUNTIME_MANIFEST_PATH,
|
|
171
|
+
{
|
|
172
|
+
'version': 1,
|
|
173
|
+
'agent_runtime_path': str(GLOBAL_RUNTIME_PATH),
|
|
174
|
+
'global_metrics_dir': str(GLOBAL_METRICS_DIR),
|
|
175
|
+
'managed_agents_markers': [AGENTS_START, AGENTS_END],
|
|
176
|
+
'local_runtime_relative_path': str(LOCAL_RUNTIME_PATH),
|
|
177
|
+
},
|
|
178
|
+
)
|
|
179
|
+
return [GLOBAL_RUNTIME_PATH, GLOBAL_RUNTIME_MANIFEST_PATH]
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def copy_local_agent_runtime(repo: Path) -> Path:
|
|
183
|
+
source = GLOBAL_RUNTIME_PATH
|
|
184
|
+
target = repo / LOCAL_RUNTIME_PATH
|
|
185
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
186
|
+
shutil.copyfile(source, target)
|
|
187
|
+
return target
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def resolve_workspace_root(repo: Path, roots: list[str]) -> Path | None:
|
|
191
|
+
candidates: list[Path] = []
|
|
192
|
+
for root in roots:
|
|
193
|
+
root_path = Path(root).expanduser().resolve()
|
|
194
|
+
try:
|
|
195
|
+
repo.relative_to(root_path)
|
|
196
|
+
except ValueError:
|
|
197
|
+
continue
|
|
198
|
+
candidates.append(root_path)
|
|
199
|
+
if not candidates:
|
|
200
|
+
return None
|
|
201
|
+
return sorted(candidates, key=lambda p: len(p.parts), reverse=True)[0]
|