intentos 0.4.1__tar.gz → 0.4.2__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.
- {intentos-0.4.1 → intentos-0.4.2}/PKG-INFO +1 -1
- {intentos-0.4.1 → intentos-0.4.2}/cli.py +23 -1
- intentos-0.4.2/commands/doctor.py +210 -0
- intentos-0.4.2/commands/proxy.py +84 -0
- {intentos-0.4.1 → intentos-0.4.2}/core/event_store.py +121 -1
- intentos-0.4.2/core/federated.py +268 -0
- {intentos-0.4.1 → intentos-0.4.2}/core/scheduler.py +56 -8
- {intentos-0.4.1 → intentos-0.4.2}/intentos.egg-info/PKG-INFO +1 -1
- {intentos-0.4.1 → intentos-0.4.2}/intentos.egg-info/SOURCES.txt +8 -0
- {intentos-0.4.1 → intentos-0.4.2}/intentos.egg-info/top_level.txt +1 -0
- intentos-0.4.2/proxy/__init__.py +1 -0
- intentos-0.4.2/proxy/server.py +249 -0
- intentos-0.4.2/proxy/tracer.py +165 -0
- {intentos-0.4.1 → intentos-0.4.2}/pyproject.toml +2 -2
- intentos-0.4.2/tests/test_doctor.py +144 -0
- intentos-0.4.2/tests/test_proxy.py +270 -0
- {intentos-0.4.1 → intentos-0.4.2}/README.md +0 -0
- {intentos-0.4.1 → intentos-0.4.2}/adapters/__init__.py +0 -0
- {intentos-0.4.1 → intentos-0.4.2}/adapters/anthropic_adapter.py +0 -0
- {intentos-0.4.1 → intentos-0.4.2}/adapters/base.py +0 -0
- {intentos-0.4.1 → intentos-0.4.2}/adapters/github_models_adapter.py +0 -0
- {intentos-0.4.1 → intentos-0.4.2}/adapters/ollama_adapter.py +0 -0
- {intentos-0.4.1 → intentos-0.4.2}/adapters/openai_adapter.py +0 -0
- {intentos-0.4.1 → intentos-0.4.2}/adapters/openrouter_adapter.py +0 -0
- {intentos-0.4.1 → intentos-0.4.2}/commands/__init__.py +0 -0
- {intentos-0.4.1 → intentos-0.4.2}/commands/analytics.py +0 -0
- {intentos-0.4.1 → intentos-0.4.2}/commands/ask.py +0 -0
- {intentos-0.4.1 → intentos-0.4.2}/commands/compare.py +0 -0
- {intentos-0.4.1 → intentos-0.4.2}/commands/demo.py +0 -0
- {intentos-0.4.1 → intentos-0.4.2}/commands/event.py +0 -0
- {intentos-0.4.1 → intentos-0.4.2}/commands/evolution.py +0 -0
- {intentos-0.4.1 → intentos-0.4.2}/commands/export.py +0 -0
- {intentos-0.4.1 → intentos-0.4.2}/commands/helpers.py +0 -0
- {intentos-0.4.1 → intentos-0.4.2}/commands/import_cmd.py +0 -0
- {intentos-0.4.1 → intentos-0.4.2}/commands/list.py +0 -0
- {intentos-0.4.1 → intentos-0.4.2}/commands/mcp_server.py +0 -0
- {intentos-0.4.1 → intentos-0.4.2}/commands/quickstart.py +0 -0
- {intentos-0.4.1 → intentos-0.4.2}/commands/registry.py +0 -0
- {intentos-0.4.1 → intentos-0.4.2}/commands/run.py +0 -0
- {intentos-0.4.1 → intentos-0.4.2}/commands/security.py +0 -0
- {intentos-0.4.1 → intentos-0.4.2}/commands/trace.py +0 -0
- {intentos-0.4.1 → intentos-0.4.2}/commands/validate.py +0 -0
- {intentos-0.4.1 → intentos-0.4.2}/commands/workflow.py +0 -0
- {intentos-0.4.1 → intentos-0.4.2}/core/__init__.py +0 -0
- {intentos-0.4.1 → intentos-0.4.2}/core/analytics.py +0 -0
- {intentos-0.4.1 → intentos-0.4.2}/core/ask.py +0 -0
- {intentos-0.4.1 → intentos-0.4.2}/core/ask_preferences.py +0 -0
- {intentos-0.4.1 → intentos-0.4.2}/core/conditions.py +0 -0
- {intentos-0.4.1 → intentos-0.4.2}/core/cost_model.py +0 -0
- {intentos-0.4.1 → intentos-0.4.2}/core/evolution.py +0 -0
- {intentos-0.4.1 → intentos-0.4.2}/core/executor.py +0 -0
- {intentos-0.4.1 → intentos-0.4.2}/core/llm_provider.py +0 -0
- {intentos-0.4.1 → intentos-0.4.2}/core/models.py +0 -0
- {intentos-0.4.1 → intentos-0.4.2}/core/parser.py +0 -0
- {intentos-0.4.1 → intentos-0.4.2}/core/planner.py +0 -0
- {intentos-0.4.1 → intentos-0.4.2}/core/recorder.py +0 -0
- {intentos-0.4.1 → intentos-0.4.2}/core/registry.py +0 -0
- {intentos-0.4.1 → intentos-0.4.2}/core/search.py +0 -0
- {intentos-0.4.1 → intentos-0.4.2}/core/security.py +0 -0
- {intentos-0.4.1 → intentos-0.4.2}/core/workflow.py +0 -0
- {intentos-0.4.1 → intentos-0.4.2}/core/workflow_parser.py +0 -0
- {intentos-0.4.1 → intentos-0.4.2}/core/workflow_runner.py +0 -0
- {intentos-0.4.1 → intentos-0.4.2}/examples/__init__.py +0 -0
- {intentos-0.4.1 → intentos-0.4.2}/examples/code_review.yaml +0 -0
- {intentos-0.4.1 → intentos-0.4.2}/examples/data_extract.yaml +0 -0
- {intentos-0.4.1 → intentos-0.4.2}/examples/image_analyze.yaml +0 -0
- {intentos-0.4.1 → intentos-0.4.2}/examples/research_workflow.yaml +0 -0
- {intentos-0.4.1 → intentos-0.4.2}/examples/sentiment_analyze.yaml +0 -0
- {intentos-0.4.1 → intentos-0.4.2}/examples/text_summarize.yaml +0 -0
- {intentos-0.4.1 → intentos-0.4.2}/examples/translate.yaml +0 -0
- {intentos-0.4.1 → intentos-0.4.2}/intentos.egg-info/dependency_links.txt +0 -0
- {intentos-0.4.1 → intentos-0.4.2}/intentos.egg-info/entry_points.txt +0 -0
- {intentos-0.4.1 → intentos-0.4.2}/intentos.egg-info/requires.txt +0 -0
- {intentos-0.4.1 → intentos-0.4.2}/mcp_server.py +0 -0
- {intentos-0.4.1 → intentos-0.4.2}/setup.cfg +0 -0
- {intentos-0.4.1 → intentos-0.4.2}/tests/test_adapters.py +0 -0
- {intentos-0.4.1 → intentos-0.4.2}/tests/test_adaptive_workflow.py +0 -0
- {intentos-0.4.1 → intentos-0.4.2}/tests/test_ask.py +0 -0
- {intentos-0.4.1 → intentos-0.4.2}/tests/test_ask_integration.py +0 -0
- {intentos-0.4.1 → intentos-0.4.2}/tests/test_cli_commands.py +0 -0
- {intentos-0.4.1 → intentos-0.4.2}/tests/test_conditions.py +0 -0
- {intentos-0.4.1 → intentos-0.4.2}/tests/test_cost_model.py +0 -0
- {intentos-0.4.1 → intentos-0.4.2}/tests/test_cross_runtime.py +0 -0
- {intentos-0.4.1 → intentos-0.4.2}/tests/test_event_store.py +0 -0
- {intentos-0.4.1 → intentos-0.4.2}/tests/test_evolution.py +0 -0
- {intentos-0.4.1 → intentos-0.4.2}/tests/test_executor.py +0 -0
- {intentos-0.4.1 → intentos-0.4.2}/tests/test_import_export.py +0 -0
- {intentos-0.4.1 → intentos-0.4.2}/tests/test_mcp_server.py +0 -0
- {intentos-0.4.1 → intentos-0.4.2}/tests/test_registry_persistence.py +0 -0
- {intentos-0.4.1 → intentos-0.4.2}/tests/test_search.py +0 -0
- {intentos-0.4.1 → intentos-0.4.2}/tests/test_security.py +0 -0
- {intentos-0.4.1 → intentos-0.4.2}/tests/test_security_integration.py +0 -0
- {intentos-0.4.1 → intentos-0.4.2}/tests/test_workflow.py +0 -0
- {intentos-0.4.1 → intentos-0.4.2}/tests/test_workflow_adapter_integration.py +0 -0
- {intentos-0.4.1 → intentos-0.4.2}/tests/test_workflow_integration.py +0 -0
- {intentos-0.4.1 → intentos-0.4.2}/tests/test_workflow_parser.py +0 -0
- {intentos-0.4.1 → intentos-0.4.2}/tools/__init__.py +0 -0
- {intentos-0.4.1 → intentos-0.4.2}/tools/exporter.py +0 -0
- {intentos-0.4.1 → intentos-0.4.2}/tools/formats/__init__.py +0 -0
- {intentos-0.4.1 → intentos-0.4.2}/tools/formats/mcp.py +0 -0
- {intentos-0.4.1 → intentos-0.4.2}/tools/formats/openai.py +0 -0
- {intentos-0.4.1 → intentos-0.4.2}/tools/importer.py +0 -0
|
@@ -23,6 +23,8 @@ import commands.evolution
|
|
|
23
23
|
import commands.ask
|
|
24
24
|
import commands.demo
|
|
25
25
|
import commands.trace
|
|
26
|
+
import commands.proxy
|
|
27
|
+
import commands.doctor
|
|
26
28
|
|
|
27
29
|
# All cmd_* functions are imported from command modules via the registry pattern below
|
|
28
30
|
CMD_MAP = {
|
|
@@ -42,6 +44,8 @@ CMD_MAP = {
|
|
|
42
44
|
"ask": commands.ask.cmd_ask,
|
|
43
45
|
"demo": commands.demo.cmd_demo,
|
|
44
46
|
"inspect": commands.trace.cmd_inspect,
|
|
47
|
+
"proxy": commands.proxy.cmd_proxy,
|
|
48
|
+
"doctor": commands.doctor.cmd_doctor,
|
|
45
49
|
}
|
|
46
50
|
|
|
47
51
|
def build_parser() -> argparse.ArgumentParser:
|
|
@@ -51,7 +55,7 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
51
55
|
description="Intent OS Reference Runtime - Open AI Capability Interoperability",
|
|
52
56
|
epilog="Phase 0 - Prove that one Manifest can run on multiple runtimes.",
|
|
53
57
|
)
|
|
54
|
-
parser.add_argument("--version", action="version", version="intent-os 0.4.
|
|
58
|
+
parser.add_argument("--version", action="version", version="intent-os 0.4.2")
|
|
55
59
|
|
|
56
60
|
subparsers = parser.add_subparsers(dest="command", help="Available commands")
|
|
57
61
|
|
|
@@ -235,6 +239,24 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
235
239
|
help="Export trace as a standalone HTML file for sharing")
|
|
236
240
|
inspect_parser.set_defaults(func=CMD_MAP["inspect"])
|
|
237
241
|
|
|
242
|
+
# proxy
|
|
243
|
+
proxy_parser = subparsers.add_parser("proxy",
|
|
244
|
+
help="Start the Agent Hook proxy to record AI agent API calls")
|
|
245
|
+
proxy_sub = proxy_parser.add_subparsers(dest="proxy_action", help="Proxy actions")
|
|
246
|
+
ps = proxy_sub.add_parser("start", help="Start the proxy server")
|
|
247
|
+
ps.add_argument("--port", type=int, default=8377, help="Port (default: 8377)")
|
|
248
|
+
ps.add_argument("--host", default="127.0.0.1", help="Host (default: 127.0.0.1)")
|
|
249
|
+
ps.set_defaults(func=CMD_MAP["proxy"])
|
|
250
|
+
pst = proxy_sub.add_parser("status", help="Check if the proxy is running")
|
|
251
|
+
pst.add_argument("--port", type=int, default=8377, help="Port (default: 8377)")
|
|
252
|
+
pst.add_argument("--host", default="127.0.0.1", help="Host (default: 127.0.0.1)")
|
|
253
|
+
pst.set_defaults(func=CMD_MAP["proxy"])
|
|
254
|
+
|
|
255
|
+
# doctor
|
|
256
|
+
doctor_parser = subparsers.add_parser("doctor",
|
|
257
|
+
help="Check your AI agent's health — see what happened, what went wrong, and how to fix it")
|
|
258
|
+
doctor_parser.set_defaults(func=CMD_MAP["doctor"])
|
|
259
|
+
|
|
238
260
|
return parser
|
|
239
261
|
|
|
240
262
|
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
"""Intent OS — doctor command: AI Agent Health Report.
|
|
2
|
+
|
|
3
|
+
The first thing a user runs when something feels wrong.
|
|
4
|
+
No understanding of traces, events, or manifests required.
|
|
5
|
+
|
|
6
|
+
intent-os doctor
|
|
7
|
+
|
|
8
|
+
Shows whether the last agent run succeeded or failed,
|
|
9
|
+
what went wrong, and how to fix it.
|
|
10
|
+
"""
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import sys
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import Any
|
|
16
|
+
|
|
17
|
+
from commands.helpers import get_event_store, setup_executor, find_builtin_manifest
|
|
18
|
+
|
|
19
|
+
_project_root = Path(__file__).parent.parent
|
|
20
|
+
if str(_project_root) not in sys.path:
|
|
21
|
+
sys.path.insert(0, str(_project_root))
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _get_agent_status(record: dict[str, Any] | None) -> str:
|
|
25
|
+
"""Classify the agent's last run as healthy, failed, or unknown."""
|
|
26
|
+
if record is None:
|
|
27
|
+
return "unknown"
|
|
28
|
+
status = record.get("status", "")
|
|
29
|
+
if status == "success":
|
|
30
|
+
return "healthy"
|
|
31
|
+
elif status == "failure":
|
|
32
|
+
return "failed"
|
|
33
|
+
elif status == "partial":
|
|
34
|
+
return "partial"
|
|
35
|
+
return "unknown"
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _summarize_failure(events: list[dict[str, Any]], record: dict[str, Any] | None) -> dict[str, Any]:
|
|
39
|
+
"""Analyze failure: find the failed step and reason."""
|
|
40
|
+
result = {
|
|
41
|
+
"failed_events": [],
|
|
42
|
+
"error_message": None,
|
|
43
|
+
"suggestion": None,
|
|
44
|
+
"failed_at": None,
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if record and record.get("error"):
|
|
48
|
+
result["error_message"] = record["error"]
|
|
49
|
+
|
|
50
|
+
# Scan events for failures
|
|
51
|
+
for evt in events:
|
|
52
|
+
etype = evt.get("event_type", "")
|
|
53
|
+
if etype in ("TaskFailed", "WorkflowFailed"):
|
|
54
|
+
cap = evt.get("capability", "")
|
|
55
|
+
source = evt.get("source", "")
|
|
56
|
+
result["failed_events"].append(f"{cap} ({source})")
|
|
57
|
+
result["failed_at"] = cap or source or "unknown"
|
|
58
|
+
|
|
59
|
+
# Extract error from payload (stored as JSON string in Event Store)
|
|
60
|
+
raw_payload = evt.get("payload", "{}")
|
|
61
|
+
if isinstance(raw_payload, str):
|
|
62
|
+
import json
|
|
63
|
+
try:
|
|
64
|
+
payload = json.loads(raw_payload)
|
|
65
|
+
except json.JSONDecodeError:
|
|
66
|
+
payload = {}
|
|
67
|
+
else:
|
|
68
|
+
payload = raw_payload
|
|
69
|
+
if isinstance(payload, dict) and payload.get("error_message"):
|
|
70
|
+
result["error_message"] = payload["error_message"]
|
|
71
|
+
|
|
72
|
+
# Generate suggestion from error patterns
|
|
73
|
+
err = (result["error_message"] or "").lower()
|
|
74
|
+
if "timeout" in err or "timed out" in err:
|
|
75
|
+
result["suggestion"] = "Increase timeout duration or retry with a simpler request."
|
|
76
|
+
elif "rate limit" in err or "429" in err:
|
|
77
|
+
result["suggestion"] = "You're being rate-limited. Slow down requests or check your API plan."
|
|
78
|
+
elif "authentication" in err or "api key" in err or "unauthorized" in err:
|
|
79
|
+
result["suggestion"] = "Check that your API key is set and valid."
|
|
80
|
+
elif "context" in err and ("length" in err or "token" in err):
|
|
81
|
+
result["suggestion"] = "The input was too long. Try a shorter prompt or use a model with larger context."
|
|
82
|
+
elif "connect" in err or "connection" in err:
|
|
83
|
+
result["suggestion"] = "Could not reach the API. Check your network or if the service is down."
|
|
84
|
+
elif "permission" in err or "denied" in err or "forbidden" in err:
|
|
85
|
+
result["suggestion"] = "Permission denied. Check file permissions or access rights."
|
|
86
|
+
elif "not found" in err or "no such" in err:
|
|
87
|
+
result["suggestion"] = "A required file or resource was not found. Check the path exists."
|
|
88
|
+
elif "server error" in err or "500" in err or "502" in err or "503" in err:
|
|
89
|
+
result["suggestion"] = "The API server returned an error. This is usually temporary — try again."
|
|
90
|
+
else:
|
|
91
|
+
result["suggestion"] = "Check the full trace for details."
|
|
92
|
+
|
|
93
|
+
return result
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _suggest_next_step(record: dict[str, Any] | None) -> str:
|
|
97
|
+
"""Suggest what the user should do next."""
|
|
98
|
+
if record is None:
|
|
99
|
+
return "Run a capability first: intent-os run translate -p text=hello -p target_lang=zh"
|
|
100
|
+
return "View full trace: intent-os inspect latest"
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def cmd_doctor(args: Any) -> None:
|
|
104
|
+
"""Check the health of your last AI agent execution.
|
|
105
|
+
|
|
106
|
+
Analyzes the most recent execution record and tells you:
|
|
107
|
+
- Did it succeed or fail?
|
|
108
|
+
- What went wrong?
|
|
109
|
+
- How to fix it?
|
|
110
|
+
- How much it cost?
|
|
111
|
+
"""
|
|
112
|
+
store = get_event_store()
|
|
113
|
+
all_ids = store.get_all_trace_ids()
|
|
114
|
+
|
|
115
|
+
if not all_ids:
|
|
116
|
+
# No traces yet — guide the user to create one
|
|
117
|
+
print()
|
|
118
|
+
print(" ================================================")
|
|
119
|
+
print(" Intent OS Doctor - Health Check")
|
|
120
|
+
print(" ================================================")
|
|
121
|
+
print()
|
|
122
|
+
print(" No agent executions found.")
|
|
123
|
+
print()
|
|
124
|
+
print(" Run a demo to see how it works:")
|
|
125
|
+
print(" intent-os demo --auto")
|
|
126
|
+
print()
|
|
127
|
+
print(" Or run a capability:")
|
|
128
|
+
print(" intent-os run translate -p text=hello -p target_lang=zh")
|
|
129
|
+
print()
|
|
130
|
+
print(" Want to record your own AI agent?")
|
|
131
|
+
print(" intent-os proxy start")
|
|
132
|
+
print()
|
|
133
|
+
return
|
|
134
|
+
|
|
135
|
+
# Get latest trace
|
|
136
|
+
trace_id = all_ids[0]
|
|
137
|
+
record = store.get_record(trace_id)
|
|
138
|
+
events = store.get_events_by_trace(trace_id)
|
|
139
|
+
|
|
140
|
+
status = _get_agent_status(record)
|
|
141
|
+
|
|
142
|
+
print()
|
|
143
|
+
print(" ================================================")
|
|
144
|
+
print(" Intent OS Doctor - Health Check")
|
|
145
|
+
print(" ================================================")
|
|
146
|
+
print()
|
|
147
|
+
|
|
148
|
+
if status == "healthy":
|
|
149
|
+
name = record.get("manifest_name", "?") if record else "?"
|
|
150
|
+
version = record.get("manifest_version", "?") if record else "?"
|
|
151
|
+
runtime = record.get("runtime_id", "?") if record else "?"
|
|
152
|
+
adapter = record.get("adapter", "?") if record else "?"
|
|
153
|
+
latency = record.get("total_latency_ms", 0) if record else 0
|
|
154
|
+
cost = record.get("total_cost_usd", 0.0) if record else 0.0
|
|
155
|
+
tokens = record.get("total_tokens", 0) if record else 0
|
|
156
|
+
|
|
157
|
+
print(f" [OK] Last Agent Run: Healthy")
|
|
158
|
+
print()
|
|
159
|
+
print(f" Capability:")
|
|
160
|
+
print(f" {name}@{version}")
|
|
161
|
+
print()
|
|
162
|
+
print(f" Runtime: {runtime} ({adapter})")
|
|
163
|
+
print(f" Duration: {latency:.0f}ms")
|
|
164
|
+
print(f" Steps: {len(events)}")
|
|
165
|
+
print(f" Cost: ${cost:.4f}")
|
|
166
|
+
print(f" Tokens: {tokens}")
|
|
167
|
+
|
|
168
|
+
elif status == "failed":
|
|
169
|
+
name = record.get("manifest_name", "?") if record else "?"
|
|
170
|
+
analysis = _summarize_failure(events, record)
|
|
171
|
+
|
|
172
|
+
print(f" [!!] Last Agent Run: Failed")
|
|
173
|
+
print()
|
|
174
|
+
print(f" Capability: {name}")
|
|
175
|
+
|
|
176
|
+
if analysis["failed_at"]:
|
|
177
|
+
print(f" Failed at: {analysis['failed_at']}")
|
|
178
|
+
|
|
179
|
+
print()
|
|
180
|
+
print(" Problem:")
|
|
181
|
+
print(f" {analysis['error_message'] or 'Unknown error'}")
|
|
182
|
+
print()
|
|
183
|
+
|
|
184
|
+
if analysis["failed_events"]:
|
|
185
|
+
print(" Failed events:")
|
|
186
|
+
for fe in analysis["failed_events"]:
|
|
187
|
+
print(f" - {fe}")
|
|
188
|
+
print()
|
|
189
|
+
|
|
190
|
+
if analysis["suggestion"]:
|
|
191
|
+
print(" Suggestion:")
|
|
192
|
+
print(f" {analysis['suggestion']}")
|
|
193
|
+
print()
|
|
194
|
+
|
|
195
|
+
elif status == "partial":
|
|
196
|
+
print(f" [..] Last Agent Run: Partial")
|
|
197
|
+
print()
|
|
198
|
+
print(" Some steps succeeded, some failed.")
|
|
199
|
+
|
|
200
|
+
else:
|
|
201
|
+
print(f" [?] Last Agent Run: Unknown")
|
|
202
|
+
print()
|
|
203
|
+
|
|
204
|
+
# Next step suggestion
|
|
205
|
+
print()
|
|
206
|
+
next_step = _suggest_next_step(record)
|
|
207
|
+
print(f" {'=' * 48}")
|
|
208
|
+
print(f" {next_step}")
|
|
209
|
+
print(f" {'=' * 48}")
|
|
210
|
+
print()
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"""Intent OS CLI — proxy command: Agent Hook proxy server."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import sys
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def cmd_proxy(args: Any) -> None:
|
|
9
|
+
"""Start or manage the Agent Hook proxy server.
|
|
10
|
+
|
|
11
|
+
The proxy intercepts OpenAI and Anthropic API calls from AI coding
|
|
12
|
+
agents (Claude Code, Cursor, Copilot, etc.) and records every call
|
|
13
|
+
to the Intent OS Event Store for later inspection.
|
|
14
|
+
"""
|
|
15
|
+
action = args.proxy_action
|
|
16
|
+
|
|
17
|
+
if action == "start":
|
|
18
|
+
_cmd_start(args)
|
|
19
|
+
elif action == "status":
|
|
20
|
+
_cmd_status(args)
|
|
21
|
+
else:
|
|
22
|
+
print(f"Unknown proxy action: {action}", file=sys.stderr)
|
|
23
|
+
sys.exit(1)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _cmd_start(args: Any) -> None:
|
|
27
|
+
"""Start the proxy server."""
|
|
28
|
+
from proxy.server import start_proxy
|
|
29
|
+
|
|
30
|
+
port = getattr(args, "port", 8377)
|
|
31
|
+
host = getattr(args, "host", "127.0.0.1")
|
|
32
|
+
|
|
33
|
+
print()
|
|
34
|
+
print(" ================================================")
|
|
35
|
+
print(" Intent OS Agent Hook Proxy")
|
|
36
|
+
print(" ================================================")
|
|
37
|
+
print()
|
|
38
|
+
print(f" Proxy listening on http://{host}:{port}")
|
|
39
|
+
print()
|
|
40
|
+
print(" Connect your AI agent:")
|
|
41
|
+
print()
|
|
42
|
+
print(f" export OPENAI_BASE_URL=http://{host}:{port}")
|
|
43
|
+
print(f" export ANTHROPIC_BASE_URL=http://{host}:{port}")
|
|
44
|
+
print()
|
|
45
|
+
print(" Then restart your agent. All executions will be recorded.")
|
|
46
|
+
print()
|
|
47
|
+
print(" Check your agent's health with: intent-os doctor")
|
|
48
|
+
print()
|
|
49
|
+
|
|
50
|
+
try:
|
|
51
|
+
server = start_proxy(port=port, host=host)
|
|
52
|
+
server.serve_forever()
|
|
53
|
+
except PermissionError:
|
|
54
|
+
print(f" Error: Permission denied for port {port}.", file=sys.stderr)
|
|
55
|
+
print(f" Try a higher port: intent-os proxy start --port 8377", file=sys.stderr)
|
|
56
|
+
sys.exit(1)
|
|
57
|
+
except OSError as exc:
|
|
58
|
+
print(f" Error: {exc}", file=sys.stderr)
|
|
59
|
+
sys.exit(1)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _cmd_status(args: Any) -> None:
|
|
63
|
+
"""Check if the proxy server is running."""
|
|
64
|
+
import socket
|
|
65
|
+
|
|
66
|
+
port = getattr(args, "port", 8377)
|
|
67
|
+
host = getattr(args, "host", "127.0.0.1")
|
|
68
|
+
|
|
69
|
+
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
70
|
+
try:
|
|
71
|
+
sock.settimeout(2)
|
|
72
|
+
result = sock.connect_ex((host, port))
|
|
73
|
+
if result == 0:
|
|
74
|
+
print(f" Agent Hook Proxy is RUNNING on http://{host}:{port}")
|
|
75
|
+
print()
|
|
76
|
+
print(" To view captured traces:")
|
|
77
|
+
print(" intent-os inspect latest")
|
|
78
|
+
else:
|
|
79
|
+
print(f" Agent Hook Proxy is NOT running on http://{host}:{port}")
|
|
80
|
+
print()
|
|
81
|
+
print(" To start:")
|
|
82
|
+
print(f" intent-os proxy start --port {port}")
|
|
83
|
+
finally:
|
|
84
|
+
sock.close()
|
|
@@ -34,7 +34,7 @@ from core.recorder import ExecutionRecorder
|
|
|
34
34
|
|
|
35
35
|
|
|
36
36
|
# Schema version to support future migrations
|
|
37
|
-
SCHEMA_VERSION =
|
|
37
|
+
SCHEMA_VERSION = 2
|
|
38
38
|
|
|
39
39
|
# SQL statements for table creation
|
|
40
40
|
CREATE_EVENTS_TABLE = """
|
|
@@ -97,6 +97,18 @@ CREATE TABLE IF NOT EXISTS meta (
|
|
|
97
97
|
);
|
|
98
98
|
"""
|
|
99
99
|
|
|
100
|
+
CREATE_TASK_STATE_TABLE = """
|
|
101
|
+
CREATE TABLE IF NOT EXISTS task_state (
|
|
102
|
+
trace_id TEXT NOT NULL,
|
|
103
|
+
task_id TEXT NOT NULL,
|
|
104
|
+
workflow_id TEXT NOT NULL DEFAULT '',
|
|
105
|
+
field_name TEXT NOT NULL,
|
|
106
|
+
field_value TEXT NOT NULL DEFAULT '{}',
|
|
107
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
108
|
+
PRIMARY KEY (trace_id, task_id, field_name)
|
|
109
|
+
);
|
|
110
|
+
"""
|
|
111
|
+
|
|
100
112
|
|
|
101
113
|
class EventStoreError(Exception):
|
|
102
114
|
"""Raised when Event Store operations fail."""
|
|
@@ -154,6 +166,7 @@ class EventStore:
|
|
|
154
166
|
conn = self._get_conn()
|
|
155
167
|
conn.execute(CREATE_EVENTS_TABLE)
|
|
156
168
|
conn.execute(CREATE_EXECUTION_RECORDS_TABLE)
|
|
169
|
+
conn.execute(CREATE_TASK_STATE_TABLE)
|
|
157
170
|
conn.execute(CREATE_META_TABLE)
|
|
158
171
|
for idx in CREATE_INDEXES:
|
|
159
172
|
try:
|
|
@@ -517,6 +530,113 @@ class EventStore:
|
|
|
517
530
|
cursor = conn.execute(sql, params)
|
|
518
531
|
return [dict(row) for row in cursor.fetchall()]
|
|
519
532
|
|
|
533
|
+
# ── Task State (Data Plane storage for Scheduler) ──
|
|
534
|
+
|
|
535
|
+
def save_task_state(
|
|
536
|
+
self,
|
|
537
|
+
trace_id: str,
|
|
538
|
+
task_id: str,
|
|
539
|
+
field_name: str,
|
|
540
|
+
field_value: str,
|
|
541
|
+
workflow_id: str = "",
|
|
542
|
+
) -> None:
|
|
543
|
+
"""Persist a single task state field to the Data Plane.
|
|
544
|
+
|
|
545
|
+
This is how the Scheduler satisfies R1 (Control Plane owns no
|
|
546
|
+
state) — instead of holding ``_task_outputs`` in memory, the
|
|
547
|
+
Scheduler writes each field as a row in the ``task_state`` table.
|
|
548
|
+
"""
|
|
549
|
+
conn = self._get_conn()
|
|
550
|
+
conn.execute(
|
|
551
|
+
"""INSERT OR REPLACE INTO task_state
|
|
552
|
+
(trace_id, task_id, workflow_id, field_name, field_value, updated_at)
|
|
553
|
+
VALUES (?, ?, ?, ?, ?, datetime('now'))""",
|
|
554
|
+
(trace_id, task_id, workflow_id, field_name, json.dumps(field_value)),
|
|
555
|
+
)
|
|
556
|
+
conn.commit()
|
|
557
|
+
|
|
558
|
+
def save_task_output(
|
|
559
|
+
self,
|
|
560
|
+
trace_id: str,
|
|
561
|
+
task_id: str,
|
|
562
|
+
output: dict[str, Any],
|
|
563
|
+
workflow_id: str = "",
|
|
564
|
+
) -> None:
|
|
565
|
+
"""Convenience: save a task's output dict as the ``output`` field."""
|
|
566
|
+
self.save_task_state(trace_id, task_id, "output", output, workflow_id)
|
|
567
|
+
|
|
568
|
+
def save_task_error(
|
|
569
|
+
self,
|
|
570
|
+
trace_id: str,
|
|
571
|
+
task_id: str,
|
|
572
|
+
error: str,
|
|
573
|
+
workflow_id: str = "",
|
|
574
|
+
) -> None:
|
|
575
|
+
"""Convenience: save a task's error as the ``error`` field."""
|
|
576
|
+
self.save_task_state(trace_id, task_id, "error", error, workflow_id)
|
|
577
|
+
|
|
578
|
+
def get_task_state(
|
|
579
|
+
self,
|
|
580
|
+
trace_id: str,
|
|
581
|
+
task_id: str,
|
|
582
|
+
field_name: str,
|
|
583
|
+
) -> Any:
|
|
584
|
+
"""Read a single task state field from the Data Plane."""
|
|
585
|
+
conn = self._get_conn()
|
|
586
|
+
cursor = conn.execute(
|
|
587
|
+
"""SELECT field_value FROM task_state
|
|
588
|
+
WHERE trace_id = ? AND task_id = ? AND field_name = ?""",
|
|
589
|
+
(trace_id, task_id, field_name),
|
|
590
|
+
)
|
|
591
|
+
row = cursor.fetchone()
|
|
592
|
+
if row is None:
|
|
593
|
+
return None
|
|
594
|
+
try:
|
|
595
|
+
return json.loads(row["field_value"])
|
|
596
|
+
except (json.JSONDecodeError, TypeError):
|
|
597
|
+
return row["field_value"]
|
|
598
|
+
|
|
599
|
+
def get_all_task_outputs(self, trace_id: str) -> dict[str, Any]:
|
|
600
|
+
"""Read all task outputs for a trace (used by Scheduler for condition evaluation)."""
|
|
601
|
+
conn = self._get_conn()
|
|
602
|
+
cursor = conn.execute(
|
|
603
|
+
"""SELECT task_id, field_value FROM task_state
|
|
604
|
+
WHERE trace_id = ? AND field_name = 'output'""",
|
|
605
|
+
(trace_id,),
|
|
606
|
+
)
|
|
607
|
+
result: dict[str, Any] = {}
|
|
608
|
+
for row in cursor.fetchall():
|
|
609
|
+
try:
|
|
610
|
+
result[row["task_id"]] = json.loads(row["field_value"])
|
|
611
|
+
except (json.JSONDecodeError, TypeError):
|
|
612
|
+
result[row["task_id"]] = row["field_value"]
|
|
613
|
+
return result
|
|
614
|
+
|
|
615
|
+
def get_all_task_errors(self, trace_id: str) -> dict[str, str]:
|
|
616
|
+
"""Read all task errors for a trace."""
|
|
617
|
+
conn = self._get_conn()
|
|
618
|
+
cursor = conn.execute(
|
|
619
|
+
"""SELECT task_id, field_value FROM task_state
|
|
620
|
+
WHERE trace_id = ? AND field_name = 'error'""",
|
|
621
|
+
(trace_id,),
|
|
622
|
+
)
|
|
623
|
+
result: dict[str, str] = {}
|
|
624
|
+
for row in cursor.fetchall():
|
|
625
|
+
try:
|
|
626
|
+
val = json.loads(row["field_value"])
|
|
627
|
+
result[row["task_id"]] = str(val)
|
|
628
|
+
except (json.JSONDecodeError, TypeError):
|
|
629
|
+
result[row["task_id"]] = str(row["field_value"])
|
|
630
|
+
return result
|
|
631
|
+
|
|
632
|
+
def delete_task_state(self, trace_id: str) -> None:
|
|
633
|
+
"""Clean up task state for a trace when it's no longer needed."""
|
|
634
|
+
conn = self._get_conn()
|
|
635
|
+
conn.execute("DELETE FROM task_state WHERE trace_id = ?", (trace_id,))
|
|
636
|
+
conn.commit()
|
|
637
|
+
|
|
638
|
+
# ── Legacy query interface ──
|
|
639
|
+
|
|
520
640
|
def get_all_trace_ids(self) -> list[str]:
|
|
521
641
|
"""List all trace IDs in the store."""
|
|
522
642
|
conn = self._get_conn()
|