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.
- ark_agent_runtime-0.1.0a1.data/purelib/ark/__init__.py +43 -0
- ark_agent_runtime-0.1.0a1.data/purelib/ark/_bridge/__init__.py +8 -0
- ark_agent_runtime-0.1.0a1.data/purelib/ark/_bridge/ark-bridge.exe +0 -0
- ark_agent_runtime-0.1.0a1.data/purelib/ark/bridge.py +96 -0
- ark_agent_runtime-0.1.0a1.data/purelib/ark/client.py +82 -0
- ark_agent_runtime-0.1.0a1.data/purelib/ark/errors.py +14 -0
- ark_agent_runtime-0.1.0a1.data/purelib/ark/integrations/__init__.py +7 -0
- ark_agent_runtime-0.1.0a1.data/purelib/ark/integrations/langgraph.py +243 -0
- ark_agent_runtime-0.1.0a1.data/purelib/ark/models.py +229 -0
- ark_agent_runtime-0.1.0a1.data/purelib/ark/session.py +235 -0
- ark_agent_runtime-0.1.0a1.dist-info/METADATA +82 -0
- ark_agent_runtime-0.1.0a1.dist-info/RECORD +15 -0
- ark_agent_runtime-0.1.0a1.dist-info/WHEEL +5 -0
- ark_agent_runtime-0.1.0a1.dist-info/licenses/LICENSE +202 -0
- ark_agent_runtime-0.1.0a1.dist-info/top_level.txt +1 -0
|
@@ -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
|
+
"""
|
|
Binary file
|
|
@@ -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,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.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
ark
|