ags-cli 0.1.0__tar.gz → 0.2.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {ags_cli-0.1.0 → ags_cli-0.2.0}/PKG-INFO +1 -1
- {ags_cli-0.1.0 → ags_cli-0.2.0}/ags_cli.egg-info/PKG-INFO +1 -1
- ags_cli-0.2.0/harness/adapters/antigravity.py +338 -0
- {ags_cli-0.1.0 → ags_cli-0.2.0}/harness/adapters/claude_code.py +46 -6
- {ags_cli-0.1.0 → ags_cli-0.2.0}/harness/adapters/probe.py +13 -2
- {ags_cli-0.1.0 → ags_cli-0.2.0}/harness/memory/context_budget.py +8 -2
- {ags_cli-0.1.0 → ags_cli-0.2.0}/pyproject.toml +1 -1
- ags_cli-0.1.0/harness/adapters/antigravity.py +0 -301
- {ags_cli-0.1.0 → ags_cli-0.2.0}/README.md +0 -0
- {ags_cli-0.1.0 → ags_cli-0.2.0}/ags_cli.egg-info/SOURCES.txt +0 -0
- {ags_cli-0.1.0 → ags_cli-0.2.0}/ags_cli.egg-info/dependency_links.txt +0 -0
- {ags_cli-0.1.0 → ags_cli-0.2.0}/ags_cli.egg-info/entry_points.txt +0 -0
- {ags_cli-0.1.0 → ags_cli-0.2.0}/ags_cli.egg-info/requires.txt +0 -0
- {ags_cli-0.1.0 → ags_cli-0.2.0}/ags_cli.egg-info/top_level.txt +0 -0
- {ags_cli-0.1.0 → ags_cli-0.2.0}/cli/__init__.py +0 -0
- {ags_cli-0.1.0 → ags_cli-0.2.0}/cli/__main__.py +0 -0
- {ags_cli-0.1.0 → ags_cli-0.2.0}/cli/client.py +0 -0
- {ags_cli-0.1.0 → ags_cli-0.2.0}/cli/commands/__init__.py +0 -0
- {ags_cli-0.1.0 → ags_cli-0.2.0}/cli/commands/adapter.py +0 -0
- {ags_cli-0.1.0 → ags_cli-0.2.0}/cli/commands/artifact.py +0 -0
- {ags_cli-0.1.0 → ags_cli-0.2.0}/cli/commands/ask.py +0 -0
- {ags_cli-0.1.0 → ags_cli-0.2.0}/cli/commands/chat.py +0 -0
- {ags_cli-0.1.0 → ags_cli-0.2.0}/cli/commands/diff.py +0 -0
- {ags_cli-0.1.0 → ags_cli-0.2.0}/cli/commands/doctor.py +0 -0
- {ags_cli-0.1.0 → ags_cli-0.2.0}/cli/commands/gui.py +0 -0
- {ags_cli-0.1.0 → ags_cli-0.2.0}/cli/commands/init.py +0 -0
- {ags_cli-0.1.0 → ags_cli-0.2.0}/cli/commands/mcp.py +0 -0
- {ags_cli-0.1.0 → ags_cli-0.2.0}/cli/commands/memory.py +0 -0
- {ags_cli-0.1.0 → ags_cli-0.2.0}/cli/commands/model.py +0 -0
- {ags_cli-0.1.0 → ags_cli-0.2.0}/cli/commands/policy.py +0 -0
- {ags_cli-0.1.0 → ags_cli-0.2.0}/cli/commands/project.py +0 -0
- {ags_cli-0.1.0 → ags_cli-0.2.0}/cli/commands/quota.py +0 -0
- {ags_cli-0.1.0 → ags_cli-0.2.0}/cli/commands/run.py +0 -0
- {ags_cli-0.1.0 → ags_cli-0.2.0}/cli/commands/serve.py +0 -0
- {ags_cli-0.1.0 → ags_cli-0.2.0}/cli/commands/service.py +0 -0
- {ags_cli-0.1.0 → ags_cli-0.2.0}/cli/commands/session.py +0 -0
- {ags_cli-0.1.0 → ags_cli-0.2.0}/cli/commands/stats.py +0 -0
- {ags_cli-0.1.0 → ags_cli-0.2.0}/cli/commands/status.py +0 -0
- {ags_cli-0.1.0 → ags_cli-0.2.0}/cli/commands/task.py +0 -0
- {ags_cli-0.1.0 → ags_cli-0.2.0}/cli/commands/usage.py +0 -0
- {ags_cli-0.1.0 → ags_cli-0.2.0}/cli/local.py +0 -0
- {ags_cli-0.1.0 → ags_cli-0.2.0}/cli/main.py +0 -0
- {ags_cli-0.1.0 → ags_cli-0.2.0}/harness/__init__.py +0 -0
- {ags_cli-0.1.0 → ags_cli-0.2.0}/harness/adapters/__init__.py +0 -0
- {ags_cli-0.1.0 → ags_cli-0.2.0}/harness/adapters/local_openai.py +0 -0
- {ags_cli-0.1.0 → ags_cli-0.2.0}/harness/adapters/mock.py +0 -0
- {ags_cli-0.1.0 → ags_cli-0.2.0}/harness/adapters/registry.py +0 -0
- {ags_cli-0.1.0 → ags_cli-0.2.0}/harness/adapters/warm_pool.py +0 -0
- {ags_cli-0.1.0 → ags_cli-0.2.0}/harness/api/__init__.py +0 -0
- {ags_cli-0.1.0 → ags_cli-0.2.0}/harness/api/router.py +0 -0
- {ags_cli-0.1.0 → ags_cli-0.2.0}/harness/api/v1/__init__.py +0 -0
- {ags_cli-0.1.0 → ags_cli-0.2.0}/harness/api/v1/adapters.py +0 -0
- {ags_cli-0.1.0 → ags_cli-0.2.0}/harness/api/v1/approvals.py +0 -0
- {ags_cli-0.1.0 → ags_cli-0.2.0}/harness/api/v1/artifacts.py +0 -0
- {ags_cli-0.1.0 → ags_cli-0.2.0}/harness/api/v1/attachments.py +0 -0
- {ags_cli-0.1.0 → ags_cli-0.2.0}/harness/api/v1/doctor.py +0 -0
- {ags_cli-0.1.0 → ags_cli-0.2.0}/harness/api/v1/health.py +0 -0
- {ags_cli-0.1.0 → ags_cli-0.2.0}/harness/api/v1/mcp.py +0 -0
- {ags_cli-0.1.0 → ags_cli-0.2.0}/harness/api/v1/memory.py +0 -0
- {ags_cli-0.1.0 → ags_cli-0.2.0}/harness/api/v1/policies.py +0 -0
- {ags_cli-0.1.0 → ags_cli-0.2.0}/harness/api/v1/project.py +0 -0
- {ags_cli-0.1.0 → ags_cli-0.2.0}/harness/api/v1/runs.py +0 -0
- {ags_cli-0.1.0 → ags_cli-0.2.0}/harness/api/v1/sessions.py +0 -0
- {ags_cli-0.1.0 → ags_cli-0.2.0}/harness/api/v1/stream.py +0 -0
- {ags_cli-0.1.0 → ags_cli-0.2.0}/harness/api/v1/tasks.py +0 -0
- {ags_cli-0.1.0 → ags_cli-0.2.0}/harness/api/v1/usage.py +0 -0
- {ags_cli-0.1.0 → ags_cli-0.2.0}/harness/bootstrap.py +0 -0
- {ags_cli-0.1.0 → ags_cli-0.2.0}/harness/db.py +0 -0
- {ags_cli-0.1.0 → ags_cli-0.2.0}/harness/deps.py +0 -0
- {ags_cli-0.1.0 → ags_cli-0.2.0}/harness/eventbus/__init__.py +0 -0
- {ags_cli-0.1.0 → ags_cli-0.2.0}/harness/eventbus/inprocess_bus.py +0 -0
- {ags_cli-0.1.0 → ags_cli-0.2.0}/harness/main.py +0 -0
- {ags_cli-0.1.0 → ags_cli-0.2.0}/harness/mcp/__init__.py +0 -0
- {ags_cli-0.1.0 → ags_cli-0.2.0}/harness/mcp/client.py +0 -0
- {ags_cli-0.1.0 → ags_cli-0.2.0}/harness/mcp/config_gen.py +0 -0
- {ags_cli-0.1.0 → ags_cli-0.2.0}/harness/mcp/registry.py +0 -0
- {ags_cli-0.1.0 → ags_cli-0.2.0}/harness/mcp/tools.py +0 -0
- {ags_cli-0.1.0 → ags_cli-0.2.0}/harness/memory/__init__.py +0 -0
- {ags_cli-0.1.0 → ags_cli-0.2.0}/harness/memory/embed.py +0 -0
- {ags_cli-0.1.0 → ags_cli-0.2.0}/harness/memory/extract.py +0 -0
- {ags_cli-0.1.0 → ags_cli-0.2.0}/harness/memory/ingest.py +0 -0
- {ags_cli-0.1.0 → ags_cli-0.2.0}/harness/memory/namespaces.py +0 -0
- {ags_cli-0.1.0 → ags_cli-0.2.0}/harness/memory/ranking.py +0 -0
- {ags_cli-0.1.0 → ags_cli-0.2.0}/harness/memory/redact.py +0 -0
- {ags_cli-0.1.0 → ags_cli-0.2.0}/harness/memory/service.py +0 -0
- {ags_cli-0.1.0 → ags_cli-0.2.0}/harness/memory/sqlite_vec_backend.py +0 -0
- {ags_cli-0.1.0 → ags_cli-0.2.0}/harness/memory/summarize.py +0 -0
- {ags_cli-0.1.0 → ags_cli-0.2.0}/harness/models/__init__.py +0 -0
- {ags_cli-0.1.0 → ags_cli-0.2.0}/harness/models/adapter_health.py +0 -0
- {ags_cli-0.1.0 → ags_cli-0.2.0}/harness/models/approval.py +0 -0
- {ags_cli-0.1.0 → ags_cli-0.2.0}/harness/models/artifact.py +0 -0
- {ags_cli-0.1.0 → ags_cli-0.2.0}/harness/models/audit_log.py +0 -0
- {ags_cli-0.1.0 → ags_cli-0.2.0}/harness/models/comparison.py +0 -0
- {ags_cli-0.1.0 → ags_cli-0.2.0}/harness/models/enums.py +0 -0
- {ags_cli-0.1.0 → ags_cli-0.2.0}/harness/models/mcp_server_config.py +0 -0
- {ags_cli-0.1.0 → ags_cli-0.2.0}/harness/models/memory.py +0 -0
- {ags_cli-0.1.0 → ags_cli-0.2.0}/harness/models/policy.py +0 -0
- {ags_cli-0.1.0 → ags_cli-0.2.0}/harness/models/provider_config.py +0 -0
- {ags_cli-0.1.0 → ags_cli-0.2.0}/harness/models/run.py +0 -0
- {ags_cli-0.1.0 → ags_cli-0.2.0}/harness/models/run_event.py +0 -0
- {ags_cli-0.1.0 → ags_cli-0.2.0}/harness/models/session.py +0 -0
- {ags_cli-0.1.0 → ags_cli-0.2.0}/harness/models/task.py +0 -0
- {ags_cli-0.1.0 → ags_cli-0.2.0}/harness/models/types.py +0 -0
- {ags_cli-0.1.0 → ags_cli-0.2.0}/harness/models/workspace.py +0 -0
- {ags_cli-0.1.0 → ags_cli-0.2.0}/harness/observability/__init__.py +0 -0
- {ags_cli-0.1.0 → ags_cli-0.2.0}/harness/observability/audit.py +0 -0
- {ags_cli-0.1.0 → ags_cli-0.2.0}/harness/observability/logging.py +0 -0
- {ags_cli-0.1.0 → ags_cli-0.2.0}/harness/observability/metrics.py +0 -0
- {ags_cli-0.1.0 → ags_cli-0.2.0}/harness/observability/tracing.py +0 -0
- {ags_cli-0.1.0 → ags_cli-0.2.0}/harness/orchestrator/__init__.py +0 -0
- {ags_cli-0.1.0 → ags_cli-0.2.0}/harness/orchestrator/engine.py +0 -0
- {ags_cli-0.1.0 → ags_cli-0.2.0}/harness/orchestrator/handoff.py +0 -0
- {ags_cli-0.1.0 → ags_cli-0.2.0}/harness/orchestrator/lifecycle.py +0 -0
- {ags_cli-0.1.0 → ags_cli-0.2.0}/harness/orchestrator/native_resume.py +0 -0
- {ags_cli-0.1.0 → ags_cli-0.2.0}/harness/orchestrator/policies.py +0 -0
- {ags_cli-0.1.0 → ags_cli-0.2.0}/harness/orchestrator/reconcile.py +0 -0
- {ags_cli-0.1.0 → ags_cli-0.2.0}/harness/orchestrator/run_graph.py +0 -0
- {ags_cli-0.1.0 → ags_cli-0.2.0}/harness/orchestrator/snapshot.py +0 -0
- {ags_cli-0.1.0 → ags_cli-0.2.0}/harness/schemas/__init__.py +0 -0
- {ags_cli-0.1.0 → ags_cli-0.2.0}/harness/schemas/adapter.py +0 -0
- {ags_cli-0.1.0 → ags_cli-0.2.0}/harness/schemas/approval.py +0 -0
- {ags_cli-0.1.0 → ags_cli-0.2.0}/harness/schemas/artifact.py +0 -0
- {ags_cli-0.1.0 → ags_cli-0.2.0}/harness/schemas/common.py +0 -0
- {ags_cli-0.1.0 → ags_cli-0.2.0}/harness/schemas/memory.py +0 -0
- {ags_cli-0.1.0 → ags_cli-0.2.0}/harness/schemas/policy.py +0 -0
- {ags_cli-0.1.0 → ags_cli-0.2.0}/harness/schemas/run.py +0 -0
- {ags_cli-0.1.0 → ags_cli-0.2.0}/harness/schemas/session.py +0 -0
- {ags_cli-0.1.0 → ags_cli-0.2.0}/harness/schemas/task.py +0 -0
- {ags_cli-0.1.0 → ags_cli-0.2.0}/harness/security/__init__.py +0 -0
- {ags_cli-0.1.0 → ags_cli-0.2.0}/harness/security/approval_policy.py +0 -0
- {ags_cli-0.1.0 → ags_cli-0.2.0}/harness/security/auth.py +0 -0
- {ags_cli-0.1.0 → ags_cli-0.2.0}/harness/security/permissions.py +0 -0
- {ags_cli-0.1.0 → ags_cli-0.2.0}/harness/security/risk.py +0 -0
- {ags_cli-0.1.0 → ags_cli-0.2.0}/harness/security/secrets.py +0 -0
- {ags_cli-0.1.0 → ags_cli-0.2.0}/harness/services/__init__.py +0 -0
- {ags_cli-0.1.0 → ags_cli-0.2.0}/harness/services/adapters_health.py +0 -0
- {ags_cli-0.1.0 → ags_cli-0.2.0}/harness/services/approvals.py +0 -0
- {ags_cli-0.1.0 → ags_cli-0.2.0}/harness/services/artifacts.py +0 -0
- {ags_cli-0.1.0 → ags_cli-0.2.0}/harness/services/attachments.py +0 -0
- {ags_cli-0.1.0 → ags_cli-0.2.0}/harness/services/comparison.py +0 -0
- {ags_cli-0.1.0 → ags_cli-0.2.0}/harness/services/doctor.py +0 -0
- {ags_cli-0.1.0 → ags_cli-0.2.0}/harness/services/files.py +0 -0
- {ags_cli-0.1.0 → ags_cli-0.2.0}/harness/services/mcp_servers.py +0 -0
- {ags_cli-0.1.0 → ags_cli-0.2.0}/harness/services/project_git.py +0 -0
- {ags_cli-0.1.0 → ags_cli-0.2.0}/harness/services/retention.py +0 -0
- {ags_cli-0.1.0 → ags_cli-0.2.0}/harness/services/runs.py +0 -0
- {ags_cli-0.1.0 → ags_cli-0.2.0}/harness/services/runs_diff.py +0 -0
- {ags_cli-0.1.0 → ags_cli-0.2.0}/harness/services/sessions.py +0 -0
- {ags_cli-0.1.0 → ags_cli-0.2.0}/harness/services/ssh.py +0 -0
- {ags_cli-0.1.0 → ags_cli-0.2.0}/harness/services/stats.py +0 -0
- {ags_cli-0.1.0 → ags_cli-0.2.0}/harness/services/tasks.py +0 -0
- {ags_cli-0.1.0 → ags_cli-0.2.0}/harness/services/workspaces.py +0 -0
- {ags_cli-0.1.0 → ags_cli-0.2.0}/harness/services/worktrees.py +0 -0
- {ags_cli-0.1.0 → ags_cli-0.2.0}/harness/settings.py +0 -0
- {ags_cli-0.1.0 → ags_cli-0.2.0}/harness/skills/__init__.py +0 -0
- {ags_cli-0.1.0 → ags_cli-0.2.0}/harness/skills/manager.py +0 -0
- {ags_cli-0.1.0 → ags_cli-0.2.0}/harness/tools/__init__.py +0 -0
- {ags_cli-0.1.0 → ags_cli-0.2.0}/harness/tools/fs_tools.py +0 -0
- {ags_cli-0.1.0 → ags_cli-0.2.0}/harness/workers/__init__.py +0 -0
- {ags_cli-0.1.0 → ags_cli-0.2.0}/harness/workers/dispatch.py +0 -0
- {ags_cli-0.1.0 → ags_cli-0.2.0}/harness/workers/inprocess.py +0 -0
- {ags_cli-0.1.0 → ags_cli-0.2.0}/setup.cfg +0 -0
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
"""AntigravityCliAdapter — drives Google's official ``agy`` CLI.
|
|
2
|
+
|
|
3
|
+
Empirically verified CLI contract (agy 1.1.27, run live on this machine):
|
|
4
|
+
|
|
5
|
+
1. **``--output-format stream-json``** — emits NDJSON on stdout, one object per
|
|
6
|
+
line, with three top-level shapes (``init``, ``step_update``, ``result``). This
|
|
7
|
+
replaces the old plain-text scraping: we now get tool calls, per-step usage,
|
|
8
|
+
the conversation id, and the terminal status/denials as structured data.
|
|
9
|
+
2. **Piped stdout works.** The old non-TTY suppression bug (agy emitting 0 bytes
|
|
10
|
+
when stdout was not a terminal) is fixed upstream, so we run on plain pipes —
|
|
11
|
+
no PTY, and stderr stays separately captured (under a PTY it was merged into
|
|
12
|
+
the stream, which is how silent failures used to go unnoticed).
|
|
13
|
+
3. **The prompt goes on stdin.** With no ``-p``/``--prompt`` flag, agy reads the
|
|
14
|
+
prompt from stdin until EOF. That sidesteps every argv length limit (Windows
|
|
15
|
+
``CreateProcess`` 32 KiB / ``cmd.exe`` 8 KiB) and the Go-style flag-parsing
|
|
16
|
+
hazard where a value-taking flag swallows the next token.
|
|
17
|
+
4. **``--mode plan|accept-edits``** — the permission model. ``plan`` is a real
|
|
18
|
+
read-only boundary (verified: an explicit "create WROTE.txt" instruction left
|
|
19
|
+
no file behind). It must be combined with ``--dangerously-skip-permissions``:
|
|
20
|
+
headless runs cannot prompt, so without it agy auto-denies the first tool that
|
|
21
|
+
needs a permission and terminates with ``status: "CANCELED"`` and an empty
|
|
22
|
+
response — a silent no-op run.
|
|
23
|
+
5. **``--conversation <id>``** — resumes a previous conversation by id. The id is
|
|
24
|
+
inline in every stream-json event, so the old ``--log-file`` glog scraping is
|
|
25
|
+
gone.
|
|
26
|
+
6. **No ``--mcp-config``** — probe gating handles that (an ``agy mcp`` subcommand
|
|
27
|
+
exists but takes no run-scoped config file).
|
|
28
|
+
|
|
29
|
+
Invocation (auth via the CLI's own login or an injected ``ANTIGRAVITY_API_KEY`` /
|
|
30
|
+
``GEMINI_API_KEY`` from a ``secret_ref`` — never token hijacking)::
|
|
31
|
+
|
|
32
|
+
agy --output-format stream-json --model <model> --add-dir <cwd> \
|
|
33
|
+
--print-timeout <N>s --mode plan|accept-edits \
|
|
34
|
+
--dangerously-skip-permissions [--sandbox] [--conversation <id>]
|
|
35
|
+
# prompt written to stdin, then closed
|
|
36
|
+
|
|
37
|
+
The adapter is the abstract "official Google CLI" slot: the binary and flags are
|
|
38
|
+
config-driven (``params.bin`` / ``params.cmd``), so a future successor swaps in via
|
|
39
|
+
config without code changes.
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
from __future__ import annotations
|
|
43
|
+
|
|
44
|
+
import contextlib
|
|
45
|
+
import json
|
|
46
|
+
import logging
|
|
47
|
+
import shutil
|
|
48
|
+
import tempfile
|
|
49
|
+
from collections.abc import Iterable
|
|
50
|
+
|
|
51
|
+
from harness_sdk.base import Capabilities, SessionContext
|
|
52
|
+
from harness_sdk.cli_base import CliSubprocessAdapter, ParserState
|
|
53
|
+
from harness_sdk.events import (
|
|
54
|
+
AdapterMetaEvent,
|
|
55
|
+
CostEvent,
|
|
56
|
+
ErrorEvent,
|
|
57
|
+
NormalizedEvent,
|
|
58
|
+
ToolCallEvent,
|
|
59
|
+
ToolResultEvent,
|
|
60
|
+
TokenEvent,
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
from harness.adapters.probe import probe_cli, supports_flag
|
|
64
|
+
from harness.models.enums import AdapterKind
|
|
65
|
+
|
|
66
|
+
log = logging.getLogger(__name__)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class AntigravityCliAdapter(CliSubprocessAdapter):
|
|
70
|
+
kind = AdapterKind.antigravity
|
|
71
|
+
# agy streams stream-json fine on a plain pipe as of 1.1.x; a PTY would merge
|
|
72
|
+
# stderr into stdout and hide failure detail. Overridable via params.pty.
|
|
73
|
+
default_use_pty = False
|
|
74
|
+
|
|
75
|
+
def __init__(self, config) -> None: # noqa: ANN001 - AdapterConfig
|
|
76
|
+
super().__init__(config)
|
|
77
|
+
self._mcp_dirs: dict[str, str] = {}
|
|
78
|
+
self._probe: dict | None = None
|
|
79
|
+
|
|
80
|
+
async def ensure_probe(self) -> dict:
|
|
81
|
+
"""Run ``probe_cli`` once, cache the result on the instance, and return it.
|
|
82
|
+
|
|
83
|
+
On any failure returns a safe fallback ``{"ok": False, "flags": set(), "version": None}``
|
|
84
|
+
so callers never need to guard against exceptions from this method.
|
|
85
|
+
"""
|
|
86
|
+
if self._probe is not None:
|
|
87
|
+
return self._probe
|
|
88
|
+
try:
|
|
89
|
+
result = await probe_cli(self.binary)
|
|
90
|
+
except Exception:
|
|
91
|
+
return {"ok": False, "flags": set(), "version": None}
|
|
92
|
+
self._probe = result
|
|
93
|
+
return self._probe
|
|
94
|
+
|
|
95
|
+
@property
|
|
96
|
+
def binary(self) -> str:
|
|
97
|
+
return self.config.params.get("bin") or self.config.params.get("cmd") or "agy"
|
|
98
|
+
|
|
99
|
+
def env_overrides(self) -> dict[str, str]:
|
|
100
|
+
env: dict[str, str] = {}
|
|
101
|
+
if self.config.api_key:
|
|
102
|
+
# agy accepts either; set both so it works regardless of which it reads.
|
|
103
|
+
env["ANTIGRAVITY_API_KEY"] = self.config.api_key
|
|
104
|
+
env["GEMINI_API_KEY"] = self.config.api_key
|
|
105
|
+
# Opt-in managed-skill home. agy's config-dir env var must be verified against
|
|
106
|
+
# the pinned CLI, so we only set it when explicitly named via params.config_dir_env.
|
|
107
|
+
from harness.skills import provision_config_home
|
|
108
|
+
if (envname := self.config.params.get("config_dir_env")) and (
|
|
109
|
+
home := provision_config_home(self.config.name)
|
|
110
|
+
):
|
|
111
|
+
env[envname] = home
|
|
112
|
+
return env
|
|
113
|
+
|
|
114
|
+
def health_argv(self) -> list[str]:
|
|
115
|
+
return [self.binary, "--version"]
|
|
116
|
+
|
|
117
|
+
async def list_models(self) -> list[str]:
|
|
118
|
+
"""``agy models`` prints a status line, then one TAB-separated
|
|
119
|
+
``<id>\\t<display name>`` row per model. Verified against v1.1.27::
|
|
120
|
+
|
|
121
|
+
Fetching available models...
|
|
122
|
+
gemini-3.1-pro-high\\tGemini 3.1 Pro (High)
|
|
123
|
+
|
|
124
|
+
We keep the id (field 0) and drop any line without a TAB — that filters the
|
|
125
|
+
status line and any future chatter without hard-coding its wording.
|
|
126
|
+
"""
|
|
127
|
+
import asyncio
|
|
128
|
+
|
|
129
|
+
try:
|
|
130
|
+
proc = await asyncio.create_subprocess_exec(
|
|
131
|
+
self.binary, "models",
|
|
132
|
+
stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.DEVNULL,
|
|
133
|
+
stdin=asyncio.subprocess.DEVNULL,
|
|
134
|
+
)
|
|
135
|
+
out, _ = await asyncio.wait_for(proc.communicate(), timeout=20)
|
|
136
|
+
except (FileNotFoundError, TimeoutError, OSError):
|
|
137
|
+
return [self.config.model] if self.config.model else []
|
|
138
|
+
models = [
|
|
139
|
+
ln.split("\t", 1)[0].strip()
|
|
140
|
+
for ln in out.decode(errors="replace").splitlines()
|
|
141
|
+
if "\t" in ln and ln.split("\t", 1)[0].strip()
|
|
142
|
+
]
|
|
143
|
+
return models or ([self.config.model] if self.config.model else [])
|
|
144
|
+
|
|
145
|
+
def build_argv(self, *, run_id: str, ctx: SessionContext, message: str) -> list[str]:
|
|
146
|
+
p = self.config.params
|
|
147
|
+
argv = [self.binary, "--output-format", "stream-json"]
|
|
148
|
+
if self.config.model:
|
|
149
|
+
argv += ["--model", self.config.model]
|
|
150
|
+
if (cwd := self.working_dir(ctx)):
|
|
151
|
+
argv += ["--add-dir", cwd]
|
|
152
|
+
argv += ["--print-timeout", f"{int(self.timeout_s)}s"]
|
|
153
|
+
if (conv := ctx.extra.get("conversation_id")):
|
|
154
|
+
argv += ["--conversation", str(conv)]
|
|
155
|
+
# Safe-by-default: no unattended edits. Write mode (the per-run workspace
|
|
156
|
+
# mode on the context) lets agy apply changes, in its own sandbox.
|
|
157
|
+
write = not ctx.read_only
|
|
158
|
+
argv += ["--mode", "accept-edits" if write else "plan"]
|
|
159
|
+
# Required on BOTH paths: headless runs cannot answer a permission prompt,
|
|
160
|
+
# so without this agy auto-denies the first gated tool and the run ends
|
|
161
|
+
# CANCELED with an empty response. `--mode plan` remains the write barrier.
|
|
162
|
+
argv.append("--dangerously-skip-permissions")
|
|
163
|
+
if p.get("sandbox", write):
|
|
164
|
+
argv.append("--sandbox")
|
|
165
|
+
argv += self._mcp_argv(run_id, ctx)
|
|
166
|
+
return argv
|
|
167
|
+
|
|
168
|
+
def stdin_payload(self, message: str, ctx: SessionContext | None = None) -> bytes | None:
|
|
169
|
+
"""The prompt goes on stdin (no ``-p``), which agy reads to EOF. System and
|
|
170
|
+
memory context are prepended — agy has no system-prompt flag."""
|
|
171
|
+
system = ""
|
|
172
|
+
if ctx is not None:
|
|
173
|
+
system = "\n\n".join(x for x in (ctx.system_prompt, ctx.memory_context) if x)
|
|
174
|
+
prompt = f"{system}\n\n---\n\n{message}" if system else message
|
|
175
|
+
return prompt.encode("utf-8")
|
|
176
|
+
|
|
177
|
+
def _mcp_argv(self, run_id: str, ctx: SessionContext) -> list[str]:
|
|
178
|
+
"""agy is its own MCP client: write the harness-resolved servers to a run-scoped
|
|
179
|
+
config and point agy at it.
|
|
180
|
+
|
|
181
|
+
The flag used (``--mcp-config`` by default) is verified against the probed CLI
|
|
182
|
+
capability set. If the probe is available and the flag is not supported, MCP
|
|
183
|
+
servers are skipped (returning ``[]``) and a one-time warning is logged.
|
|
184
|
+
|
|
185
|
+
Manual override: setting ``params.mcp_config_flag`` explicitly bypasses probe
|
|
186
|
+
gating — the user-supplied flag is always passed, letting operators work with
|
|
187
|
+
pre-release CLIs or renamed flags without a code change."""
|
|
188
|
+
from harness.mcp import config_gen
|
|
189
|
+
|
|
190
|
+
servers = ctx.extra.get("mcp_servers") or []
|
|
191
|
+
if not servers:
|
|
192
|
+
return []
|
|
193
|
+
|
|
194
|
+
# If the user set an explicit flag, it bypasses probe gating (manual override wins).
|
|
195
|
+
explicit_flag = self.config.params.get("mcp_config_flag")
|
|
196
|
+
flag = explicit_flag or "--mcp-config"
|
|
197
|
+
|
|
198
|
+
# Probe gating: only applies when no explicit override AND a probe result is cached.
|
|
199
|
+
if not explicit_flag and self._probe is not None:
|
|
200
|
+
if not supports_flag(self._probe, flag):
|
|
201
|
+
log.warning(
|
|
202
|
+
"mcp_flag_not_supported_by_cli",
|
|
203
|
+
extra={"flag": flag, "binary": self.binary},
|
|
204
|
+
)
|
|
205
|
+
log.warning(
|
|
206
|
+
"Skipping MCP servers: flag %r not found in %r help output; "
|
|
207
|
+
"set params.mcp_config_flag to override",
|
|
208
|
+
flag, self.binary,
|
|
209
|
+
)
|
|
210
|
+
return []
|
|
211
|
+
|
|
212
|
+
d = tempfile.mkdtemp(prefix=f"ags-mcp-{run_id[:8]}-")
|
|
213
|
+
self._mcp_dirs[run_id] = d
|
|
214
|
+
path = config_gen.write_json(
|
|
215
|
+
config_gen.antigravity_mcp_config(servers), d, "mcp.json"
|
|
216
|
+
)
|
|
217
|
+
return [flag, path]
|
|
218
|
+
|
|
219
|
+
async def _reap(self, proc, run_id: str) -> None: # noqa: ANN001
|
|
220
|
+
if (d := self._mcp_dirs.pop(run_id, None)):
|
|
221
|
+
with contextlib.suppress(Exception):
|
|
222
|
+
shutil.rmtree(d, ignore_errors=True)
|
|
223
|
+
await super()._reap(proc, run_id)
|
|
224
|
+
|
|
225
|
+
# ── stream-json parsing ───────────────────────────────────────────────────
|
|
226
|
+
def parse_chunk(self, line: str, state: ParserState) -> Iterable[NormalizedEvent]:
|
|
227
|
+
line = line.strip()
|
|
228
|
+
# agy interleaves plain-text progress/diagnostic lines (`jetski: …`) with the
|
|
229
|
+
# NDJSON stream; they are not part of the answer.
|
|
230
|
+
if not line.startswith("{"):
|
|
231
|
+
return
|
|
232
|
+
try:
|
|
233
|
+
obj = json.loads(line)
|
|
234
|
+
except json.JSONDecodeError:
|
|
235
|
+
return
|
|
236
|
+
event = obj.get("event")
|
|
237
|
+
if event == "init":
|
|
238
|
+
yield from self._conv_meta(obj.get("conversation_id")
|
|
239
|
+
or (obj.get("init") or {}).get("conversation_id"), state)
|
|
240
|
+
elif event == "step_update":
|
|
241
|
+
yield from self._parse_step(obj.get("step_update") or {}, state)
|
|
242
|
+
elif event == "result":
|
|
243
|
+
yield from self._parse_result(obj.get("result") or {}, state)
|
|
244
|
+
|
|
245
|
+
def _conv_meta(self, conv_id, state: ParserState) -> Iterable[NormalizedEvent]: # noqa: ANN001
|
|
246
|
+
"""Emit the conversation id once per run so the engine can `--conversation`
|
|
247
|
+
resume the next turn."""
|
|
248
|
+
if conv_id and state.get("conversation_id") != conv_id:
|
|
249
|
+
state["conversation_id"] = conv_id
|
|
250
|
+
yield AdapterMetaEvent(seq=0, data={"conversation_id": str(conv_id)})
|
|
251
|
+
|
|
252
|
+
def _parse_step(self, su: dict, state: ParserState) -> Iterable[NormalizedEvent]:
|
|
253
|
+
yield from self._conv_meta(su.get("conversation_id"), state)
|
|
254
|
+
step_type = su.get("step_type")
|
|
255
|
+
step_state = su.get("state")
|
|
256
|
+
|
|
257
|
+
if step_type == "agent_response":
|
|
258
|
+
# `text_delta` is a true incremental delta and is absent on thinking /
|
|
259
|
+
# tool-planning steps, which carry only `usage`.
|
|
260
|
+
if (delta := su.get("text_delta")):
|
|
261
|
+
state.setdefault("text", []).append(delta)
|
|
262
|
+
yield TokenEvent(seq=0, text=delta)
|
|
263
|
+
elif step_type == "tool":
|
|
264
|
+
yield from self._parse_tool_step(su, state, step_state)
|
|
265
|
+
|
|
266
|
+
if (usage := su.get("usage")):
|
|
267
|
+
state["usage"] = usage
|
|
268
|
+
|
|
269
|
+
def _parse_tool_step(self, su: dict, state: ParserState,
|
|
270
|
+
step_state: str | None) -> Iterable[NormalizedEvent]:
|
|
271
|
+
info = su.get("tool_info") or {}
|
|
272
|
+
name = su.get("tool_name") or info.get("name") or "tool"
|
|
273
|
+
call_id = f"step-{su.get('step_index')}"
|
|
274
|
+
seen: set[str] = state.setdefault("tool_calls", set())
|
|
275
|
+
# ACTIVE announces the call; DONE/ERROR carries the outcome. A step can
|
|
276
|
+
# arrive already DONE (fast tools), so emit the call lazily either way.
|
|
277
|
+
if call_id not in seen:
|
|
278
|
+
seen.add(call_id)
|
|
279
|
+
yield ToolCallEvent(seq=0, name=name,
|
|
280
|
+
arguments=info.get("parameters") or {}, call_id=call_id)
|
|
281
|
+
if step_state in ("DONE", "ERROR"):
|
|
282
|
+
err = info.get("error") or {}
|
|
283
|
+
output = err.get("message") if err else info.get("output")
|
|
284
|
+
yield ToolResultEvent(seq=0, call_id=call_id, output=str(output or ""),
|
|
285
|
+
is_error=bool(err) or step_state == "ERROR")
|
|
286
|
+
|
|
287
|
+
def _parse_result(self, res: dict, state: ParserState) -> Iterable[NormalizedEvent]:
|
|
288
|
+
yield from self._conv_meta(res.get("conversation_id"), state)
|
|
289
|
+
# `response` is the full consolidated answer — authoritative over the deltas.
|
|
290
|
+
if (response := res.get("response")):
|
|
291
|
+
state["final_text"] = response
|
|
292
|
+
status = str(res.get("status") or "").upper()
|
|
293
|
+
state["result_status"] = status
|
|
294
|
+
|
|
295
|
+
usage = res.get("usage") or state.get("usage") or {}
|
|
296
|
+
if usage:
|
|
297
|
+
prompt_tokens = int(usage.get("input_tokens") or 0)
|
|
298
|
+
completion_tokens = int(usage.get("output_tokens") or 0)
|
|
299
|
+
yield CostEvent(
|
|
300
|
+
seq=0,
|
|
301
|
+
prompt_tokens=prompt_tokens,
|
|
302
|
+
completion_tokens=completion_tokens,
|
|
303
|
+
# agy's own `total_tokens` excludes cache reads; keep its arithmetic.
|
|
304
|
+
total_tokens=int(usage.get("total_tokens")
|
|
305
|
+
or prompt_tokens + completion_tokens),
|
|
306
|
+
usd=None, # agy reports no cost; estimate_cost fills it in downstream
|
|
307
|
+
)
|
|
308
|
+
|
|
309
|
+
if status and status != "SUCCESS":
|
|
310
|
+
denied = [d.get("display_name") or d.get("action")
|
|
311
|
+
for d in (res.get("denied_actions") or []) if isinstance(d, dict)]
|
|
312
|
+
msg = f"agy run ended with status {status}"
|
|
313
|
+
if denied:
|
|
314
|
+
msg += f"; denied actions: {', '.join(str(d) for d in denied)}"
|
|
315
|
+
yield ErrorEvent(seq=0, message=msg, retriable=False)
|
|
316
|
+
|
|
317
|
+
def finalize(self, state: ParserState) -> Iterable[NormalizedEvent]:
|
|
318
|
+
# Consolidate into one canonical assistant message, preferring the result
|
|
319
|
+
# event's full response over the concatenated deltas.
|
|
320
|
+
if (final := state.get("final_text")):
|
|
321
|
+
state["text"] = [final]
|
|
322
|
+
yield from self._final_text_message(state)
|
|
323
|
+
# agy exits 0 even for a CANCELED run, so the base class's returncode check
|
|
324
|
+
# would report "succeeded". Reflect the CLI's own terminal status instead.
|
|
325
|
+
if state.get("result_status") not in (None, "", "SUCCESS"):
|
|
326
|
+
if state.get("returncode") in (0, None):
|
|
327
|
+
state["returncode"] = 1
|
|
328
|
+
|
|
329
|
+
def list_capabilities(self) -> Capabilities:
|
|
330
|
+
return Capabilities(
|
|
331
|
+
streaming=True,
|
|
332
|
+
tool_calling=bool(self.config.capabilities.get("tool_calling", True)),
|
|
333
|
+
max_context=int(self.config.params.get("max_context", 1_000_000)),
|
|
334
|
+
# agy reports token usage but no price, and we carry no Gemini price
|
|
335
|
+
# table — so token counts are emitted, USD is not.
|
|
336
|
+
reasoning=True, cost_known=False,
|
|
337
|
+
notes=f"Official Google CLI '{self.binary}' (stream-json, token usage).",
|
|
338
|
+
)
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
"""ClaudeCodeAdapter — drives the official ``claude`` CLI (verified
|
|
2
|
-
v2.1.
|
|
1
|
+
"""ClaudeCodeAdapter — drives the official ``claude`` CLI (flag set re-verified
|
|
2
|
+
against v2.1.261).
|
|
3
3
|
|
|
4
4
|
Invocation (no token hijacking — the CLI uses its own supported auth; an optional
|
|
5
5
|
``ANTHROPIC_API_KEY`` is injected from a ``secret_ref``):
|
|
@@ -42,6 +42,7 @@ from __future__ import annotations
|
|
|
42
42
|
import contextlib
|
|
43
43
|
import itertools
|
|
44
44
|
import json
|
|
45
|
+
import re
|
|
45
46
|
import shutil
|
|
46
47
|
import tempfile
|
|
47
48
|
from collections.abc import AsyncIterator, Iterable
|
|
@@ -131,11 +132,10 @@ class ClaudeCodeAdapter(CliSubprocessAdapter):
|
|
|
131
132
|
if configured:
|
|
132
133
|
return list(configured)
|
|
133
134
|
return [
|
|
134
|
-
"claude-
|
|
135
|
+
"claude-opus-5",
|
|
136
|
+
"claude-fable-5-1",
|
|
135
137
|
"claude-opus-4-8",
|
|
136
|
-
"claude-opus-4-7",
|
|
137
138
|
"claude-sonnet-5",
|
|
138
|
-
"claude-sonnet-4-6",
|
|
139
139
|
"claude-haiku-4-5",
|
|
140
140
|
]
|
|
141
141
|
|
|
@@ -445,7 +445,12 @@ class ClaudeCodeAdapter(CliSubprocessAdapter):
|
|
|
445
445
|
try:
|
|
446
446
|
obj = json.loads(line)
|
|
447
447
|
except json.JSONDecodeError:
|
|
448
|
-
|
|
448
|
+
# The CLI interleaves plain-text diagnostics with the NDJSON stream, e.g.
|
|
449
|
+
# [claude-code:unrecognized_model] {"model":"…","query_source":"sdk"}
|
|
450
|
+
# Dropping them silently makes a bad `model:` in config look like a
|
|
451
|
+
# normal (empty) answer, so surface them instead of returning.
|
|
452
|
+
yield from _diagnostic_events(line)
|
|
453
|
+
return
|
|
449
454
|
t = obj.get("type")
|
|
450
455
|
|
|
451
456
|
if t == "system":
|
|
@@ -512,6 +517,41 @@ class ClaudeCodeAdapter(CliSubprocessAdapter):
|
|
|
512
517
|
)
|
|
513
518
|
|
|
514
519
|
|
|
520
|
+
_DIAGNOSTIC_RE = re.compile(r"^\[claude-code:([a-z_]+)\]\s*(.*)$")
|
|
521
|
+
|
|
522
|
+
# Diagnostics that mean the run cannot succeed, mapped to the operator-facing text.
|
|
523
|
+
_FATAL_DIAGNOSTICS = {
|
|
524
|
+
"unrecognized_model": (
|
|
525
|
+
"claude does not recognize the configured model {model!r} — it may not exist "
|
|
526
|
+
"or this account may not have access. Update the provider's `model:` "
|
|
527
|
+
"(or params.models) in config.yaml."
|
|
528
|
+
),
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
|
|
532
|
+
def _diagnostic_events(line: str) -> Iterable[NormalizedEvent]:
|
|
533
|
+
"""Turn a plain-text ``[claude-code:<kind>] {json}`` diagnostic into an event.
|
|
534
|
+
|
|
535
|
+
Fatal kinds become an ``ErrorEvent`` so a misconfigured model surfaces as a
|
|
536
|
+
failure rather than an empty run; anything else is passed through as adapter
|
|
537
|
+
metadata. Unrecognized noise is dropped."""
|
|
538
|
+
m = _DIAGNOSTIC_RE.match(line)
|
|
539
|
+
if not m:
|
|
540
|
+
return
|
|
541
|
+
kind, payload = m.group(1), m.group(2).strip()
|
|
542
|
+
try:
|
|
543
|
+
data = json.loads(payload) if payload else {}
|
|
544
|
+
except json.JSONDecodeError:
|
|
545
|
+
data = {"detail": payload}
|
|
546
|
+
if not isinstance(data, dict):
|
|
547
|
+
data = {"detail": data}
|
|
548
|
+
if (template := _FATAL_DIAGNOSTICS.get(kind)):
|
|
549
|
+
yield ErrorEvent(seq=0, message=template.format(model=data.get("model")),
|
|
550
|
+
retriable=False)
|
|
551
|
+
else:
|
|
552
|
+
yield AdapterMetaEvent(seq=0, data={"diagnostic": kind, **data})
|
|
553
|
+
|
|
554
|
+
|
|
515
555
|
def _stringify(content: object) -> str:
|
|
516
556
|
if isinstance(content, str):
|
|
517
557
|
return content
|
|
@@ -8,7 +8,15 @@ from pathlib import Path
|
|
|
8
8
|
|
|
9
9
|
from harness_sdk.cli_base import resolve_binary
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
# Flags are case-SENSITIVE and camelCase is real: `claude --help` advertises
|
|
12
|
+
# `--allowedTools` alongside `--allowed-tools`. A lowercase-only class silently
|
|
13
|
+
# truncated it to `--allowed`, so `supports_flag(probe, "--allowedTools")` was
|
|
14
|
+
# always False and MCP tool pre-authorization was skipped on every run.
|
|
15
|
+
_FLAG_RE = re.compile(r"(--[A-Za-z][A-Za-z0-9-]+)")
|
|
16
|
+
|
|
17
|
+
# Bumped whenever the parse above changes meaning: entries written by an older
|
|
18
|
+
# parser are ignored (and pruned) instead of short-circuiting the re-parse.
|
|
19
|
+
_PROBE_SCHEMA = 2
|
|
12
20
|
|
|
13
21
|
|
|
14
22
|
def _cache_path() -> Path:
|
|
@@ -49,11 +57,14 @@ async def probe_cli(binary: str) -> dict:
|
|
|
49
57
|
cache = json.loads(cache_file.read_text())
|
|
50
58
|
except json.JSONDecodeError:
|
|
51
59
|
cache = {}
|
|
52
|
-
|
|
60
|
+
suffix = f"::v{_PROBE_SCHEMA}"
|
|
61
|
+
key = f"{path}::{version}{suffix}"
|
|
53
62
|
if key in cache:
|
|
54
63
|
return {"version": version, "flags": set(cache[key]), "ok": True}
|
|
55
64
|
help_text = await _capture([path, "--help"])
|
|
56
65
|
flags = set(_FLAG_RE.findall(help_text))
|
|
66
|
+
# Drop entries from older parser schemas rather than letting them accumulate.
|
|
67
|
+
cache = {k: v for k, v in cache.items() if k.endswith(suffix)}
|
|
57
68
|
cache[key] = sorted(flags)
|
|
58
69
|
cache_file.parent.mkdir(parents=True, exist_ok=True)
|
|
59
70
|
cache_file.write_text(json.dumps(cache, indent=1))
|
|
@@ -20,13 +20,19 @@ from harness.models.enums import AdapterKind
|
|
|
20
20
|
# name — first match wins, so list more specific keys first. Override per provider with
|
|
21
21
|
# ``params.max_context``.
|
|
22
22
|
MODEL_CONTEXT_WINDOWS: dict[str, int] = {
|
|
23
|
+
"claude-opus-5": 1_000_000,
|
|
23
24
|
"claude-opus-4-8": 1_000_000,
|
|
25
|
+
"claude-sonnet-5": 1_000_000,
|
|
24
26
|
"claude-sonnet-4-6": 1_000_000,
|
|
25
27
|
"claude-sonnet-4": 1_000_000,
|
|
26
|
-
"claude-fable-5": 1_000_000,
|
|
28
|
+
"claude-fable-5": 1_000_000, # also matches claude-fable-5-1
|
|
27
29
|
"claude-haiku": 200_000,
|
|
28
|
-
"gemini": 1_000_000, #
|
|
30
|
+
"gemini": 1_000_000, # every id `agy models` lists is 1M
|
|
29
31
|
"gemma": 131_072,
|
|
32
|
+
# gpt-oss is served locally (vLLM) and used to fall through to the caller's
|
|
33
|
+
# per-kind default — 1M under the antigravity kind — so history was never
|
|
34
|
+
# trimmed and the request overflowed the model instead.
|
|
35
|
+
"gpt-oss": 131_072,
|
|
30
36
|
"gpt-4o": 128_000,
|
|
31
37
|
"gpt-4.1": 1_000_000,
|
|
32
38
|
"llama": 131_072,
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
# either way. The desktop app is unaffected — its PyInstaller build freezes
|
|
6
6
|
# cli/main.py directly and never resolves this name (see ags.spec).
|
|
7
7
|
name = "ags-cli"
|
|
8
|
-
version = "0.
|
|
8
|
+
version = "0.2.0"
|
|
9
9
|
description = "AgentS: one unified interface over Claude Code, Google CLI (agy), and OpenAI-compatible local LLMs."
|
|
10
10
|
readme = "README.md"
|
|
11
11
|
requires-python = ">=3.12"
|