ark-agent-runtime 0.1.0a1__py3-none-win_amd64.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.
@@ -0,0 +1,43 @@
1
+ """ARK Runtime SDK — a thin Python client over the Go ARK runtime and its canonical
2
+ telemetry contract (RunResult / DecisionRecord). Execution, decision traces, cost
3
+ attribution, routing info, tool activity, verification (where available), audit/
4
+ intervention records, and (experimental, opt-in) constrained supervision.
5
+
6
+ This SDK does not reimplement ARK and makes no unproven claims (e.g. automatic cost
7
+ savings): it exposes the factual telemetry the Go runtime already produces.
8
+ """
9
+ from typing import Optional
10
+
11
+ from .client import ARK
12
+ from .errors import ArkError, ArkBridgeError, ArkSupervisionDisabled
13
+ from .session import RunSession, Verdict
14
+ from .models import (
15
+ RunResult, DecisionRecord, DecisionCost, SupervisionRecord, SupervisionResult,
16
+ Verification, RoutingDecision, ToolDecision, SupervisionSummary, RoutingSummary, ToolSummary,
17
+ )
18
+
19
+
20
+ def trace(task: str, *, supervision: str = "off", task_type: Optional[str] = None,
21
+ provider: str = "openai", budget: int = 4) -> RunSession:
22
+ """Open an external-agent trace with a default client. Keep your own agent/model/tools
23
+ and attach ARK around your runtime:
24
+
25
+ import ark
26
+ with ark.trace("find the top Python web frameworks") as run:
27
+ ... # your loop reports decisions
28
+ result = run.result # canonical RunResult
29
+
30
+ Pass supervision="experimental" to enable run.check(...) gating.
31
+ """
32
+ return ARK(supervision=supervision).trace(task, task_type=task_type,
33
+ provider=provider, budget=budget)
34
+
35
+
36
+ __all__ = [
37
+ "ARK", "trace", "RunSession", "Verdict",
38
+ "ArkError", "ArkBridgeError", "ArkSupervisionDisabled",
39
+ "RunResult", "DecisionRecord", "DecisionCost", "SupervisionRecord", "SupervisionResult",
40
+ "Verification", "RoutingDecision", "ToolDecision",
41
+ "SupervisionSummary", "RoutingSummary", "ToolSummary",
42
+ ]
43
+ __version__ = "0.1.0a1"
@@ -0,0 +1,8 @@
1
+ """Home of the bundled ARK bridge binary.
2
+
3
+ At build time the platform-native `ark-bridge` (or `ark-bridge.exe`) executable is compiled
4
+ from the Go source (cmd/ark-bridge) and placed in this directory, then shipped as package
5
+ data inside the wheel. `ark.bridge._find_binary` locates it here, so an installed package
6
+ needs no Go toolchain and no ARK_BRIDGE_BIN. The binary itself is a build artifact and is not
7
+ committed to source control.
8
+ """
@@ -0,0 +1,96 @@
1
+ """Go<->Python transport. Default: invoke the local ark-bridge binary as a subprocess.
2
+
3
+ The transport is an IMPLEMENTATION DETAIL: the public ARK API does not depend on this
4
+ being a subprocess, so it can later be replaced (e.g. a local service) without changing
5
+ the Python API. Any object with a ``call(request: dict) -> dict`` method works.
6
+ """
7
+ from __future__ import annotations
8
+
9
+ import json
10
+ import os
11
+ import shutil
12
+ import subprocess
13
+
14
+ from .errors import ArkBridgeError
15
+
16
+
17
+ def _bundled_binary() -> "str | None":
18
+ """The bridge shipped inside the installed wheel (ark/_bridge/ark-bridge[.exe])."""
19
+ name = "ark-bridge.exe" if os.name == "nt" else "ark-bridge"
20
+ p = os.path.join(os.path.dirname(os.path.abspath(__file__)), "_bridge", name)
21
+ return p if os.path.isfile(p) else None
22
+
23
+
24
+ def _ensure_executable(path: str) -> None:
25
+ """Wheels don't always preserve the +x bit for package data; restore it if missing."""
26
+ if os.name == "nt":
27
+ return
28
+ try:
29
+ mode = os.stat(path).st_mode
30
+ if not (mode & 0o111):
31
+ os.chmod(path, mode | 0o111)
32
+ except OSError:
33
+ pass
34
+
35
+
36
+ def _find_binary() -> str:
37
+ # 1. explicit override — for developers/debugging (not needed by an installed package)
38
+ b = os.environ.get("ARK_BRIDGE_BIN")
39
+ if b and os.path.exists(b):
40
+ return b
41
+ # 2. the bundled bridge inside the installed wheel — the normal path, zero setup
42
+ bundled = _bundled_binary()
43
+ if bundled:
44
+ _ensure_executable(bundled)
45
+ return bundled
46
+ # 3. a bridge on PATH, then a source-checkout build (developer convenience)
47
+ p = shutil.which("ark-bridge")
48
+ if p:
49
+ return p
50
+ for guess in (os.path.expanduser("~/ark/ark-bridge-bin"),
51
+ os.path.expanduser("~/ark/ark-bridge-test-bin")):
52
+ if os.path.exists(guess):
53
+ return guess
54
+ raise ArkBridgeError(
55
+ "ark-bridge binary not found. An installed `ark-agent-runtime` wheel bundles it "
56
+ "automatically; if you are running from a source checkout, build it with "
57
+ "`go build -o ark-bridge-bin ./cmd/ark-bridge` and set ARK_BRIDGE_BIN."
58
+ )
59
+
60
+
61
+ class SubprocessBridge:
62
+ def __init__(self, binary: str | None = None, timeout: int = 120):
63
+ self._bin = binary or _find_binary()
64
+ self._timeout = timeout
65
+
66
+ def call(self, request: dict) -> dict:
67
+ try:
68
+ p = subprocess.run([self._bin], input=json.dumps(request).encode(),
69
+ capture_output=True, timeout=self._timeout)
70
+ except (OSError, subprocess.TimeoutExpired) as e:
71
+ raise ArkBridgeError(f"bridge invocation failed: {e}") from e
72
+ out = (p.stdout or b"").decode().strip()
73
+ if not out:
74
+ err = (p.stderr or b"").decode()[:300]
75
+ raise ArkBridgeError(f"bridge produced no output (exit {p.returncode}): {err}")
76
+ data = _parse_json(out)
77
+ if data is None:
78
+ raise ArkBridgeError(f"bridge returned non-JSON: {out[:300]}")
79
+ if isinstance(data, dict) and data.get("error"):
80
+ raise ArkBridgeError(data["error"])
81
+ return data
82
+
83
+
84
+ def _parse_json(out: str):
85
+ """The bridge emits a single JSON object (possibly multi-line/indented). Parse the
86
+ whole payload; tolerate any surrounding text by falling back to the outermost braces."""
87
+ try:
88
+ return json.loads(out)
89
+ except json.JSONDecodeError:
90
+ i, j = out.find("{"), out.rfind("}")
91
+ if 0 <= i < j:
92
+ try:
93
+ return json.loads(out[i:j + 1])
94
+ except json.JSONDecodeError:
95
+ return None
96
+ return None
@@ -0,0 +1,82 @@
1
+ """The ARK client — the front door to the ARK runtime.
2
+
3
+ from ark import ARK
4
+ ark = ARK()
5
+ result = ark.run(task="find the top Python web frameworks on GitHub")
6
+ print(result.success, result.total_cost, result.decisions)
7
+
8
+ Experimental constrained supervision is explicit and off by default:
9
+
10
+ ark = ARK(supervision="experimental")
11
+
12
+ The Go runtime is the source of truth; this client only submits requests and parses the
13
+ canonical RunResult. It reimplements no routing/supervision/cost/verification/retry logic.
14
+ """
15
+ from __future__ import annotations
16
+
17
+ from typing import Any, Optional
18
+
19
+ from .bridge import SubprocessBridge
20
+ from .errors import ArkSupervisionDisabled
21
+ from .models import RunResult, SupervisionRecord, SupervisionResult
22
+ from .session import RunSession
23
+
24
+
25
+ class ARK:
26
+ def __init__(self, supervision: str = "off", bridge=None):
27
+ if supervision not in ("off", "experimental"):
28
+ raise ValueError("supervision must be 'off' or 'experimental'")
29
+ self.supervision = supervision
30
+ self._bridge = bridge or SubprocessBridge()
31
+
32
+ def run(self, task: Optional[str] = None, agent: Any = None, *,
33
+ mode: str = "mock", config: Optional[str] = None) -> RunResult:
34
+ """Run a task through the Go ARK runtime and return the canonical RunResult.
35
+
36
+ mode="mock" (default) runs the real runtime deterministically with no API cost;
37
+ mode="live" uses the provider configured in agent.yaml (needs a real key).
38
+ """
39
+ if not task and agent is None:
40
+ raise ValueError("run() requires a task")
41
+ req: dict = {"kind": "run", "task": task or "", "mode": mode, "supervision": self.supervision}
42
+ if isinstance(agent, str): # agent given as a config path
43
+ req["config"] = agent
44
+ if config:
45
+ req["config"] = config
46
+ return RunResult.from_dict(self._bridge.call(req))
47
+
48
+ def trace(self, task: str, *, task_type: Optional[str] = None,
49
+ provider: str = "openai", budget: int = 4) -> RunSession:
50
+ """Open an external-agent trace: keep your own agent/model/tools and attach ARK
51
+ around your runtime. Use as a context manager:
52
+
53
+ with ark.trace("book the 2nd-cheapest flight") as run:
54
+ ... # your loop
55
+ run.record(action="tool_call", model="gpt-4o", input_tokens=..., ...)
56
+ result = run.result # canonical RunResult
57
+
58
+ Supervision follows this client's setting: ARK(supervision="experimental").trace(...)
59
+ enables run.check(...). ARK never executes your agent — you do.
60
+ """
61
+ return RunSession(task, task_type=task_type, supervision=self.supervision,
62
+ provider=provider, budget=budget)
63
+
64
+ def supervise(self, constraint: str, proposed: dict, evidence: dict,
65
+ retry_count: int = 0, budget: int = 4) -> SupervisionResult:
66
+ """Evaluate one agent-authored action against a runtime-derived constraint.
67
+
68
+ Experimental: requires ARK(supervision="experimental"). Returns a verdict
69
+ (ALLOW/REJECT/REQUIRE_EVIDENCE/RECOVERY_EXHAUSTED); ARK never authors the action.
70
+ """
71
+ if self.supervision != "experimental":
72
+ raise ArkSupervisionDisabled(
73
+ "constrained supervision is experimental and off by default; "
74
+ "create ARK(supervision='experimental')")
75
+ data = self._bridge.call({
76
+ "kind": "supervise", "supervision": "experimental", "constraint": constraint,
77
+ "proposed": proposed, "evidence": evidence, "retry_count": retry_count, "budget": budget,
78
+ })
79
+ return SupervisionResult(
80
+ verdict=data.get("verdict"), reason=data.get("reason"),
81
+ record=SupervisionRecord.from_dict(data.get("supervision")),
82
+ )
@@ -0,0 +1,14 @@
1
+ """SDK error types. Runtime/bridge failures map to ArkBridgeError; using experimental
2
+ supervision without opting in raises ArkSupervisionDisabled."""
3
+
4
+
5
+ class ArkError(Exception):
6
+ """Base class for all ARK SDK errors."""
7
+
8
+
9
+ class ArkBridgeError(ArkError):
10
+ """The Go bridge/runtime failed, was unreachable, or returned an error."""
11
+
12
+
13
+ class ArkSupervisionDisabled(ArkError):
14
+ """Constrained supervision was used without opting in (it is experimental/off by default)."""
@@ -0,0 +1,7 @@
1
+ """Framework integrations for ARK.
2
+
3
+ Each integration is a THIN adapter that maps a framework's native lifecycle onto the generic
4
+ `ark.trace()` session primitives (`run.record(...)`, `run.check(...)`). Adapters never
5
+ reimplement routing, supervision, retries, pricing, or telemetry — those live in Go behind
6
+ the session. Integrations import their framework lazily, so `import ark` never requires it.
7
+ """
@@ -0,0 +1,243 @@
1
+ """LangGraph / LangChain integration for ARK — a thin adapter over `ark.trace()`.
2
+
3
+ LangGraph lifecycle/events -> ARK generic trace/check/record -> Go ARK telemetry + supervision
4
+
5
+ It maps LangChain's callback events onto the generic session and (optionally) gates proposed
6
+ tool calls. It reimplements NOTHING — no routing, supervision, retries, pricing, or telemetry;
7
+ every field is either reported straight from a LangChain event or derived by Go ARK. What the
8
+ adapter can observe automatically vs. what the developer must supply:
9
+
10
+ observed automatically by the adapter (from LangChain callbacks)
11
+ - model calls (start/end), model name, input/output tokens -> ARK derives cost
12
+ - tool calls: name, args, outcome/error, latency
13
+ the developer must still supply (ARK cannot infer it from the graph)
14
+ - which tool to supervise, the applicable constraint, and the trusted runtime evidence
15
+ (e.g. the retrieved, priced options) passed to `ark_supervise_tool(...)`
16
+
17
+ Interception audit (LangGraph 1.x / langchain-core 1.x): LangChain callback handlers are
18
+ OBSERVATIONAL — `on_tool_start` cannot cleanly veto an execution. The clean, framework-native
19
+ pre-execution gate is the tool itself: a tool is a plain callable, so wrapping it lets ARK
20
+ check the proposed action BEFORE the real logic runs and, on a non-ALLOW verdict, return the
21
+ runtime-derived suggestion as the tool result — which LangGraph's own agent loop feeds back to
22
+ the model to REPLAN. `ark_supervise_tool` uses exactly that; it fakes no interception.
23
+ (`create_react_agent`'s `post_model_hook` / `interrupt_before=["tools"]` are alternative
24
+ native gates; the tool wrapper is the thinnest and works with any graph.)
25
+
26
+ Sync graphs only (the common `.invoke(...)` path); async/parallel tool fan-out is out of scope.
27
+ """
28
+ from __future__ import annotations
29
+
30
+ import time
31
+ from typing import Any, Callable, Dict, List, Optional, Tuple
32
+
33
+ try:
34
+ from langchain_core.callbacks import BaseCallbackHandler
35
+ from langchain_core.tools import BaseTool, StructuredTool
36
+ except ModuleNotFoundError as e: # optional dependency — give a clear, actionable message
37
+ raise ImportError(
38
+ "ARK's LangGraph integration requires the optional 'langgraph' extra. "
39
+ "Install it with: pip install 'ark-agent-runtime[langgraph]' "
40
+ "(or: pip install langgraph langchain-core ). "
41
+ "The core ARK SDK (import ark) does not need it."
42
+ ) from e
43
+
44
+ from ..session import RunSession, Verdict
45
+
46
+
47
+ class ArkCallbackHandler(BaseCallbackHandler):
48
+ """A LangChain callback handler that reports every model/tool event to an ARK trace.
49
+
50
+ with ark.trace("...") as run:
51
+ agent.invoke(inputs, config={"callbacks": [ArkCallbackHandler(run)]})
52
+ result = run.result
53
+
54
+ Pass ``record_tools=False`` when tools are gated with :func:`ark_supervise_tool` (the
55
+ wrapper records those tool decisions itself, so the handler should record only the model
56
+ turns to avoid double-counting).
57
+ """
58
+
59
+ # this handler only reports; it never blocks or mutates the run.
60
+ raise_error = False
61
+
62
+ def __init__(self, run: RunSession, *, record_tools: bool = True):
63
+ self._run = run
64
+ self._record_tools = record_tools
65
+ self._llm_start: Dict[Any, float] = {}
66
+ self._llm_model: Dict[Any, Optional[str]] = {}
67
+ self._tool_start: Dict[Any, float] = {}
68
+ self._tool_meta: Dict[Any, Tuple[str, Optional[dict]]] = {}
69
+
70
+ # ---- model calls -> cost-bearing decisions ----
71
+ def on_chat_model_start(self, serialized, messages, *, run_id, **kwargs):
72
+ self._begin_llm(run_id, serialized, kwargs)
73
+
74
+ def on_llm_start(self, serialized, prompts, *, run_id, **kwargs):
75
+ self._begin_llm(run_id, serialized, kwargs)
76
+
77
+ def _begin_llm(self, run_id, serialized, kwargs):
78
+ self._llm_start[run_id] = time.monotonic()
79
+ ip = kwargs.get("invocation_params") or {}
80
+ name = ip.get("model") or ip.get("model_name")
81
+ if not name and serialized:
82
+ name = (serialized.get("kwargs") or {}).get("model_name") or serialized.get("name")
83
+ self._llm_model[run_id] = name
84
+
85
+ def on_llm_end(self, response, *, run_id, **kwargs):
86
+ model, in_tok, out_tok, tool_names = _llm_end_fields(response)
87
+ if not model:
88
+ model = self._llm_model.get(run_id)
89
+ latency_ms = _elapsed_ms(self._llm_start.pop(run_id, None))
90
+ self._llm_model.pop(run_id, None)
91
+ # a model turn that proposed tool(s) is a "model_call"; a final turn is "complete".
92
+ # cost is DERIVED by ARK from the reported tokens+model — the adapter computes none.
93
+ self._run.record(
94
+ action="model_call" if tool_names else "complete",
95
+ model=model, input_tokens=in_tok, output_tokens=out_tok,
96
+ latency_ms=latency_ms, outcome=("proposed_tools" if tool_names else "stop"),
97
+ )
98
+
99
+ def on_llm_error(self, error, *, run_id, **kwargs):
100
+ latency_ms = _elapsed_ms(self._llm_start.pop(run_id, None))
101
+ self._run.record(action="model_call", model=self._llm_model.pop(run_id, None),
102
+ latency_ms=latency_ms, outcome=f"error: {type(error).__name__}",
103
+ error=_err(error), executed=False)
104
+
105
+ # ---- tool executions -> tool-activity decisions (no token cost) ----
106
+ def on_tool_start(self, serialized, input_str, *, run_id, inputs=None, **kwargs):
107
+ if not self._record_tools:
108
+ return
109
+ self._tool_start[run_id] = time.monotonic()
110
+ name = (serialized or {}).get("name") or "tool"
111
+ self._tool_meta[run_id] = (name, inputs if isinstance(inputs, dict) else None)
112
+
113
+ def on_tool_end(self, output, *, run_id, **kwargs):
114
+ if not self._record_tools:
115
+ return
116
+ name, args = self._tool_meta.pop(run_id, ("tool", None))
117
+ latency_ms = _elapsed_ms(self._tool_start.pop(run_id, None))
118
+ self._run.record(action="tool_call", tool=name, tool_args=args,
119
+ latency_ms=latency_ms, outcome="success")
120
+
121
+ def on_tool_error(self, error, *, run_id, **kwargs):
122
+ if not self._record_tools:
123
+ return
124
+ name, args = self._tool_meta.pop(run_id, ("tool", None))
125
+ latency_ms = _elapsed_ms(self._tool_start.pop(run_id, None))
126
+ self._run.record(action="tool_call", tool=name, tool_args=args, latency_ms=latency_ms,
127
+ outcome=f"error: {type(error).__name__}", error=_err(error))
128
+
129
+
130
+ def build_agent(model: Any, tools: Any, **kwargs) -> Any:
131
+ """Construct a LangGraph ReAct-style agent using the SUPPORTED constructor for the
132
+ installed version — ``langchain.agents.create_agent`` (LangGraph/LangChain 1.x) when
133
+ available, else ``langgraph.prebuilt.create_react_agent`` (older, deprecation silenced).
134
+
135
+ This is only a convenience so callers avoid the deprecation churn; ARK's observation and
136
+ supervision hooks work identically with either constructor (verified). It builds nothing
137
+ ARK-specific — the returned agent is a plain LangGraph graph you invoke yourself.
138
+ """
139
+ try:
140
+ from langchain.agents import create_agent # LangChain 1.x, the supported path
141
+ return create_agent(model, tools, **kwargs)
142
+ except ModuleNotFoundError:
143
+ import warnings
144
+ from langgraph.prebuilt import create_react_agent
145
+ with warnings.catch_warnings():
146
+ warnings.simplefilter("ignore") # its own move-to-langchain deprecation notice
147
+ return create_react_agent(model, tools, **kwargs)
148
+
149
+
150
+ def ark_supervise_tool(run: RunSession, tool: Any, *, constraint: str,
151
+ evidence: "dict | Callable[[dict], dict]",
152
+ proposed: "Optional[Callable[[dict], dict]]" = None) -> StructuredTool:
153
+ """Wrap a LangGraph/LangChain tool so ARK gates each proposed call before it executes.
154
+
155
+ ``constraint`` names the runtime constraint (e.g. "rank"); ``evidence`` is the trusted
156
+ runtime evidence — a static dict or a ``fn(tool_args) -> dict`` computed from the call.
157
+ ``proposed`` maps the tool args to the ProposedAction dict (default: ``{"option": <first
158
+ arg>}``). On ALLOW the real tool runs and the executed telemetry is recorded on the SAME
159
+ decision as the verdict (``of=verdict``); on any non-ALLOW verdict the tool does NOT run
160
+ and ARK's suggestion is returned as the tool result, so LangGraph's loop replans.
161
+
162
+ The wrapper calls only ``run.check`` / ``run.record`` — the verdict, retry budget, and
163
+ recovery all come from Go ARK. Requires the trace to be opened with supervision enabled.
164
+ """
165
+ base = tool if isinstance(tool, BaseTool) else StructuredTool.from_function(tool)
166
+ raw: Callable = base.func if getattr(base, "func", None) is not None else base.invoke
167
+ name, description, args_schema = base.name, base.description, base.args_schema
168
+
169
+ def supervised(**kwargs):
170
+ ev = evidence(kwargs) if callable(evidence) else evidence
171
+ prop = proposed(kwargs) if callable(proposed) else _default_proposed(kwargs)
172
+ verdict: Verdict = run.check(proposed_action=prop, constraint=constraint,
173
+ evidence=ev or {}, action="tool_call", tool=name)
174
+ if verdict.allowed:
175
+ try:
176
+ result = raw(**kwargs)
177
+ except Exception as exc: # a supervised action that was allowed but then failed
178
+ run.record(action="tool_call", tool=name, tool_args=kwargs,
179
+ outcome=f"error: {type(exc).__name__}", error=_err(exc),
180
+ executed=True, of=verdict)
181
+ raise # let LangGraph handle the tool error natively
182
+ run.record(action="tool_call", tool=name, tool_args=kwargs,
183
+ outcome="success", of=verdict)
184
+ return result
185
+ run.record(action="tool_call", tool=name, tool_args=kwargs,
186
+ outcome=f"rejected:{verdict.verdict}", executed=False, of=verdict)
187
+ # returned to LangGraph as the ToolMessage -> the model re-proposes (replan).
188
+ hint = f" Suggested: {verdict.suggested}." if verdict.suggested else ""
189
+ return f"ARK blocked this action ({verdict.verdict}): {verdict.reason or 'constraint not satisfied'}.{hint}"
190
+
191
+ return StructuredTool.from_function(func=supervised, name=name,
192
+ description=description, args_schema=args_schema)
193
+
194
+
195
+ # ---- helpers (pure mapping over LangChain event shapes; no ARK logic) ----
196
+
197
+ import re as _re
198
+
199
+ # scrub obvious secret shapes from an error string so a provider error can never leak a key.
200
+ _SECRET_RE = _re.compile(
201
+ r"(sk-[A-Za-z0-9_\-]{6,}|ghp_[A-Za-z0-9]{6,}|Bearer\s+[A-Za-z0-9._\-]{6,}|"
202
+ r"api[_-]?key\s*[=:]\s*\S+)", _re.IGNORECASE)
203
+
204
+
205
+ def _err(exc: BaseException, limit: int = 300) -> str:
206
+ """A compact, secret-scrubbed error string for the canonical DecisionRecord.error field."""
207
+ msg = _SECRET_RE.sub("[redacted]", str(exc))[:limit]
208
+ return f"{type(exc).__name__}: {msg}" if msg else type(exc).__name__
209
+
210
+
211
+ def _default_proposed(args: dict) -> dict:
212
+ for v in args.values():
213
+ return {"option": v}
214
+ return {"fields": args}
215
+
216
+
217
+ def _elapsed_ms(start: Optional[float]) -> int:
218
+ return int((time.monotonic() - start) * 1000) if start else 0
219
+
220
+
221
+ def _llm_end_fields(response) -> Tuple[Optional[str], int, int, List[str]]:
222
+ """Extract (model, input_tokens, output_tokens, proposed_tool_names) from an LLMResult,
223
+ tolerating both the modern usage_metadata path and the legacy llm_output path."""
224
+ model: Optional[str] = None
225
+ in_tok = out_tok = 0
226
+ tool_names: List[str] = []
227
+ gens = getattr(response, "generations", None) or []
228
+ msg = getattr(gens[0][0], "message", None) if gens and gens[0] else None
229
+ if msg is not None:
230
+ um = getattr(msg, "usage_metadata", None) or {}
231
+ in_tok = int(um.get("input_tokens") or 0)
232
+ out_tok = int(um.get("output_tokens") or 0)
233
+ rm = getattr(msg, "response_metadata", None) or {}
234
+ model = rm.get("model_name") or rm.get("model")
235
+ tool_names = [tc.get("name") for tc in (getattr(msg, "tool_calls", None) or []) if tc.get("name")]
236
+ lo = getattr(response, "llm_output", None) or {}
237
+ if not model:
238
+ model = lo.get("model_name") or lo.get("model")
239
+ if in_tok == 0 and out_tok == 0:
240
+ tu = lo.get("token_usage") or lo.get("usage") or {}
241
+ in_tok = int(tu.get("prompt_tokens") or tu.get("input_tokens") or 0)
242
+ out_tok = int(tu.get("completion_tokens") or tu.get("output_tokens") or 0)
243
+ return model, in_tok, out_tok, tool_names
@@ -0,0 +1,229 @@
1
+ """Typed Python mirror of the canonical ARK telemetry contract (pkg/telemetry).
2
+
3
+ These objects mirror the Go `RunResult` / `DecisionRecord` JSON exactly — they do not
4
+ invent a new schema, and they never fabricate a value the runtime reported as absent
5
+ (missing fields stay ``None``). No routing/supervision/cost/verification logic lives here;
6
+ this is a passive view over what the Go runtime produced.
7
+ """
8
+ from __future__ import annotations
9
+
10
+ from dataclasses import dataclass, field
11
+ from typing import Any, Optional
12
+
13
+
14
+ def _f(d: dict, k: str, default=None):
15
+ return d.get(k, default)
16
+
17
+
18
+ @dataclass(frozen=True)
19
+ class DecisionCost:
20
+ input_tokens: int = 0
21
+ output_tokens: int = 0
22
+ input_cost: float = 0.0
23
+ output_cost: float = 0.0
24
+ model_cost: float = 0.0
25
+ tool_cost: float = 0.0
26
+ total_cost: float = 0.0
27
+
28
+ @classmethod
29
+ def from_dict(cls, d: Optional[dict]) -> "DecisionCost":
30
+ d = d or {}
31
+ return cls(
32
+ input_tokens=_f(d, "input_tokens", 0),
33
+ output_tokens=_f(d, "output_tokens", 0),
34
+ input_cost=_f(d, "input_cost", 0.0),
35
+ output_cost=_f(d, "output_cost", 0.0),
36
+ model_cost=_f(d, "model_cost", 0.0),
37
+ tool_cost=_f(d, "tool_cost", 0.0),
38
+ total_cost=_f(d, "total_cost", 0.0),
39
+ )
40
+
41
+
42
+ @dataclass(frozen=True)
43
+ class Verification:
44
+ method: Optional[str] = None
45
+ passed: Optional[bool] = None
46
+ score: Optional[float] = None
47
+ confidence: Optional[float] = None
48
+ issues: Optional[list] = None
49
+
50
+ @classmethod
51
+ def from_dict(cls, d: Optional[dict]) -> Optional["Verification"]:
52
+ if not d:
53
+ return None
54
+ return cls(_f(d, "method"), _f(d, "passed"), _f(d, "score"), _f(d, "confidence"), _f(d, "issues"))
55
+
56
+
57
+ @dataclass(frozen=True)
58
+ class SupervisionRecord:
59
+ applicable_constraint: Optional[str] = None
60
+ verdict: Optional[str] = None # ALLOW / REJECT / REQUIRE_EVIDENCE / RECOVERY_EXHAUSTED
61
+ trusted_evidence_ref: Optional[str] = None
62
+ rejection_reason: Optional[str] = None
63
+ retry_number: Optional[int] = None
64
+ suggested_from_evidence: Optional[str] = None
65
+ executed: Optional[bool] = None
66
+
67
+ @classmethod
68
+ def from_dict(cls, d: Optional[dict]) -> Optional["SupervisionRecord"]:
69
+ if not d:
70
+ return None
71
+ return cls(
72
+ _f(d, "applicable_constraint"), _f(d, "verdict"), _f(d, "trusted_evidence_ref"),
73
+ _f(d, "rejection_reason"), _f(d, "retry_number"), _f(d, "suggested_from_evidence"),
74
+ _f(d, "executed"),
75
+ )
76
+
77
+
78
+ @dataclass(frozen=True)
79
+ class RoutingDecision:
80
+ """A view of a decision's routing: which model, and why."""
81
+ model: Optional[str] = None
82
+ routing_reason: Optional[str] = None
83
+
84
+
85
+ @dataclass(frozen=True)
86
+ class ToolDecision:
87
+ """A view of a decision's tool activity (args are a redacted reference, never secrets)."""
88
+ tool: Optional[str] = None
89
+ tool_args_ref: Optional[str] = None
90
+
91
+
92
+ @dataclass(frozen=True)
93
+ class DecisionRecord:
94
+ id: str
95
+ sequence: int
96
+ decision_type: str
97
+ timestamp: Optional[str] = None
98
+ action: Optional[str] = None
99
+ model: Optional[str] = None
100
+ routing_reason: Optional[str] = None
101
+ tool: Optional[str] = None
102
+ tool_args_ref: Optional[str] = None
103
+ cost: DecisionCost = field(default_factory=DecisionCost)
104
+ latency_ms: int = 0
105
+ verification: Optional[Verification] = None
106
+ supervision: Optional[SupervisionRecord] = None
107
+ outcome: Optional[str] = None
108
+ error: Optional[str] = None
109
+ executed: bool = False
110
+
111
+ @property
112
+ def latency(self) -> int:
113
+ return self.latency_ms
114
+
115
+ @property
116
+ def routing(self) -> RoutingDecision:
117
+ return RoutingDecision(self.model, self.routing_reason)
118
+
119
+ @property
120
+ def tool_decision(self) -> ToolDecision:
121
+ return ToolDecision(self.tool, self.tool_args_ref)
122
+
123
+ @classmethod
124
+ def from_dict(cls, d: dict) -> "DecisionRecord":
125
+ return cls(
126
+ id=d["id"], sequence=d["sequence"], decision_type=d["decision_type"],
127
+ timestamp=_f(d, "timestamp"), action=_f(d, "action"), model=_f(d, "model"),
128
+ routing_reason=_f(d, "routing_reason"), tool=_f(d, "tool"), tool_args_ref=_f(d, "tool_args_ref"),
129
+ cost=DecisionCost.from_dict(_f(d, "cost")), latency_ms=_f(d, "latency_ms", 0),
130
+ verification=Verification.from_dict(_f(d, "verification")),
131
+ supervision=SupervisionRecord.from_dict(_f(d, "supervision")),
132
+ outcome=_f(d, "outcome"), error=_f(d, "error"), executed=_f(d, "executed", False),
133
+ )
134
+
135
+
136
+ @dataclass(frozen=True)
137
+ class SupervisionSummary:
138
+ enabled: bool = False
139
+ interventions: int = 0
140
+ by_verdict: dict = field(default_factory=dict)
141
+
142
+ @classmethod
143
+ def from_dict(cls, d: Optional[dict]) -> "SupervisionSummary":
144
+ d = d or {}
145
+ return cls(_f(d, "enabled", False), _f(d, "interventions", 0), _f(d, "by_verdict") or {})
146
+
147
+
148
+ @dataclass(frozen=True)
149
+ class RoutingSummary:
150
+ by_model: dict = field(default_factory=dict)
151
+
152
+ @classmethod
153
+ def from_dict(cls, d: Optional[dict]) -> "RoutingSummary":
154
+ return cls((d or {}).get("by_model") or {})
155
+
156
+
157
+ @dataclass(frozen=True)
158
+ class ToolSummary:
159
+ calls: int = 0
160
+ by_tool: dict = field(default_factory=dict)
161
+
162
+ @classmethod
163
+ def from_dict(cls, d: Optional[dict]) -> "ToolSummary":
164
+ d = d or {}
165
+ return cls(_f(d, "calls", 0), _f(d, "by_tool") or {})
166
+
167
+
168
+ @dataclass(frozen=True)
169
+ class RunResult:
170
+ run_id: str
171
+ task: str
172
+ success: bool
173
+ decisions: list # list[DecisionRecord]
174
+ task_type: Optional[str] = None
175
+ started_at: Optional[str] = None
176
+ completed_at: Optional[str] = None
177
+ termination_reason: Optional[str] = None
178
+ output: Optional[str] = None
179
+ total_tokens: int = 0
180
+ total_latency_ms: int = 0
181
+ total_cost: float = 0.0
182
+ cost_by_model: dict = field(default_factory=dict)
183
+ cost_by_tool: dict = field(default_factory=dict)
184
+ cost_by_action: dict = field(default_factory=dict)
185
+ cost_by_supervision: Optional[dict] = None
186
+ supervision: SupervisionSummary = field(default_factory=SupervisionSummary)
187
+ routing: RoutingSummary = field(default_factory=RoutingSummary)
188
+ tools: ToolSummary = field(default_factory=ToolSummary)
189
+ providers: dict = field(default_factory=dict)
190
+ errors: Optional[list] = None
191
+ raw: Optional[dict] = None # the exact JSON the runtime returned
192
+
193
+ @property
194
+ def total_latency(self) -> int:
195
+ return self.total_latency_ms
196
+
197
+ @classmethod
198
+ def from_dict(cls, d: dict) -> "RunResult":
199
+ return cls(
200
+ run_id=d["run_id"], task=_f(d, "task", ""), success=_f(d, "success", False),
201
+ decisions=[DecisionRecord.from_dict(x) for x in (_f(d, "decisions") or [])],
202
+ task_type=_f(d, "task_type"), started_at=_f(d, "started_at"), completed_at=_f(d, "completed_at"),
203
+ termination_reason=_f(d, "termination_reason"), output=_f(d, "output"),
204
+ total_tokens=_f(d, "total_tokens", 0), total_latency_ms=_f(d, "total_latency_ms", 0),
205
+ total_cost=_f(d, "total_cost", 0.0),
206
+ cost_by_model=_f(d, "cost_by_model") or {}, cost_by_tool=_f(d, "cost_by_tool") or {},
207
+ cost_by_action=_f(d, "cost_by_action") or {}, cost_by_supervision=_f(d, "cost_by_supervision"),
208
+ supervision=SupervisionSummary.from_dict(_f(d, "supervision")),
209
+ routing=RoutingSummary.from_dict(_f(d, "routing")),
210
+ tools=ToolSummary.from_dict(_f(d, "tools")),
211
+ providers=_f(d, "providers") or {}, errors=_f(d, "errors"), raw=d,
212
+ )
213
+
214
+
215
+ @dataclass(frozen=True)
216
+ class SupervisionResult:
217
+ """Result of one constrained-supervision evaluation. ARK validates/gates; it never
218
+ authors the replacement action — the agent re-proposes."""
219
+ verdict: str # ALLOW / REJECT / REQUIRE_EVIDENCE / RECOVERY_EXHAUSTED
220
+ reason: Optional[str] = None
221
+ record: Optional[SupervisionRecord] = None
222
+
223
+ @property
224
+ def allowed(self) -> bool:
225
+ return self.verdict == "ALLOW"
226
+
227
+ @property
228
+ def suggested_from_evidence(self) -> Optional[str]:
229
+ return self.record.suggested_from_evidence if self.record else None
@@ -0,0 +1,235 @@
1
+ """External-agent integration: ``with ark.trace(...) as run:``.
2
+
3
+ The developer keeps their own agent/model/tools and drives their own loop. Inside the
4
+ trace they REPORT what happened (``run.record(...)``) and, optionally, ask ARK to gate a
5
+ proposed action before executing it (``run.check(...)``). On exit ARK returns the canonical
6
+ ``RunResult`` — the same contract ``ARK.run`` produces.
7
+
8
+ This module is a THIN line-protocol client over a persistent ``ark-bridge --session``
9
+ process. All session state that ARK owns — retry counters, verdict semantics, recovery
10
+ logic, stable decision IDs, cost pricing, aggregate derivation — lives in Go, so Python
11
+ never reimplements or drifts from it. Fields ARK cannot observe (model, tokens, tool,
12
+ latency, verification, routing) are reported by the developer and are marked as such in
13
+ ``run.provenance``; they are never presented as if ARK observed them directly.
14
+ """
15
+ from __future__ import annotations
16
+
17
+ import json
18
+ import subprocess
19
+ from typing import Any, Optional
20
+
21
+ from .bridge import _find_binary
22
+ from .errors import ArkError, ArkBridgeError, ArkSupervisionDisabled
23
+ from .models import RunResult
24
+
25
+
26
+ class Verdict:
27
+ """One supervision verdict, and the stable handle for the decision it created.
28
+
29
+ Pass it back as ``run.record(..., of=verdict)`` so the executed telemetry lands on the
30
+ SAME DecisionRecord as the verdict — an unambiguous proposal -> execution audit chain.
31
+ ``suggested`` is runtime-derived EVIDENCE (e.g. the rank-2 option id); ARK never authors
32
+ the replacement action — your agent re-proposes.
33
+ """
34
+
35
+ def __init__(self, d: dict):
36
+ self.decision_id: Optional[str] = d.get("decision_id")
37
+ self.verdict: Optional[str] = d.get("verdict")
38
+ self.reason: Optional[str] = d.get("reason")
39
+ self.retry_number: Optional[int] = d.get("retry_number")
40
+ self.suggested: Optional[str] = d.get("suggested") or None
41
+ self.allowed: bool = bool(d.get("allowed"))
42
+
43
+ def __repr__(self) -> str:
44
+ return (f"Verdict(verdict={self.verdict!r}, allowed={self.allowed}, "
45
+ f"suggested={self.suggested!r}, id={self.decision_id!r})")
46
+
47
+
48
+ class _SessionProc:
49
+ """A persistent ``ark-bridge --session`` subprocess speaking one JSON object per line.
50
+
51
+ The transport is an implementation detail behind ``RunSession`` — it can be swapped for a
52
+ local service later without changing the public API. It carries NO session logic: it just
53
+ forwards a command dict and returns the parsed response dict.
54
+ """
55
+
56
+ def __init__(self, binary: Optional[str] = None, timeout: int = 120):
57
+ self._bin = binary or _find_binary()
58
+ self._timeout = timeout
59
+ self._p: Optional[subprocess.Popen] = None
60
+
61
+ def start(self) -> None:
62
+ self._p = subprocess.Popen(
63
+ [self._bin, "--session"],
64
+ stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, bufsize=0,
65
+ )
66
+
67
+ def send(self, cmd: dict) -> dict:
68
+ if self._p is None or self._p.poll() is not None:
69
+ raise ArkBridgeError("session process is not running")
70
+ payload = (json.dumps(cmd) + "\n").encode()
71
+ try:
72
+ self._p.stdin.write(payload)
73
+ self._p.stdin.flush()
74
+ out = self._p.stdout.readline()
75
+ except (OSError, BrokenPipeError) as e:
76
+ raise ArkBridgeError(f"session transport failed: {e}") from e
77
+ if not out:
78
+ err = (self._p.stderr.read() or b"")[:300].decode(errors="replace")
79
+ raise ArkBridgeError(f"session ended without a response: {err}")
80
+ try:
81
+ data = json.loads(out.decode())
82
+ except json.JSONDecodeError as e:
83
+ raise ArkBridgeError(f"session returned non-JSON: {out[:200]!r}") from e
84
+ if isinstance(data, dict) and data.get("error"):
85
+ raise ArkBridgeError(data["error"])
86
+ return data
87
+
88
+ def close(self) -> None:
89
+ if self._p is None:
90
+ return
91
+ try:
92
+ if self._p.stdin and not self._p.stdin.closed:
93
+ self._p.stdin.close()
94
+ self._p.wait(timeout=5)
95
+ except Exception:
96
+ try:
97
+ self._p.kill()
98
+ except Exception:
99
+ pass
100
+ finally:
101
+ self._p = None
102
+
103
+
104
+ class RunSession:
105
+ """A single ARK trace around an external agent's run. Use as a context manager.
106
+
107
+ ``supervision="experimental"`` is required to call ``check``; observe-only traces omit it.
108
+ """
109
+
110
+ def __init__(self, task: str, *, task_type: Optional[str] = None, supervision: str = "off",
111
+ provider: str = "openai", budget: int = 4, run_id: Optional[str] = None,
112
+ transport: Optional[_SessionProc] = None):
113
+ if supervision not in ("off", "experimental"):
114
+ raise ValueError("supervision must be 'off' or 'experimental'")
115
+ self.task = task
116
+ self.run_id: Optional[str] = run_id
117
+ self._task_type = task_type
118
+ self._supervision = supervision
119
+ self._provider = provider
120
+ self._budget = budget
121
+ self._proc = transport or _SessionProc()
122
+ self._result: Optional[RunResult] = None
123
+ self._provenance: Optional[dict] = None
124
+ self._finished = False
125
+ self._started = False
126
+
127
+ # -- lifecycle -----------------------------------------------------------------
128
+ def __enter__(self) -> "RunSession":
129
+ self._proc.start()
130
+ r = self._proc.send({
131
+ "cmd": "start", "task": self.task, "task_type": self._task_type or "",
132
+ "supervision": self._supervision, "provider": self._provider,
133
+ "budget": self._budget, "run_id": self.run_id or "",
134
+ })
135
+ self.run_id = r.get("run_id")
136
+ self._started = True
137
+ return self
138
+
139
+ def __exit__(self, exc_type, exc, tb) -> bool:
140
+ try:
141
+ if self._started and not self._finished:
142
+ self.finish(
143
+ success=exc_type is None,
144
+ termination_reason=None if exc_type is None else f"exception: {exc_type.__name__}",
145
+ )
146
+ finally:
147
+ self._proc.close()
148
+ return False # never suppress the developer's exception
149
+
150
+ # -- supervision gate ----------------------------------------------------------
151
+ def check(self, proposed_action: Any, constraint: str, evidence: dict, *,
152
+ action: str = "tool_call", tool: Optional[str] = None) -> Verdict:
153
+ """Gate a proposed action BEFORE executing it. Synchronous, non-authoring.
154
+
155
+ Returns a :class:`Verdict`. ARK owns the retry budget and verdict semantics; on a
156
+ non-ALLOW verdict your agent re-authors the next action and calls ``check`` again.
157
+ """
158
+ if not self._started:
159
+ raise ArkError("check() outside an open trace")
160
+ if self._supervision != "experimental":
161
+ raise ArkSupervisionDisabled(
162
+ "constrained supervision is experimental and off by default; "
163
+ "open the trace with supervision='experimental'")
164
+ r = self._proc.send({
165
+ "cmd": "check", "action": action, "tool": tool or "",
166
+ "constraint": constraint, "proposed": _as_proposed(proposed_action),
167
+ "evidence": evidence or {},
168
+ })
169
+ return Verdict(r)
170
+
171
+ # -- telemetry report ----------------------------------------------------------
172
+ def record(self, *, action: Optional[str] = None, model: Optional[str] = None,
173
+ tool: Optional[str] = None, tool_args: Optional[dict] = None,
174
+ input_tokens: Optional[int] = None, output_tokens: Optional[int] = None,
175
+ cost: Optional[float] = None, latency_ms: Optional[int] = None,
176
+ routing_reason: Optional[str] = None, verification: Optional[dict] = None,
177
+ outcome: Optional[str] = None, error: Optional[str] = None,
178
+ executed: bool = True, of: "Verdict | str | None" = None) -> Optional[str]:
179
+ """Report one decision's telemetry. With ``of=<verdict>`` it completes the decision
180
+ that ``check`` created; otherwise it records a fresh (unsupervised) decision.
181
+
182
+ Everything here is REPORTED by your runtime — ARK derives only ids/ordering and, when
183
+ you give tokens+model but no ``cost``, the cost (via ARK's pricing tables). Pass
184
+ ``error`` for a real failure: it populates the canonical DecisionRecord.error field
185
+ (aggregated into RunResult.errors); ``outcome`` is kept separately for compatibility.
186
+ """
187
+ if not self._started:
188
+ raise ArkError("record() outside an open trace")
189
+ of_id = of.decision_id if isinstance(of, Verdict) else of
190
+ r = self._proc.send({
191
+ "cmd": "record", "action": action or "", "model": model or "",
192
+ "tool": tool or "", "tool_args": tool_args,
193
+ "input_tokens": input_tokens or 0, "output_tokens": output_tokens or 0,
194
+ "cost": cost, "latency_ms": latency_ms or 0,
195
+ "routing_reason": routing_reason or "", "verification": verification,
196
+ "outcome": outcome or "", "error": error or "",
197
+ "executed": executed, "of": of_id or "",
198
+ })
199
+ return r.get("decision_id")
200
+
201
+ def finish(self, *, success: bool = True, termination_reason: Optional[str] = None,
202
+ output: Optional[str] = None) -> RunResult:
203
+ """End the trace and return the canonical RunResult. Called automatically on exit."""
204
+ if self._finished:
205
+ return self._result # type: ignore[return-value]
206
+ r = self._proc.send({
207
+ "cmd": "finish", "success": success,
208
+ "termination_reason": termination_reason or "", "output": output or "",
209
+ })
210
+ self._result = RunResult.from_dict(r["run_result"])
211
+ self._provenance = r.get("provenance")
212
+ self._finished = True
213
+ return self._result
214
+
215
+ # -- results -------------------------------------------------------------------
216
+ @property
217
+ def result(self) -> RunResult:
218
+ if self._result is None:
219
+ raise ArkError("run result is not available until the trace finishes")
220
+ return self._result
221
+
222
+ @property
223
+ def provenance(self) -> Optional[dict]:
224
+ """Per-decision {reported:[...], derived:[...]} split — which facts came from your
225
+ runtime vs which ARK generated. Available after the trace finishes."""
226
+ return self._provenance
227
+
228
+
229
+ def _as_proposed(p: Any) -> dict:
230
+ """Accept a bare option id, ``{"option": ...}``, or a full ProposedAction-shaped dict."""
231
+ if isinstance(p, dict):
232
+ if "option" in p or "kind" in p or "fields" in p:
233
+ return p
234
+ return {"fields": p}
235
+ return {"option": str(p)}
@@ -0,0 +1,82 @@
1
+ Metadata-Version: 2.4
2
+ Name: ark-agent-runtime
3
+ Version: 0.1.0a1
4
+ Summary: ARK Runtime SDK — attach ARK decision telemetry, cost attribution, and (experimental) constrained supervision around your existing agent.
5
+ Author: Abhishek Tripathi
6
+ License-Expression: Apache-2.0
7
+ Project-URL: Homepage, https://github.com/atripati/ark
8
+ Project-URL: Repository, https://github.com/atripati/ark
9
+ Keywords: ark,agent,llm,telemetry,cost,supervision,observability,langgraph
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Topic :: Software Development :: Libraries
14
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
15
+ Requires-Python: >=3.9
16
+ Description-Content-Type: text/markdown
17
+ License-File: LICENSE
18
+ Provides-Extra: langgraph
19
+ Requires-Dist: langgraph>=0.2; extra == "langgraph"
20
+ Requires-Dist: langchain-core>=0.3; extra == "langgraph"
21
+ Dynamic: license-file
22
+
23
+ # ARK Runtime SDK (`ark-agent-runtime`)
24
+
25
+ Attach ARK's decision telemetry, cost attribution, and (experimental) constrained supervision
26
+ around your existing agent — you keep your model, your tools, and your runtime.
27
+
28
+ ```bash
29
+ pip install ark-agent-runtime
30
+ ```
31
+
32
+ ```python
33
+ from ark import ARK
34
+
35
+ # run a task through the ARK runtime and get the canonical RunResult
36
+ result = ARK().run(task="find the top Python web frameworks on GitHub")
37
+ print(result.success, result.total_cost, [d.model for d in result.decisions])
38
+ ```
39
+
40
+ The wheel bundles the ARK runtime bridge for your platform — **no Go toolchain, no build step,
41
+ no `ARK_BRIDGE_BIN`.** `import ark` needs no other dependencies.
42
+
43
+ ## Attach ARK around your own agent
44
+
45
+ ```python
46
+ import ark
47
+
48
+ with ark.trace("book the 2nd-cheapest flight") as run:
49
+ # ... your agent loop; you own the model and tools ...
50
+ run.record(action="tool_call", model="gpt-4o", tool="search",
51
+ input_tokens=449, output_tokens=16, outcome="success")
52
+ result = run.result # canonical RunResult (same schema as ARK.run)
53
+ ```
54
+
55
+ Experimental constrained supervision is **off by default** and opt-in:
56
+
57
+ ```python
58
+ ark.ARK(supervision="experimental")
59
+ ```
60
+
61
+ ## LangGraph integration (optional)
62
+
63
+ ```bash
64
+ pip install "ark-agent-runtime[langgraph]"
65
+ ```
66
+
67
+ ```python
68
+ from ark.integrations.langgraph import ArkCallbackHandler, ark_supervise_tool, build_agent
69
+ ```
70
+
71
+ Without the extra, `import ark` still works; importing the integration prints a clear install
72
+ message.
73
+
74
+ ## What you get
75
+
76
+ - Canonical `RunResult` / `DecisionRecord` telemetry (decision-level).
77
+ - Decision-level cost attribution (routing-aware, deterministic model pricing).
78
+ - Routing, tool activity, verification (where available), and errors.
79
+ - Experimental constrained supervision (ALLOW / REJECT / REQUIRE_EVIDENCE / RECOVERY_EXHAUSTED).
80
+
81
+ The SDK does not reimplement ARK; it is a thin client over the bundled ARK runtime and makes
82
+ no unproven claims.
@@ -0,0 +1,15 @@
1
+ ark_agent_runtime-0.1.0a1.data/purelib/ark/__init__.py,sha256=og_TiEyZnBbVCrRjDjdaNGGvJkgqDFHEaEZb8BW4p60,2005
2
+ ark_agent_runtime-0.1.0a1.data/purelib/ark/bridge.py,sha256=PSzz5NaB7KAnZIYaonpKiIzQq1xslbbFRguRj-XUAho,3727
3
+ ark_agent_runtime-0.1.0a1.data/purelib/ark/client.py,sha256=-0MitCWe_VgeUfoG87n7Y5tlurwuVyXAQ18O4jQBXmM,3936
4
+ ark_agent_runtime-0.1.0a1.data/purelib/ark/errors.py,sha256=RcQ-9JE1kMVV5zKAJyyDGqkFx6CeRSl_4tb0qKuMJT0,491
5
+ ark_agent_runtime-0.1.0a1.data/purelib/ark/models.py,sha256=rVcx6f2xuckgOQTAPRQ2g1nXYVclzhlDvs4UnGveuTY,8419
6
+ ark_agent_runtime-0.1.0a1.data/purelib/ark/session.py,sha256=p4py6VlZJO0JIXpfNkdq1WuzLMoPHJzqY_WntT9d10w,11008
7
+ ark_agent_runtime-0.1.0a1.data/purelib/ark/_bridge/__init__.py,sha256=y0wdDUrW7lZ-CKpLS7e0ZdIZuT2UbQShenbiTACv2iU,450
8
+ ark_agent_runtime-0.1.0a1.data/purelib/ark/_bridge/ark-bridge.exe,sha256=HCV2uL1yAFygam_QibuC0Ve3SoZUguTaxaLfH2fGrVs,7158784
9
+ ark_agent_runtime-0.1.0a1.data/purelib/ark/integrations/__init__.py,sha256=sMb48TLyjsHyk-GeYGp3yYlwUxXDJE_khxJ3ei44wDA,412
10
+ ark_agent_runtime-0.1.0a1.data/purelib/ark/integrations/langgraph.py,sha256=1wWH1rbRD2toRZON_HsdrJXgrSZviXxZUhGChMnEvm4,12870
11
+ ark_agent_runtime-0.1.0a1.dist-info/licenses/LICENSE,sha256=Pd-b5cKP4n2tFDpdx27qJSIq0d1ok0oEcGTlbtL6QMU,11560
12
+ ark_agent_runtime-0.1.0a1.dist-info/METADATA,sha256=NS-_p-vJA0CTn_m0CAwF2UeCSIEQvHe4572DB0F8k14,2987
13
+ ark_agent_runtime-0.1.0a1.dist-info/WHEEL,sha256=Zx98gwb_dQKckJK3HEOVKnxxD52EfU_PXDG_usMJ2ng,98
14
+ ark_agent_runtime-0.1.0a1.dist-info/top_level.txt,sha256=GvM87AHKwkkvIN4c8QP2WILF2jp67ns1g_pv5owIQXQ,4
15
+ ark_agent_runtime-0.1.0a1.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.0.0)
3
+ Root-Is-Purelib: false
4
+ Tag: py3-none-win_amd64
5
+
@@ -0,0 +1,202 @@
1
+
2
+ Apache License
3
+ Version 2.0, January 2004
4
+ http://www.apache.org/licenses/
5
+
6
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
7
+
8
+ 1. Definitions.
9
+
10
+ "License" shall mean the terms and conditions for use, reproduction,
11
+ and distribution as defined by Sections 1 through 9 of this document.
12
+
13
+ "Licensor" shall mean the copyright owner or entity authorized by
14
+ the copyright owner that is granting the License.
15
+
16
+ "Legal Entity" shall mean the union of the acting entity and all
17
+ other entities that control, are controlled by, or are under common
18
+ control with that entity. For the purposes of this definition,
19
+ "control" means (i) the power, direct or indirect, to cause the
20
+ direction or management of such entity, whether by contract or
21
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
22
+ outstanding shares, or (iii) beneficial ownership of such entity.
23
+
24
+ "You" (or "Your") shall mean an individual or Legal Entity
25
+ exercising permissions granted by this License.
26
+
27
+ "Source" form shall mean the preferred form for making modifications,
28
+ including but not limited to software source code, documentation
29
+ source, and configuration files.
30
+
31
+ "Object" form shall mean any form resulting from mechanical
32
+ transformation or translation of a Source form, including but
33
+ not limited to compiled object code, generated documentation,
34
+ and conversions to other media types.
35
+
36
+ "Work" shall mean the work of authorship, whether in Source or
37
+ Object form, made available under the License, as indicated by a
38
+ copyright notice that is included in or attached to the work
39
+ (an example is provided in the Appendix below).
40
+
41
+ "Derivative Works" shall mean any work, whether in Source or Object
42
+ form, that is based on (or derived from) the Work and for which the
43
+ editorial revisions, annotations, elaborations, or other modifications
44
+ represent, as a whole, an original work of authorship. For the purposes
45
+ of this License, Derivative Works shall not include works that remain
46
+ separable from, or merely link (or bind by name) to the interfaces of,
47
+ the Work and Derivative Works thereof.
48
+
49
+ "Contribution" shall mean any work of authorship, including
50
+ the original version of the Work and any modifications or additions
51
+ to that Work or Derivative Works thereof, that is intentionally
52
+ submitted to Licensor for inclusion in the Work by the copyright owner
53
+ or by an individual or Legal Entity authorized to submit on behalf of
54
+ the copyright owner. For the purposes of this definition, "submitted"
55
+ means any form of electronic, verbal, or written communication sent
56
+ to the Licensor or its representatives, including but not limited to
57
+ communication on electronic mailing lists, source code control systems,
58
+ and issue tracking systems that are managed by, or on behalf of, the
59
+ Licensor for the purpose of discussing and improving the Work, but
60
+ excluding communication that is conspicuously marked or otherwise
61
+ designated in writing by the copyright owner as "Not a Contribution."
62
+
63
+ "Contributor" shall mean Licensor and any individual or Legal Entity
64
+ on behalf of whom a Contribution has been received by Licensor and
65
+ subsequently incorporated within the Work.
66
+
67
+ 2. Grant of Copyright License. Subject to the terms and conditions of
68
+ this License, each Contributor hereby grants to You a perpetual,
69
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
70
+ copyright license to reproduce, prepare Derivative Works of,
71
+ publicly display, publicly perform, sublicense, and distribute the
72
+ Work and such Derivative Works in Source or Object form.
73
+
74
+ 3. Grant of Patent License. Subject to the terms and conditions of
75
+ this License, each Contributor hereby grants to You a perpetual,
76
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
77
+ (except as stated in this section) patent license to make, have made,
78
+ use, offer to sell, sell, import, and otherwise transfer the Work,
79
+ where such license applies only to those patent claims licensable
80
+ by such Contributor that are necessarily infringed by their
81
+ Contribution(s) alone or by combination of their Contribution(s)
82
+ with the Work to which such Contribution(s) was submitted. If You
83
+ institute patent litigation against any entity (including a
84
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
85
+ or a Contribution incorporated within the Work constitutes direct
86
+ or contributory patent infringement, then any patent licenses
87
+ granted to You under this License for that Work shall terminate
88
+ as of the date such litigation is filed.
89
+
90
+ 4. Redistribution. You may reproduce and distribute copies of the
91
+ Work or Derivative Works thereof in any medium, with or without
92
+ modifications, and in Source or Object form, provided that You
93
+ meet the following conditions:
94
+
95
+ (a) You must give any other recipients of the Work or
96
+ Derivative Works a copy of this License; and
97
+
98
+ (b) You must cause any modified files to carry prominent notices
99
+ stating that You changed the files; and
100
+
101
+ (c) You must retain, in the Source form of any Derivative Works
102
+ that You distribute, all copyright, patent, trademark, and
103
+ attribution notices from the Source form of the Work,
104
+ excluding those notices that do not pertain to any part of
105
+ the Derivative Works; and
106
+
107
+ (d) If the Work includes a "NOTICE" text file as part of its
108
+ distribution, then any Derivative Works that You distribute must
109
+ include a readable copy of the attribution notices contained
110
+ within such NOTICE file, excluding those notices that do not
111
+ pertain to any part of the Derivative Works, in at least one
112
+ of the following places: within a NOTICE text file distributed
113
+ as part of the Derivative Works; within the Source form or
114
+ documentation, if provided along with the Derivative Works; or,
115
+ within a display generated by the Derivative Works, if and
116
+ wherever such third-party notices normally appear. The contents
117
+ of the NOTICE file are for informational purposes only and
118
+ do not modify the License. You may add Your own attribution
119
+ notices within Derivative Works that You distribute, alongside
120
+ or as an addendum to the NOTICE text from the Work, provided
121
+ that such additional attribution notices cannot be construed
122
+ as modifying the License.
123
+
124
+ You may add Your own copyright statement to Your modifications and
125
+ may provide additional or different license terms and conditions
126
+ for use, reproduction, or distribution of Your modifications, or
127
+ for any such Derivative Works as a whole, provided Your use,
128
+ reproduction, and distribution of the Work otherwise complies with
129
+ the conditions stated in this License.
130
+
131
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
132
+ any Contribution intentionally submitted for inclusion in the Work
133
+ by You to the Licensor shall be under the terms and conditions of
134
+ this License, without any additional terms or conditions.
135
+ Notwithstanding the above, nothing herein shall supersede or modify
136
+ the terms of any separate license agreement you may have executed
137
+ with Licensor regarding such Contributions.
138
+
139
+ 6. Trademarks. This License does not grant permission to use the trade
140
+ names, trademarks, service marks, or product names of the Licensor,
141
+ except as required for reasonable and customary use in describing the
142
+ origin of the Work and reproducing the content of the NOTICE file.
143
+
144
+ 7. Disclaimer of Warranty. Unless required by applicable law or
145
+ agreed to in writing, Licensor provides the Work (and each
146
+ Contributor provides its Contributions) on an "AS IS" BASIS,
147
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
148
+ implied, including, without limitation, any warranties or conditions
149
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
150
+ PARTICULAR PURPOSE. You are solely responsible for determining the
151
+ appropriateness of using or redistributing the Work and assume any
152
+ risks associated with Your exercise of permissions under this License.
153
+
154
+ 8. Limitation of Liability. In no event and under no legal theory,
155
+ whether in tort (including negligence), contract, or otherwise,
156
+ unless required by applicable law (such as deliberate and grossly
157
+ negligent acts) or agreed to in writing, shall any Contributor be
158
+ liable to You for damages, including any direct, indirect, special,
159
+ incidental, or consequential damages of any character arising as a
160
+ result of this License or out of the use or inability to use the
161
+ Work (including but not limited to damages for loss of goodwill,
162
+ work stoppage, computer failure or malfunction, or any and all
163
+ other commercial damages or losses), even if such Contributor
164
+ has been advised of the possibility of such damages.
165
+
166
+ 9. Accepting Warranty or Additional Liability. While redistributing
167
+ the Work or Derivative Works thereof, You may choose to offer,
168
+ and charge a fee for, acceptance of support, warranty, indemnity,
169
+ or other liability obligations and/or rights consistent with this
170
+ License. However, in accepting such obligations, You may act only
171
+ on Your own behalf and on Your sole responsibility, not on behalf
172
+ of any other Contributor, and only if You agree to indemnify,
173
+ defend, and hold each Contributor harmless for any liability
174
+ incurred by, or claims asserted against, such Contributor by reason
175
+ of your accepting any such warranty or additional liability.
176
+
177
+ END OF TERMS AND CONDITIONS
178
+
179
+ APPENDIX: How to apply the Apache License to your work.
180
+
181
+ To apply the Apache License to your work, attach the following
182
+ boilerplate notice, with the fields enclosed by brackets "[]"
183
+ replaced with your own identifying information. (Don't include
184
+ the brackets!) The text should be enclosed in the appropriate
185
+ comment syntax for the file format. We also recommend that a
186
+ file or class name and description of purpose be included on the
187
+ same "printed page" as the copyright notice for easier
188
+ identification within third-party archives.
189
+
190
+ Copyright [yyyy] [name of copyright owner]
191
+
192
+ Licensed under the Apache License, Version 2.0 (the "License");
193
+ you may not use this file except in compliance with the License.
194
+ You may obtain a copy of the License at
195
+
196
+ http://www.apache.org/licenses/LICENSE-2.0
197
+
198
+ Unless required by applicable law or agreed to in writing, software
199
+ distributed under the License is distributed on an "AS IS" BASIS,
200
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
201
+ See the License for the specific language governing permissions and
202
+ limitations under the License.