WP-0004: ecosystem integration complete
Add Helix Forge correlation (HELIX_SESSION_UID env, metrics correlate), artifact-store publish (metrics publish), activity-core ActivityDefinition references, integration patterns docs, and canon/knowledge design artifacts.
This commit is contained in:
@@ -11,6 +11,12 @@ from typing import List, Optional
|
||||
|
||||
from .registry import AgentRegistry, AgentCategory
|
||||
from .installer import AgentInstaller, ProjectInitializer, InstallationConfig
|
||||
from .integrations.artifact_store import (
|
||||
default_api_token,
|
||||
default_api_url,
|
||||
publish_optimizer_evidence,
|
||||
)
|
||||
from .integrations.helix import HelixCorrelationAdapter, enrich_helix_correlation
|
||||
from .metrics import MetricsStore, OptimizerStore, performance_summary_markdown
|
||||
from .optimization import OptimizationLoop, MIN_SAMPLES_FOR_RECOMMENDATIONS
|
||||
|
||||
@@ -999,6 +1005,8 @@ def metrics_record(
|
||||
if session_id:
|
||||
payload["session_id"] = session_id
|
||||
|
||||
payload = enrich_helix_correlation(payload)
|
||||
|
||||
if store.append(payload, idempotency_key=idempotency_key):
|
||||
click.echo(f"Recorded metrics for '{agent_name}'")
|
||||
else:
|
||||
@@ -1106,6 +1114,84 @@ def metrics_optimize(agent_name: Optional[str], target: str, min_samples: int):
|
||||
click.echo(f"Wrote optimizer analysis: {analysis_path}")
|
||||
|
||||
|
||||
@metrics.command("correlate")
|
||||
@click.argument("session_uid")
|
||||
@click.option(
|
||||
"--store-db",
|
||||
envvar="HELIX_STORE_DB",
|
||||
help="Helix Forge session-memory SQLite database path",
|
||||
)
|
||||
def metrics_correlate(session_uid: str, store_db: Optional[str]):
|
||||
"""Look up Helix Forge digest summary for a session UID (read-only)."""
|
||||
adapter = HelixCorrelationAdapter(
|
||||
store_db=Path(store_db).resolve() if store_db else None
|
||||
)
|
||||
if adapter.store_db is None:
|
||||
adapter = HelixCorrelationAdapter.from_env()
|
||||
summary = adapter.lookup(session_uid)
|
||||
click.echo(json.dumps(summary, indent=2, sort_keys=True))
|
||||
|
||||
|
||||
@metrics.command("publish")
|
||||
@click.option("--target", "-t", default=".", help="Project root (default: current)")
|
||||
@click.option(
|
||||
"--api-url",
|
||||
default=default_api_url,
|
||||
show_default=True,
|
||||
help="artifact-store API base URL (ARTIFACTSTORE_API_URL)",
|
||||
)
|
||||
@click.option(
|
||||
"--token",
|
||||
default=default_api_token,
|
||||
help="artifact-store bearer token (ARTIFACTSTORE_API_TOKEN)",
|
||||
)
|
||||
@click.option(
|
||||
"--subject",
|
||||
help="Package subject (default: project directory name)",
|
||||
)
|
||||
@click.option(
|
||||
"--retention-class",
|
||||
default="raw-evidence",
|
||||
show_default=True,
|
||||
help="artifact-store retention class",
|
||||
)
|
||||
def metrics_publish(
|
||||
target: str,
|
||||
api_url: str,
|
||||
token: str,
|
||||
subject: Optional[str],
|
||||
retention_class: str,
|
||||
):
|
||||
"""Publish optimizer evidence to artifact-store (optional integration)."""
|
||||
project_root = _project_root(target)
|
||||
if not token:
|
||||
click.echo(
|
||||
"Error: artifact-store token required. Set ARTIFACTSTORE_API_TOKEN or --token.",
|
||||
err=True,
|
||||
)
|
||||
sys.exit(1)
|
||||
try:
|
||||
result = publish_optimizer_evidence(
|
||||
project_root,
|
||||
api_url=api_url,
|
||||
token=token,
|
||||
subject=subject,
|
||||
retention_class=retention_class,
|
||||
)
|
||||
except FileNotFoundError as exc:
|
||||
click.echo(f"Error: {exc}", err=True)
|
||||
sys.exit(1)
|
||||
except RuntimeError as exc:
|
||||
click.echo(f"Error: {exc}", err=True)
|
||||
sys.exit(1)
|
||||
|
||||
click.echo(f"Published optimizer evidence package: {result.package_id}")
|
||||
click.echo(f" Files uploaded: {result.files_uploaded}")
|
||||
click.echo(f" Retention class: {result.retention_class}")
|
||||
if result.manifest_digest:
|
||||
click.echo(f" Manifest digest: {result.manifest_digest}")
|
||||
|
||||
|
||||
@metrics.command("export")
|
||||
@click.argument("agent_name")
|
||||
@click.option("--target", "-t", default=".", help="Project root (default: current)")
|
||||
|
||||
10
src/kaizen_agentic/integrations/__init__.py
Normal file
10
src/kaizen_agentic/integrations/__init__.py
Normal file
@@ -0,0 +1,10 @@
|
||||
"""Ecosystem integration adapters (Helix Forge, artifact-store)."""
|
||||
|
||||
from .artifact_store import publish_optimizer_evidence
|
||||
from .helix import HelixCorrelationAdapter, enrich_helix_correlation
|
||||
|
||||
__all__ = [
|
||||
"HelixCorrelationAdapter",
|
||||
"enrich_helix_correlation",
|
||||
"publish_optimizer_evidence",
|
||||
]
|
||||
233
src/kaizen_agentic/integrations/artifact_store.py
Normal file
233
src/kaizen_agentic/integrations/artifact_store.py
Normal file
@@ -0,0 +1,233 @@
|
||||
"""artifact-store publish adapter for optimizer evidence (WP-0004 Part 3)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import uuid
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List, Optional
|
||||
from urllib import error, parse, request
|
||||
|
||||
from ..metrics import OptimizerStore
|
||||
|
||||
ENV_API_URL = "ARTIFACTSTORE_API_URL"
|
||||
ENV_API_TOKEN = "ARTIFACTSTORE_API_TOKEN"
|
||||
DEFAULT_RETENTION_CLASS = "raw-evidence"
|
||||
PRODUCER = "kaizen-agentic"
|
||||
|
||||
|
||||
@dataclass
|
||||
class PublishResult:
|
||||
package_id: str
|
||||
manifest_digest: Optional[str]
|
||||
files_uploaded: int
|
||||
retention_class: str
|
||||
|
||||
|
||||
def build_optimizer_manifest(
|
||||
project_root: Path,
|
||||
*,
|
||||
agents: Optional[List[str]] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""Manifest metadata for an optimizer evidence package."""
|
||||
store = OptimizerStore(project_root)
|
||||
analysis = {}
|
||||
if store.analysis_path.exists():
|
||||
analysis = json.loads(store.analysis_path.read_text(encoding="utf-8"))
|
||||
|
||||
return {
|
||||
"schema": "kaizen-agentic/optimizer-evidence/v1",
|
||||
"project": project_root.name,
|
||||
"project_root": str(project_root.resolve()),
|
||||
"producer": PRODUCER,
|
||||
"retention_class": DEFAULT_RETENTION_CLASS,
|
||||
"retention_days": 180,
|
||||
"optimized_at": analysis.get("optimized_at"),
|
||||
"agents": agents or [item.get("agent") for item in analysis.get("agents", [])],
|
||||
"files": [
|
||||
"optimizer/analysis.json",
|
||||
"optimizer/recommendations.jsonl",
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
def publish_optimizer_evidence(
|
||||
project_root: Path,
|
||||
*,
|
||||
api_url: str,
|
||||
token: str,
|
||||
subject: Optional[str] = None,
|
||||
retention_class: str = DEFAULT_RETENTION_CLASS,
|
||||
) -> PublishResult:
|
||||
"""Register optimizer outputs as an artifact-store package."""
|
||||
store = OptimizerStore(project_root)
|
||||
if not store.analysis_path.exists():
|
||||
raise FileNotFoundError(
|
||||
f"No optimizer analysis at {store.analysis_path}. "
|
||||
"Run: kaizen-agentic metrics optimize"
|
||||
)
|
||||
|
||||
manifest = build_optimizer_manifest(project_root)
|
||||
package_name = f"kaizen-optimizer-{project_root.name}"
|
||||
package_subject = subject or project_root.name
|
||||
|
||||
created = _http_json(
|
||||
"POST",
|
||||
api_url,
|
||||
"/packages",
|
||||
token,
|
||||
{
|
||||
"name": package_name,
|
||||
"producer": PRODUCER,
|
||||
"subject": package_subject,
|
||||
"retention_class": retention_class,
|
||||
"metadata": manifest,
|
||||
},
|
||||
)
|
||||
package_id = created["id"]
|
||||
|
||||
uploads = [
|
||||
(
|
||||
store.analysis_path,
|
||||
"optimizer/analysis.json",
|
||||
"application/json",
|
||||
),
|
||||
]
|
||||
if store.recommendations_path.exists():
|
||||
uploads.append(
|
||||
(
|
||||
store.recommendations_path,
|
||||
"optimizer/recommendations.jsonl",
|
||||
"application/x-ndjson",
|
||||
)
|
||||
)
|
||||
|
||||
for path, relative_path, media_type in uploads:
|
||||
_http_multipart(
|
||||
api_url,
|
||||
f"/packages/{package_id}/files",
|
||||
token,
|
||||
fields={"relative_path": relative_path, "media_type": media_type},
|
||||
file_field="file",
|
||||
file_name=path.name,
|
||||
file_content_type=media_type,
|
||||
file_bytes=path.read_bytes(),
|
||||
)
|
||||
|
||||
finalized = _http_json(
|
||||
"POST",
|
||||
api_url,
|
||||
f"/packages/{package_id}/finalize",
|
||||
token,
|
||||
{},
|
||||
)
|
||||
|
||||
return PublishResult(
|
||||
package_id=package_id,
|
||||
manifest_digest=finalized.get("manifest_digest"),
|
||||
files_uploaded=len(uploads),
|
||||
retention_class=retention_class,
|
||||
)
|
||||
|
||||
|
||||
def default_api_url() -> str:
|
||||
return os.environ.get(ENV_API_URL, "http://127.0.0.1:8000").rstrip("/")
|
||||
|
||||
|
||||
def default_api_token() -> str:
|
||||
return os.environ.get(ENV_API_TOKEN, "")
|
||||
|
||||
|
||||
def _http_json(
|
||||
method: str,
|
||||
base_url: str,
|
||||
path: str,
|
||||
token: str,
|
||||
payload: Dict[str, Any],
|
||||
) -> Dict[str, Any]:
|
||||
body = json.dumps(payload).encode("utf-8") if payload else None
|
||||
headers = {"Accept": "application/json"}
|
||||
if body is not None:
|
||||
headers["Content-Type"] = "application/json"
|
||||
response = _http_bytes(method, base_url, path, token, body=body, headers=headers)
|
||||
decoded = json.loads(response)
|
||||
if not isinstance(decoded, dict):
|
||||
raise ValueError(f"expected JSON object from {path}")
|
||||
return decoded
|
||||
|
||||
|
||||
def _http_multipart(
|
||||
base_url: str,
|
||||
path: str,
|
||||
token: str,
|
||||
*,
|
||||
fields: Dict[str, str],
|
||||
file_field: str,
|
||||
file_name: str,
|
||||
file_content_type: str,
|
||||
file_bytes: bytes,
|
||||
) -> Dict[str, Any]:
|
||||
boundary = f"kaizen-{uuid.uuid4().hex}"
|
||||
body = bytearray()
|
||||
for name, value in fields.items():
|
||||
body.extend(f"--{boundary}\r\n".encode("ascii"))
|
||||
body.extend(
|
||||
f'Content-Disposition: form-data; name="{_quote(name)}"\r\n\r\n'.encode()
|
||||
)
|
||||
body.extend(value.encode())
|
||||
body.extend(b"\r\n")
|
||||
body.extend(f"--{boundary}\r\n".encode("ascii"))
|
||||
body.extend(
|
||||
(
|
||||
f'Content-Disposition: form-data; name="{_quote(file_field)}"; '
|
||||
f'filename="{_quote(file_name)}"\r\n'
|
||||
f"Content-Type: {file_content_type}\r\n\r\n"
|
||||
).encode()
|
||||
)
|
||||
body.extend(file_bytes)
|
||||
body.extend(b"\r\n")
|
||||
body.extend(f"--{boundary}--\r\n".encode("ascii"))
|
||||
|
||||
response = _http_bytes(
|
||||
"POST",
|
||||
base_url,
|
||||
path,
|
||||
token,
|
||||
body=bytes(body),
|
||||
headers={
|
||||
"Content-Type": f"multipart/form-data; boundary={boundary}",
|
||||
"Accept": "application/json",
|
||||
},
|
||||
)
|
||||
decoded = json.loads(response)
|
||||
if not isinstance(decoded, dict):
|
||||
raise ValueError(f"expected JSON object from {path}")
|
||||
return decoded
|
||||
|
||||
|
||||
def _http_bytes(
|
||||
method: str,
|
||||
base_url: str,
|
||||
path: str,
|
||||
token: str,
|
||||
*,
|
||||
body: Optional[bytes] = None,
|
||||
headers: Optional[Dict[str, str]] = None,
|
||||
) -> bytes:
|
||||
url = f"{base_url.rstrip('/')}/{path.lstrip('/')}"
|
||||
effective_headers = dict(headers or {})
|
||||
if token:
|
||||
effective_headers["Authorization"] = f"Bearer {token}"
|
||||
req = request.Request(url, data=body, headers=effective_headers, method=method)
|
||||
try:
|
||||
with request.urlopen(req, timeout=30) as resp:
|
||||
return resp.read()
|
||||
except error.HTTPError as exc:
|
||||
detail = exc.read().decode("utf-8", errors="replace")
|
||||
raise RuntimeError(f"HTTP {exc.code} from {path}: {detail}") from exc
|
||||
|
||||
|
||||
def _quote(value: str) -> str:
|
||||
return parse.quote(value, safe="")
|
||||
171
src/kaizen_agentic/integrations/helix.py
Normal file
171
src/kaizen_agentic/integrations/helix.py
Normal file
@@ -0,0 +1,171 @@
|
||||
"""Helix Forge correlation adapter (ADR-004, agentic-resources)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import sqlite3
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
|
||||
ENV_SESSION_UID = "HELIX_SESSION_UID"
|
||||
ENV_REPO = "HELIX_REPO"
|
||||
ENV_FLAVOR = "HELIX_FLAVOR"
|
||||
ENV_TOKENS = "HELIX_TOKENS"
|
||||
ENV_INFRA_SHARE = "HELIX_INFRA_OVERHEAD_SHARE"
|
||||
ENV_STORE_DB = "HELIX_STORE_DB"
|
||||
|
||||
|
||||
def enrich_helix_correlation(record: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Apply optional Helix correlation fields from env or existing record."""
|
||||
payload = dict(record)
|
||||
|
||||
uid = payload.get("helix_session_uid") or os.environ.get(ENV_SESSION_UID)
|
||||
if uid:
|
||||
payload["helix_session_uid"] = uid
|
||||
|
||||
repo = payload.get("repo") or os.environ.get(ENV_REPO)
|
||||
if repo:
|
||||
payload["repo"] = repo
|
||||
|
||||
flavor = payload.get("flavor") or os.environ.get(ENV_FLAVOR)
|
||||
if flavor:
|
||||
payload["flavor"] = flavor
|
||||
|
||||
tokens_raw = payload.get("tokens")
|
||||
if tokens_raw is None and ENV_TOKENS in os.environ:
|
||||
try:
|
||||
tokens_raw = int(os.environ[ENV_TOKENS])
|
||||
except ValueError:
|
||||
pass
|
||||
if tokens_raw is not None:
|
||||
payload["tokens"] = int(tokens_raw)
|
||||
|
||||
infra = payload.get("infra_overhead_share")
|
||||
if infra is None and ENV_INFRA_SHARE in os.environ:
|
||||
try:
|
||||
infra = float(os.environ[ENV_INFRA_SHARE])
|
||||
except ValueError:
|
||||
pass
|
||||
if infra is not None:
|
||||
payload["infra_overhead_share"] = float(infra)
|
||||
|
||||
return payload
|
||||
|
||||
|
||||
def digest_to_correlation_summary(
|
||||
session_uid: str,
|
||||
digest: Dict[str, Any],
|
||||
*,
|
||||
adapter: str,
|
||||
) -> Dict[str, Any]:
|
||||
"""Project a Helix digest into ADR-004 correlation summary fields."""
|
||||
cost = digest.get("cost") or {}
|
||||
input_tokens = int(cost.get("input_tokens") or 0)
|
||||
output_tokens = int(cost.get("output_tokens") or 0)
|
||||
wall_clock_s = cost.get("wall_clock_s")
|
||||
|
||||
summary: Dict[str, Any] = {
|
||||
"helix_session_uid": session_uid,
|
||||
"repo": digest.get("repo"),
|
||||
"flavor": digest.get("flavor"),
|
||||
"fleet_outcome": digest.get("outcome"),
|
||||
"tokens": input_tokens + output_tokens,
|
||||
"adapter": adapter,
|
||||
}
|
||||
if wall_clock_s is not None:
|
||||
summary["wall_clock_s"] = float(wall_clock_s)
|
||||
|
||||
markers = digest.get("markers") or {}
|
||||
tool_histogram = digest.get("tool_histogram") or {}
|
||||
mcp_calls = sum(
|
||||
count for tool, count in tool_histogram.items() if str(tool).startswith("mcp__")
|
||||
)
|
||||
total_calls = sum(tool_histogram.values()) or 0
|
||||
if total_calls:
|
||||
summary["infra_overhead_share"] = round(mcp_calls / total_calls, 3)
|
||||
elif "infra_overhead_share" in digest:
|
||||
summary["infra_overhead_share"] = digest["infra_overhead_share"]
|
||||
|
||||
if markers:
|
||||
summary["markers"] = {
|
||||
key: markers[key]
|
||||
for key in ("errors", "retries", "test_runs")
|
||||
if key in markers
|
||||
}
|
||||
|
||||
return summary
|
||||
|
||||
|
||||
@dataclass
|
||||
class HelixCorrelationAdapter:
|
||||
"""Read-only lookup of Helix Forge session digests."""
|
||||
|
||||
store_db: Optional[Path] = None
|
||||
|
||||
@classmethod
|
||||
def from_env(cls) -> "HelixCorrelationAdapter":
|
||||
raw = os.environ.get(ENV_STORE_DB)
|
||||
return cls(store_db=Path(raw).resolve() if raw else None)
|
||||
|
||||
def lookup(self, session_uid: str) -> Dict[str, Any]:
|
||||
if self.store_db and self.store_db.exists():
|
||||
digest = self._load_digest_sqlite(session_uid)
|
||||
if digest is not None:
|
||||
return digest_to_correlation_summary(
|
||||
session_uid,
|
||||
digest,
|
||||
adapter="helix-sqlite",
|
||||
)
|
||||
return {
|
||||
"helix_session_uid": session_uid,
|
||||
"adapter": "helix-sqlite",
|
||||
"status": "not_found",
|
||||
"message": f"No digest for session_uid in {self.store_db}",
|
||||
}
|
||||
|
||||
return {
|
||||
"helix_session_uid": session_uid,
|
||||
"adapter": "stub",
|
||||
"status": "not_configured",
|
||||
"message": (
|
||||
"Set HELIX_STORE_DB to an agentic-resources session-memory SQLite "
|
||||
"database for live lookup. Correlation fields on project metrics "
|
||||
"still work via HELIX_SESSION_UID at record time."
|
||||
),
|
||||
"expected_fields": [
|
||||
"helix_session_uid",
|
||||
"repo",
|
||||
"flavor",
|
||||
"tokens",
|
||||
"infra_overhead_share",
|
||||
"fleet_outcome",
|
||||
"wall_clock_s",
|
||||
],
|
||||
}
|
||||
|
||||
def _load_digest_sqlite(self, session_uid: str) -> Optional[Dict[str, Any]]:
|
||||
conn = sqlite3.connect(str(self.store_db))
|
||||
try:
|
||||
row = conn.execute(
|
||||
"SELECT json FROM digests WHERE session_uid = ?",
|
||||
(session_uid,),
|
||||
).fetchone()
|
||||
if not row:
|
||||
return None
|
||||
digest = json.loads(row[0])
|
||||
digest.setdefault("session_uid", session_uid)
|
||||
|
||||
session_row = conn.execute(
|
||||
"SELECT json FROM sessions WHERE session_uid = ?",
|
||||
(session_uid,),
|
||||
).fetchone()
|
||||
if session_row:
|
||||
session = json.loads(session_row[0])
|
||||
digest.setdefault("repo", session.get("repo"))
|
||||
digest.setdefault("flavor", session.get("flavor"))
|
||||
return digest
|
||||
finally:
|
||||
conn.close()
|
||||
Reference in New Issue
Block a user