ags-cli 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- ags_cli-0.1.0.dist-info/METADATA +332 -0
- ags_cli-0.1.0.dist-info/RECORD +156 -0
- ags_cli-0.1.0.dist-info/WHEEL +5 -0
- ags_cli-0.1.0.dist-info/entry_points.txt +2 -0
- ags_cli-0.1.0.dist-info/top_level.txt +2 -0
- cli/__init__.py +0 -0
- cli/__main__.py +4 -0
- cli/client.py +145 -0
- cli/commands/__init__.py +0 -0
- cli/commands/adapter.py +33 -0
- cli/commands/artifact.py +21 -0
- cli/commands/ask.py +212 -0
- cli/commands/chat.py +649 -0
- cli/commands/diff.py +49 -0
- cli/commands/doctor.py +93 -0
- cli/commands/gui.py +45 -0
- cli/commands/init.py +20 -0
- cli/commands/mcp.py +50 -0
- cli/commands/memory.py +65 -0
- cli/commands/model.py +73 -0
- cli/commands/policy.py +52 -0
- cli/commands/project.py +122 -0
- cli/commands/quota.py +107 -0
- cli/commands/run.py +84 -0
- cli/commands/serve.py +171 -0
- cli/commands/service.py +236 -0
- cli/commands/session.py +219 -0
- cli/commands/stats.py +96 -0
- cli/commands/status.py +36 -0
- cli/commands/task.py +37 -0
- cli/commands/usage.py +86 -0
- cli/local.py +84 -0
- cli/main.py +149 -0
- harness/__init__.py +4 -0
- harness/adapters/__init__.py +26 -0
- harness/adapters/antigravity.py +301 -0
- harness/adapters/claude_code.py +522 -0
- harness/adapters/local_openai.py +534 -0
- harness/adapters/mock.py +112 -0
- harness/adapters/probe.py +65 -0
- harness/adapters/registry.py +56 -0
- harness/adapters/warm_pool.py +255 -0
- harness/api/__init__.py +0 -0
- harness/api/router.py +38 -0
- harness/api/v1/__init__.py +0 -0
- harness/api/v1/adapters.py +105 -0
- harness/api/v1/approvals.py +173 -0
- harness/api/v1/artifacts.py +44 -0
- harness/api/v1/attachments.py +48 -0
- harness/api/v1/doctor.py +22 -0
- harness/api/v1/health.py +37 -0
- harness/api/v1/mcp.py +131 -0
- harness/api/v1/memory.py +80 -0
- harness/api/v1/policies.py +49 -0
- harness/api/v1/project.py +302 -0
- harness/api/v1/runs.py +98 -0
- harness/api/v1/sessions.py +248 -0
- harness/api/v1/stream.py +110 -0
- harness/api/v1/tasks.py +30 -0
- harness/api/v1/usage.py +58 -0
- harness/bootstrap.py +216 -0
- harness/db.py +164 -0
- harness/deps.py +58 -0
- harness/eventbus/__init__.py +20 -0
- harness/eventbus/inprocess_bus.py +85 -0
- harness/main.py +144 -0
- harness/mcp/__init__.py +22 -0
- harness/mcp/client.py +139 -0
- harness/mcp/config_gen.py +77 -0
- harness/mcp/registry.py +156 -0
- harness/mcp/tools.py +81 -0
- harness/memory/__init__.py +13 -0
- harness/memory/context_budget.py +116 -0
- harness/memory/embed.py +217 -0
- harness/memory/extract.py +74 -0
- harness/memory/ingest.py +106 -0
- harness/memory/namespaces.py +57 -0
- harness/memory/ranking.py +31 -0
- harness/memory/redact.py +37 -0
- harness/memory/service.py +320 -0
- harness/memory/sqlite_vec_backend.py +266 -0
- harness/memory/summarize.py +68 -0
- harness/models/__init__.py +35 -0
- harness/models/adapter_health.py +27 -0
- harness/models/approval.py +37 -0
- harness/models/artifact.py +35 -0
- harness/models/audit_log.py +33 -0
- harness/models/comparison.py +33 -0
- harness/models/enums.py +146 -0
- harness/models/mcp_server_config.py +49 -0
- harness/models/memory.py +76 -0
- harness/models/policy.py +29 -0
- harness/models/provider_config.py +35 -0
- harness/models/run.py +46 -0
- harness/models/run_event.py +35 -0
- harness/models/session.py +50 -0
- harness/models/task.py +30 -0
- harness/models/types.py +63 -0
- harness/models/workspace.py +27 -0
- harness/observability/__init__.py +4 -0
- harness/observability/audit.py +88 -0
- harness/observability/logging.py +47 -0
- harness/observability/metrics.py +43 -0
- harness/observability/tracing.py +38 -0
- harness/orchestrator/__init__.py +19 -0
- harness/orchestrator/engine.py +821 -0
- harness/orchestrator/handoff.py +33 -0
- harness/orchestrator/lifecycle.py +69 -0
- harness/orchestrator/native_resume.py +93 -0
- harness/orchestrator/policies.py +101 -0
- harness/orchestrator/reconcile.py +39 -0
- harness/orchestrator/run_graph.py +62 -0
- harness/orchestrator/snapshot.py +49 -0
- harness/schemas/__init__.py +1 -0
- harness/schemas/adapter.py +26 -0
- harness/schemas/approval.py +25 -0
- harness/schemas/artifact.py +17 -0
- harness/schemas/common.py +20 -0
- harness/schemas/memory.py +50 -0
- harness/schemas/policy.py +39 -0
- harness/schemas/run.py +116 -0
- harness/schemas/session.py +93 -0
- harness/schemas/task.py +24 -0
- harness/security/__init__.py +5 -0
- harness/security/approval_policy.py +107 -0
- harness/security/auth.py +106 -0
- harness/security/permissions.py +59 -0
- harness/security/risk.py +59 -0
- harness/security/secrets.py +49 -0
- harness/services/__init__.py +1 -0
- harness/services/adapters_health.py +212 -0
- harness/services/approvals.py +84 -0
- harness/services/artifacts.py +78 -0
- harness/services/attachments.py +228 -0
- harness/services/comparison.py +345 -0
- harness/services/doctor.py +156 -0
- harness/services/files.py +287 -0
- harness/services/mcp_servers.py +179 -0
- harness/services/project_git.py +101 -0
- harness/services/retention.py +97 -0
- harness/services/runs.py +155 -0
- harness/services/runs_diff.py +201 -0
- harness/services/sessions.py +242 -0
- harness/services/ssh.py +300 -0
- harness/services/stats.py +132 -0
- harness/services/tasks.py +41 -0
- harness/services/workspaces.py +184 -0
- harness/services/worktrees.py +439 -0
- harness/settings.py +186 -0
- harness/skills/__init__.py +11 -0
- harness/skills/manager.py +141 -0
- harness/tools/__init__.py +1 -0
- harness/tools/fs_tools.py +295 -0
- harness/workers/__init__.py +0 -0
- harness/workers/dispatch.py +26 -0
- harness/workers/inprocess.py +135 -0
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import uuid
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
|
|
6
|
+
from sqlalchemy import BigInteger, DateTime, Enum, ForeignKey, UniqueConstraint, func
|
|
7
|
+
from sqlalchemy.orm import Mapped, mapped_column
|
|
8
|
+
|
|
9
|
+
from harness.db import Base
|
|
10
|
+
from harness.models.enums import RunEventType
|
|
11
|
+
from harness.models.types import GUID, json_col
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class RunEvent(Base):
|
|
15
|
+
"""Append-only event ledger — the source of truth for a run. Run/session rows
|
|
16
|
+
are projections rebuilt from these events where practical. ``(run_id, seq)`` is
|
|
17
|
+
unique and monotonic for deterministic replay and timeline rendering."""
|
|
18
|
+
|
|
19
|
+
__tablename__ = "run_events"
|
|
20
|
+
__table_args__ = (
|
|
21
|
+
UniqueConstraint("run_id", "seq", name="uq_run_events_run_id_seq"),
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
id: Mapped[uuid.UUID] = mapped_column(
|
|
25
|
+
GUID(), primary_key=True, default=uuid.uuid4
|
|
26
|
+
)
|
|
27
|
+
run_id: Mapped[uuid.UUID] = mapped_column(
|
|
28
|
+
GUID(), ForeignKey("runs.id", ondelete="CASCADE"), index=True
|
|
29
|
+
)
|
|
30
|
+
seq: Mapped[int] = mapped_column(BigInteger)
|
|
31
|
+
ts: Mapped[datetime] = mapped_column(
|
|
32
|
+
DateTime(timezone=True), server_default=func.now()
|
|
33
|
+
)
|
|
34
|
+
type: Mapped[RunEventType] = mapped_column(Enum(RunEventType, name="run_event_type"))
|
|
35
|
+
payload: Mapped[dict] = mapped_column(json_col(), default=dict)
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import uuid
|
|
4
|
+
|
|
5
|
+
from sqlalchemy import Enum, ForeignKey, Index, Text
|
|
6
|
+
from sqlalchemy.orm import Mapped, mapped_column
|
|
7
|
+
|
|
8
|
+
from harness.db import Base, TimestampMixin
|
|
9
|
+
from harness.models.enums import SessionStatus
|
|
10
|
+
from harness.models.types import GUID, json_col
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class Session(Base, TimestampMixin):
|
|
14
|
+
"""A canonical, provider-independent unit of context. Holds the objective,
|
|
15
|
+
constraints, rolling summary, and references to provider runs. Sessions can
|
|
16
|
+
branch (``parent_session_id``) and be resumed from the summary, not the raw
|
|
17
|
+
transcript."""
|
|
18
|
+
|
|
19
|
+
__tablename__ = "sessions"
|
|
20
|
+
__table_args__ = (
|
|
21
|
+
# Recent-session lookup per workspace.
|
|
22
|
+
Index("ix_sessions_workspace_updated", "workspace_id", "updated_at"),
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
id: Mapped[uuid.UUID] = mapped_column(
|
|
26
|
+
GUID(), primary_key=True, default=uuid.uuid4
|
|
27
|
+
)
|
|
28
|
+
workspace_id: Mapped[uuid.UUID] = mapped_column(
|
|
29
|
+
GUID(), ForeignKey("workspaces.id", ondelete="CASCADE"), index=True
|
|
30
|
+
)
|
|
31
|
+
task_id: Mapped[uuid.UUID | None] = mapped_column(
|
|
32
|
+
GUID(), ForeignKey("tasks.id", ondelete="SET NULL"), nullable=True
|
|
33
|
+
)
|
|
34
|
+
parent_session_id: Mapped[uuid.UUID | None] = mapped_column(
|
|
35
|
+
GUID(), ForeignKey("sessions.id", ondelete="SET NULL"), nullable=True
|
|
36
|
+
)
|
|
37
|
+
canonical_objective: Mapped[str] = mapped_column(Text)
|
|
38
|
+
constraints: Mapped[dict] = mapped_column(json_col(), default=dict)
|
|
39
|
+
# Per-session runtime settings overriding workspace defaults. Keys:
|
|
40
|
+
# write_mode (bool|None: None = inherit workspace), workspace_mode
|
|
41
|
+
# ("main"|"worktree"), worktree_path/branch/base, worktree_fallback_reason,
|
|
42
|
+
# merged_at, merge_commit. Writers must copy-then-reassign the dict so the
|
|
43
|
+
# JSON column is flagged dirty (same pattern as Workspace.settings).
|
|
44
|
+
settings: Mapped[dict] = mapped_column(json_col(), default=dict)
|
|
45
|
+
native_sessions: Mapped[dict] = mapped_column(json_col(), default=dict)
|
|
46
|
+
summary: Mapped[str] = mapped_column(Text, default="")
|
|
47
|
+
summary_snapshots: Mapped[list] = mapped_column(json_col(), default=list)
|
|
48
|
+
status: Mapped[SessionStatus] = mapped_column(
|
|
49
|
+
Enum(SessionStatus, name="session_status"), default=SessionStatus.open
|
|
50
|
+
)
|
harness/models/task.py
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import uuid
|
|
4
|
+
|
|
5
|
+
from sqlalchemy import Enum, ForeignKey, String, Text
|
|
6
|
+
from sqlalchemy.orm import Mapped, mapped_column
|
|
7
|
+
|
|
8
|
+
from harness.db import Base, TimestampMixin
|
|
9
|
+
from harness.models.enums import TaskStatus
|
|
10
|
+
from harness.models.types import GUID, json_col
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class Task(Base, TimestampMixin):
|
|
14
|
+
"""A unit of work the user wants done; may spawn one or many sessions/runs."""
|
|
15
|
+
|
|
16
|
+
__tablename__ = "tasks"
|
|
17
|
+
|
|
18
|
+
id: Mapped[uuid.UUID] = mapped_column(
|
|
19
|
+
GUID(), primary_key=True, default=uuid.uuid4
|
|
20
|
+
)
|
|
21
|
+
workspace_id: Mapped[uuid.UUID] = mapped_column(
|
|
22
|
+
GUID(), ForeignKey("workspaces.id", ondelete="CASCADE"), index=True
|
|
23
|
+
)
|
|
24
|
+
title: Mapped[str] = mapped_column(String(512))
|
|
25
|
+
objective: Mapped[str] = mapped_column(Text)
|
|
26
|
+
constraints: Mapped[dict] = mapped_column(json_col(), default=dict)
|
|
27
|
+
status: Mapped[TaskStatus] = mapped_column(
|
|
28
|
+
Enum(TaskStatus, name="task_status"), default=TaskStatus.created
|
|
29
|
+
)
|
|
30
|
+
created_by: Mapped[str] = mapped_column(String(128), default="cli")
|
harness/models/types.py
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"""Portable column types.
|
|
2
|
+
|
|
3
|
+
The platform runs on SQLite (daemonless, single profile). These helpers wrap the
|
|
4
|
+
column types used across the ORM models so that intent (UUID, JSON object/array,
|
|
5
|
+
list of strings, list of UUIDs, embedding vector) reads clearly at the model
|
|
6
|
+
definition site, even though most of them now just delegate to a plain SQLAlchemy
|
|
7
|
+
type.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import uuid
|
|
13
|
+
|
|
14
|
+
from sqlalchemy import JSON, Uuid
|
|
15
|
+
from sqlalchemy.types import TypeDecorator, TypeEngine
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def GUID() -> TypeEngine:
|
|
19
|
+
"""UUID column, stored as ``CHAR(32)`` on SQLite.
|
|
20
|
+
|
|
21
|
+
``Uuid`` returns/accepts ``uuid.UUID`` objects."""
|
|
22
|
+
return Uuid(as_uuid=True)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def json_col() -> TypeEngine:
|
|
26
|
+
"""A JSON object/array column."""
|
|
27
|
+
return JSON()
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def str_array() -> TypeEngine:
|
|
31
|
+
"""A list of strings, stored as a JSON list."""
|
|
32
|
+
return JSON()
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class _UUIDListJSON(TypeDecorator):
|
|
36
|
+
"""Stores ``list[uuid.UUID]`` as a JSON array of strings.
|
|
37
|
+
|
|
38
|
+
Coerces to/from ``uuid.UUID`` so callers see UUID objects rather than raw
|
|
39
|
+
strings."""
|
|
40
|
+
|
|
41
|
+
impl = JSON
|
|
42
|
+
cache_ok = True
|
|
43
|
+
|
|
44
|
+
def process_bind_param(self, value, dialect): # noqa: ANN001
|
|
45
|
+
if value is None:
|
|
46
|
+
return None
|
|
47
|
+
return [str(v) for v in value]
|
|
48
|
+
|
|
49
|
+
def process_result_value(self, value, dialect): # noqa: ANN001
|
|
50
|
+
if value is None:
|
|
51
|
+
return None
|
|
52
|
+
return [v if isinstance(v, uuid.UUID) else uuid.UUID(v) for v in value]
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def uuid_array() -> TypeEngine:
|
|
56
|
+
"""A list of UUIDs, stored as a JSON list of strings."""
|
|
57
|
+
return _UUIDListJSON()
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def embedding_col(dim: int) -> TypeEngine:
|
|
61
|
+
"""An embedding vector, stored as a JSON list of floats. Similarity is computed
|
|
62
|
+
in Python (see ``memory/service.py``)."""
|
|
63
|
+
return JSON()
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import uuid
|
|
4
|
+
|
|
5
|
+
from sqlalchemy import Enum, String
|
|
6
|
+
from sqlalchemy.orm import Mapped, mapped_column
|
|
7
|
+
|
|
8
|
+
from harness.db import Base, TimestampMixin
|
|
9
|
+
from harness.models.enums import TrustPolicy
|
|
10
|
+
from harness.models.types import GUID, json_col
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class Workspace(Base, TimestampMixin):
|
|
14
|
+
"""A memory partition and trust boundary (e.g. one repo/project)."""
|
|
15
|
+
|
|
16
|
+
__tablename__ = "workspaces"
|
|
17
|
+
|
|
18
|
+
id: Mapped[uuid.UUID] = mapped_column(
|
|
19
|
+
GUID(), primary_key=True, default=uuid.uuid4
|
|
20
|
+
)
|
|
21
|
+
slug: Mapped[str] = mapped_column(String(128), unique=True, index=True)
|
|
22
|
+
name: Mapped[str] = mapped_column(String(256))
|
|
23
|
+
trust_policy: Mapped[TrustPolicy] = mapped_column(
|
|
24
|
+
Enum(TrustPolicy, name="trust_policy"), default=TrustPolicy.restricted
|
|
25
|
+
)
|
|
26
|
+
default_routing_policy: Mapped[str] = mapped_column(String(128), default="single")
|
|
27
|
+
settings: Mapped[dict] = mapped_column(json_col(), default=dict)
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"""Immutable, hash-chained audit log writer.
|
|
2
|
+
|
|
3
|
+
Each record's ``hash = sha256(prev_hash || canonical_json(record))``. The chain
|
|
4
|
+
makes tampering or deletion detectable: re-computing the chain and comparing the
|
|
5
|
+
head hash verifies integrity. Application code only ever appends."""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import hashlib
|
|
10
|
+
import json
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
from sqlalchemy import select
|
|
14
|
+
from sqlalchemy.ext.asyncio import AsyncSession
|
|
15
|
+
|
|
16
|
+
from harness.models.audit_log import AuditLog
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _canonical(payload: dict[str, Any]) -> str:
|
|
20
|
+
return json.dumps(payload, sort_keys=True, separators=(",", ":"), default=str)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _compute_hash(prev_hash: str | None, record: dict[str, Any]) -> str:
|
|
24
|
+
h = hashlib.sha256()
|
|
25
|
+
h.update((prev_hash or "").encode())
|
|
26
|
+
h.update(_canonical(record).encode())
|
|
27
|
+
return h.hexdigest()
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
async def write_audit(
|
|
31
|
+
session: AsyncSession,
|
|
32
|
+
*,
|
|
33
|
+
actor: str,
|
|
34
|
+
action: str,
|
|
35
|
+
target_type: str,
|
|
36
|
+
target_id: str | None = None,
|
|
37
|
+
before: dict | None = None,
|
|
38
|
+
after: dict | None = None,
|
|
39
|
+
) -> AuditLog:
|
|
40
|
+
"""Append one tamper-evident audit row. Caller's transaction commits it."""
|
|
41
|
+
prev = (
|
|
42
|
+
await session.execute(
|
|
43
|
+
select(AuditLog).order_by(AuditLog.ts.desc()).limit(1)
|
|
44
|
+
)
|
|
45
|
+
).scalar_one_or_none()
|
|
46
|
+
prev_hash = prev.hash if prev else None
|
|
47
|
+
|
|
48
|
+
record = {
|
|
49
|
+
"actor": actor,
|
|
50
|
+
"action": action,
|
|
51
|
+
"target_type": target_type,
|
|
52
|
+
"target_id": target_id,
|
|
53
|
+
"before": before,
|
|
54
|
+
"after": after,
|
|
55
|
+
}
|
|
56
|
+
row = AuditLog(
|
|
57
|
+
actor=actor,
|
|
58
|
+
action=action,
|
|
59
|
+
target_type=target_type,
|
|
60
|
+
target_id=target_id,
|
|
61
|
+
before=before,
|
|
62
|
+
after=after,
|
|
63
|
+
prev_hash=prev_hash,
|
|
64
|
+
hash=_compute_hash(prev_hash, record),
|
|
65
|
+
)
|
|
66
|
+
session.add(row)
|
|
67
|
+
return row
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
async def verify_chain(session: AsyncSession) -> bool:
|
|
71
|
+
"""Recompute the whole chain; returns True iff every link is intact."""
|
|
72
|
+
rows = (
|
|
73
|
+
await session.execute(select(AuditLog).order_by(AuditLog.ts.asc()))
|
|
74
|
+
).scalars().all()
|
|
75
|
+
prev_hash: str | None = None
|
|
76
|
+
for row in rows:
|
|
77
|
+
record = {
|
|
78
|
+
"actor": row.actor,
|
|
79
|
+
"action": row.action,
|
|
80
|
+
"target_type": row.target_type,
|
|
81
|
+
"target_id": row.target_id,
|
|
82
|
+
"before": row.before,
|
|
83
|
+
"after": row.after,
|
|
84
|
+
}
|
|
85
|
+
if row.prev_hash != prev_hash or row.hash != _compute_hash(prev_hash, record):
|
|
86
|
+
return False
|
|
87
|
+
prev_hash = row.hash
|
|
88
|
+
return True
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"""Structured JSON logging via structlog. One configuration entrypoint called at
|
|
2
|
+
app and worker startup."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import logging
|
|
7
|
+
import sys
|
|
8
|
+
|
|
9
|
+
import structlog
|
|
10
|
+
|
|
11
|
+
from harness.settings import get_settings
|
|
12
|
+
|
|
13
|
+
_configured = False
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def configure_logging() -> None:
|
|
17
|
+
global _configured
|
|
18
|
+
if _configured:
|
|
19
|
+
return
|
|
20
|
+
settings = get_settings()
|
|
21
|
+
level = getattr(logging, settings.log_level.upper(), logging.INFO)
|
|
22
|
+
|
|
23
|
+
shared = [
|
|
24
|
+
structlog.contextvars.merge_contextvars,
|
|
25
|
+
structlog.processors.add_log_level,
|
|
26
|
+
structlog.processors.TimeStamper(fmt="iso"),
|
|
27
|
+
structlog.processors.StackInfoRenderer(),
|
|
28
|
+
structlog.processors.format_exc_info,
|
|
29
|
+
]
|
|
30
|
+
renderer = (
|
|
31
|
+
structlog.processors.JSONRenderer()
|
|
32
|
+
if settings.log_json
|
|
33
|
+
else structlog.dev.ConsoleRenderer()
|
|
34
|
+
)
|
|
35
|
+
structlog.configure(
|
|
36
|
+
processors=[*shared, renderer],
|
|
37
|
+
wrapper_class=structlog.make_filtering_bound_logger(level),
|
|
38
|
+
logger_factory=structlog.PrintLoggerFactory(file=sys.stdout),
|
|
39
|
+
cache_logger_on_first_use=True,
|
|
40
|
+
)
|
|
41
|
+
logging.basicConfig(level=level, stream=sys.stdout, format="%(message)s")
|
|
42
|
+
_configured = True
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def get_logger(name: str = "harness") -> structlog.stdlib.BoundLogger:
|
|
46
|
+
configure_logging()
|
|
47
|
+
return structlog.get_logger(name)
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""Prometheus metrics. Exposed at ``GET /metrics`` (see api.v1.health)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from prometheus_client import Counter, Gauge, Histogram
|
|
6
|
+
|
|
7
|
+
ADAPTER_LATENCY = Histogram(
|
|
8
|
+
"harness_adapter_latency_seconds",
|
|
9
|
+
"End-to-end adapter run latency.",
|
|
10
|
+
["adapter_kind", "provider"],
|
|
11
|
+
buckets=(0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10, 30, 60, 120, 300),
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
RUN_TOTAL = Counter(
|
|
15
|
+
"harness_runs_total", "Runs by terminal status.", ["adapter_kind", "status"]
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
RUN_FAILURES = Counter(
|
|
19
|
+
"harness_run_failures_total", "Run failures by adapter and reason.",
|
|
20
|
+
["adapter_kind", "reason"],
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
TOKENS_TOTAL = Counter(
|
|
24
|
+
"harness_tokens_total", "Token accounting.", ["adapter_kind", "direction"]
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
COST_USD_TOTAL = Counter(
|
|
28
|
+
"harness_cost_usd_total", "Estimated cost in USD where known.", ["adapter_kind"]
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
MEMORY_QUERIES = Counter("harness_memory_queries_total", "Memory retrieval queries.")
|
|
32
|
+
MEMORY_HITS = Counter(
|
|
33
|
+
"harness_memory_hits_total", "Retrievals that returned >=1 item above threshold."
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
ACTIVE_RUNS = Gauge("harness_active_runs", "Currently running/streaming runs.")
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def memory_hit_rate() -> float:
|
|
40
|
+
"""Convenience for the GUI/health endpoint."""
|
|
41
|
+
queries = MEMORY_QUERIES._value.get() # type: ignore[attr-defined]
|
|
42
|
+
hits = MEMORY_HITS._value.get() # type: ignore[attr-defined]
|
|
43
|
+
return (hits / queries) if queries else 0.0
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"""OpenTelemetry bootstrap. No-op unless ``HARNESS_OTEL_ENABLED=true`` so local
|
|
2
|
+
dev and CI stay dependency-light."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from harness.settings import get_settings
|
|
9
|
+
|
|
10
|
+
_tracer: Any = None
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def setup_tracing(app: Any = None) -> None:
|
|
14
|
+
global _tracer
|
|
15
|
+
settings = get_settings()
|
|
16
|
+
if not settings.otel_enabled:
|
|
17
|
+
return
|
|
18
|
+
from opentelemetry import trace
|
|
19
|
+
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter
|
|
20
|
+
from opentelemetry.sdk.resources import Resource
|
|
21
|
+
from opentelemetry.sdk.trace import TracerProvider
|
|
22
|
+
from opentelemetry.sdk.trace.export import BatchSpanProcessor
|
|
23
|
+
|
|
24
|
+
provider = TracerProvider(resource=Resource.create({"service.name": "ags"}))
|
|
25
|
+
provider.add_span_processor(
|
|
26
|
+
BatchSpanProcessor(OTLPSpanExporter(endpoint=settings.otel_exporter_otlp_endpoint))
|
|
27
|
+
)
|
|
28
|
+
trace.set_tracer_provider(provider)
|
|
29
|
+
_tracer = trace.get_tracer("harness")
|
|
30
|
+
|
|
31
|
+
if app is not None:
|
|
32
|
+
from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor
|
|
33
|
+
|
|
34
|
+
FastAPIInstrumentor.instrument_app(app)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def get_tracer() -> Any:
|
|
38
|
+
return _tracer
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
from harness.orchestrator.engine import (
|
|
2
|
+
NodeResult,
|
|
3
|
+
create_runs,
|
|
4
|
+
execute_node,
|
|
5
|
+
execute_session,
|
|
6
|
+
)
|
|
7
|
+
from harness.orchestrator.policies import CompiledPolicy, compile_policy, load_policy_specs
|
|
8
|
+
from harness.orchestrator.run_graph import RunGraph
|
|
9
|
+
|
|
10
|
+
__all__ = [
|
|
11
|
+
"CompiledPolicy",
|
|
12
|
+
"NodeResult",
|
|
13
|
+
"RunGraph",
|
|
14
|
+
"compile_policy",
|
|
15
|
+
"create_runs",
|
|
16
|
+
"execute_node",
|
|
17
|
+
"execute_session",
|
|
18
|
+
"load_policy_specs",
|
|
19
|
+
]
|