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.
Files changed (162) hide show
  1. {ags_cli-0.1.0 → ags_cli-0.2.0}/PKG-INFO +1 -1
  2. {ags_cli-0.1.0 → ags_cli-0.2.0}/ags_cli.egg-info/PKG-INFO +1 -1
  3. ags_cli-0.2.0/harness/adapters/antigravity.py +338 -0
  4. {ags_cli-0.1.0 → ags_cli-0.2.0}/harness/adapters/claude_code.py +46 -6
  5. {ags_cli-0.1.0 → ags_cli-0.2.0}/harness/adapters/probe.py +13 -2
  6. {ags_cli-0.1.0 → ags_cli-0.2.0}/harness/memory/context_budget.py +8 -2
  7. {ags_cli-0.1.0 → ags_cli-0.2.0}/pyproject.toml +1 -1
  8. ags_cli-0.1.0/harness/adapters/antigravity.py +0 -301
  9. {ags_cli-0.1.0 → ags_cli-0.2.0}/README.md +0 -0
  10. {ags_cli-0.1.0 → ags_cli-0.2.0}/ags_cli.egg-info/SOURCES.txt +0 -0
  11. {ags_cli-0.1.0 → ags_cli-0.2.0}/ags_cli.egg-info/dependency_links.txt +0 -0
  12. {ags_cli-0.1.0 → ags_cli-0.2.0}/ags_cli.egg-info/entry_points.txt +0 -0
  13. {ags_cli-0.1.0 → ags_cli-0.2.0}/ags_cli.egg-info/requires.txt +0 -0
  14. {ags_cli-0.1.0 → ags_cli-0.2.0}/ags_cli.egg-info/top_level.txt +0 -0
  15. {ags_cli-0.1.0 → ags_cli-0.2.0}/cli/__init__.py +0 -0
  16. {ags_cli-0.1.0 → ags_cli-0.2.0}/cli/__main__.py +0 -0
  17. {ags_cli-0.1.0 → ags_cli-0.2.0}/cli/client.py +0 -0
  18. {ags_cli-0.1.0 → ags_cli-0.2.0}/cli/commands/__init__.py +0 -0
  19. {ags_cli-0.1.0 → ags_cli-0.2.0}/cli/commands/adapter.py +0 -0
  20. {ags_cli-0.1.0 → ags_cli-0.2.0}/cli/commands/artifact.py +0 -0
  21. {ags_cli-0.1.0 → ags_cli-0.2.0}/cli/commands/ask.py +0 -0
  22. {ags_cli-0.1.0 → ags_cli-0.2.0}/cli/commands/chat.py +0 -0
  23. {ags_cli-0.1.0 → ags_cli-0.2.0}/cli/commands/diff.py +0 -0
  24. {ags_cli-0.1.0 → ags_cli-0.2.0}/cli/commands/doctor.py +0 -0
  25. {ags_cli-0.1.0 → ags_cli-0.2.0}/cli/commands/gui.py +0 -0
  26. {ags_cli-0.1.0 → ags_cli-0.2.0}/cli/commands/init.py +0 -0
  27. {ags_cli-0.1.0 → ags_cli-0.2.0}/cli/commands/mcp.py +0 -0
  28. {ags_cli-0.1.0 → ags_cli-0.2.0}/cli/commands/memory.py +0 -0
  29. {ags_cli-0.1.0 → ags_cli-0.2.0}/cli/commands/model.py +0 -0
  30. {ags_cli-0.1.0 → ags_cli-0.2.0}/cli/commands/policy.py +0 -0
  31. {ags_cli-0.1.0 → ags_cli-0.2.0}/cli/commands/project.py +0 -0
  32. {ags_cli-0.1.0 → ags_cli-0.2.0}/cli/commands/quota.py +0 -0
  33. {ags_cli-0.1.0 → ags_cli-0.2.0}/cli/commands/run.py +0 -0
  34. {ags_cli-0.1.0 → ags_cli-0.2.0}/cli/commands/serve.py +0 -0
  35. {ags_cli-0.1.0 → ags_cli-0.2.0}/cli/commands/service.py +0 -0
  36. {ags_cli-0.1.0 → ags_cli-0.2.0}/cli/commands/session.py +0 -0
  37. {ags_cli-0.1.0 → ags_cli-0.2.0}/cli/commands/stats.py +0 -0
  38. {ags_cli-0.1.0 → ags_cli-0.2.0}/cli/commands/status.py +0 -0
  39. {ags_cli-0.1.0 → ags_cli-0.2.0}/cli/commands/task.py +0 -0
  40. {ags_cli-0.1.0 → ags_cli-0.2.0}/cli/commands/usage.py +0 -0
  41. {ags_cli-0.1.0 → ags_cli-0.2.0}/cli/local.py +0 -0
  42. {ags_cli-0.1.0 → ags_cli-0.2.0}/cli/main.py +0 -0
  43. {ags_cli-0.1.0 → ags_cli-0.2.0}/harness/__init__.py +0 -0
  44. {ags_cli-0.1.0 → ags_cli-0.2.0}/harness/adapters/__init__.py +0 -0
  45. {ags_cli-0.1.0 → ags_cli-0.2.0}/harness/adapters/local_openai.py +0 -0
  46. {ags_cli-0.1.0 → ags_cli-0.2.0}/harness/adapters/mock.py +0 -0
  47. {ags_cli-0.1.0 → ags_cli-0.2.0}/harness/adapters/registry.py +0 -0
  48. {ags_cli-0.1.0 → ags_cli-0.2.0}/harness/adapters/warm_pool.py +0 -0
  49. {ags_cli-0.1.0 → ags_cli-0.2.0}/harness/api/__init__.py +0 -0
  50. {ags_cli-0.1.0 → ags_cli-0.2.0}/harness/api/router.py +0 -0
  51. {ags_cli-0.1.0 → ags_cli-0.2.0}/harness/api/v1/__init__.py +0 -0
  52. {ags_cli-0.1.0 → ags_cli-0.2.0}/harness/api/v1/adapters.py +0 -0
  53. {ags_cli-0.1.0 → ags_cli-0.2.0}/harness/api/v1/approvals.py +0 -0
  54. {ags_cli-0.1.0 → ags_cli-0.2.0}/harness/api/v1/artifacts.py +0 -0
  55. {ags_cli-0.1.0 → ags_cli-0.2.0}/harness/api/v1/attachments.py +0 -0
  56. {ags_cli-0.1.0 → ags_cli-0.2.0}/harness/api/v1/doctor.py +0 -0
  57. {ags_cli-0.1.0 → ags_cli-0.2.0}/harness/api/v1/health.py +0 -0
  58. {ags_cli-0.1.0 → ags_cli-0.2.0}/harness/api/v1/mcp.py +0 -0
  59. {ags_cli-0.1.0 → ags_cli-0.2.0}/harness/api/v1/memory.py +0 -0
  60. {ags_cli-0.1.0 → ags_cli-0.2.0}/harness/api/v1/policies.py +0 -0
  61. {ags_cli-0.1.0 → ags_cli-0.2.0}/harness/api/v1/project.py +0 -0
  62. {ags_cli-0.1.0 → ags_cli-0.2.0}/harness/api/v1/runs.py +0 -0
  63. {ags_cli-0.1.0 → ags_cli-0.2.0}/harness/api/v1/sessions.py +0 -0
  64. {ags_cli-0.1.0 → ags_cli-0.2.0}/harness/api/v1/stream.py +0 -0
  65. {ags_cli-0.1.0 → ags_cli-0.2.0}/harness/api/v1/tasks.py +0 -0
  66. {ags_cli-0.1.0 → ags_cli-0.2.0}/harness/api/v1/usage.py +0 -0
  67. {ags_cli-0.1.0 → ags_cli-0.2.0}/harness/bootstrap.py +0 -0
  68. {ags_cli-0.1.0 → ags_cli-0.2.0}/harness/db.py +0 -0
  69. {ags_cli-0.1.0 → ags_cli-0.2.0}/harness/deps.py +0 -0
  70. {ags_cli-0.1.0 → ags_cli-0.2.0}/harness/eventbus/__init__.py +0 -0
  71. {ags_cli-0.1.0 → ags_cli-0.2.0}/harness/eventbus/inprocess_bus.py +0 -0
  72. {ags_cli-0.1.0 → ags_cli-0.2.0}/harness/main.py +0 -0
  73. {ags_cli-0.1.0 → ags_cli-0.2.0}/harness/mcp/__init__.py +0 -0
  74. {ags_cli-0.1.0 → ags_cli-0.2.0}/harness/mcp/client.py +0 -0
  75. {ags_cli-0.1.0 → ags_cli-0.2.0}/harness/mcp/config_gen.py +0 -0
  76. {ags_cli-0.1.0 → ags_cli-0.2.0}/harness/mcp/registry.py +0 -0
  77. {ags_cli-0.1.0 → ags_cli-0.2.0}/harness/mcp/tools.py +0 -0
  78. {ags_cli-0.1.0 → ags_cli-0.2.0}/harness/memory/__init__.py +0 -0
  79. {ags_cli-0.1.0 → ags_cli-0.2.0}/harness/memory/embed.py +0 -0
  80. {ags_cli-0.1.0 → ags_cli-0.2.0}/harness/memory/extract.py +0 -0
  81. {ags_cli-0.1.0 → ags_cli-0.2.0}/harness/memory/ingest.py +0 -0
  82. {ags_cli-0.1.0 → ags_cli-0.2.0}/harness/memory/namespaces.py +0 -0
  83. {ags_cli-0.1.0 → ags_cli-0.2.0}/harness/memory/ranking.py +0 -0
  84. {ags_cli-0.1.0 → ags_cli-0.2.0}/harness/memory/redact.py +0 -0
  85. {ags_cli-0.1.0 → ags_cli-0.2.0}/harness/memory/service.py +0 -0
  86. {ags_cli-0.1.0 → ags_cli-0.2.0}/harness/memory/sqlite_vec_backend.py +0 -0
  87. {ags_cli-0.1.0 → ags_cli-0.2.0}/harness/memory/summarize.py +0 -0
  88. {ags_cli-0.1.0 → ags_cli-0.2.0}/harness/models/__init__.py +0 -0
  89. {ags_cli-0.1.0 → ags_cli-0.2.0}/harness/models/adapter_health.py +0 -0
  90. {ags_cli-0.1.0 → ags_cli-0.2.0}/harness/models/approval.py +0 -0
  91. {ags_cli-0.1.0 → ags_cli-0.2.0}/harness/models/artifact.py +0 -0
  92. {ags_cli-0.1.0 → ags_cli-0.2.0}/harness/models/audit_log.py +0 -0
  93. {ags_cli-0.1.0 → ags_cli-0.2.0}/harness/models/comparison.py +0 -0
  94. {ags_cli-0.1.0 → ags_cli-0.2.0}/harness/models/enums.py +0 -0
  95. {ags_cli-0.1.0 → ags_cli-0.2.0}/harness/models/mcp_server_config.py +0 -0
  96. {ags_cli-0.1.0 → ags_cli-0.2.0}/harness/models/memory.py +0 -0
  97. {ags_cli-0.1.0 → ags_cli-0.2.0}/harness/models/policy.py +0 -0
  98. {ags_cli-0.1.0 → ags_cli-0.2.0}/harness/models/provider_config.py +0 -0
  99. {ags_cli-0.1.0 → ags_cli-0.2.0}/harness/models/run.py +0 -0
  100. {ags_cli-0.1.0 → ags_cli-0.2.0}/harness/models/run_event.py +0 -0
  101. {ags_cli-0.1.0 → ags_cli-0.2.0}/harness/models/session.py +0 -0
  102. {ags_cli-0.1.0 → ags_cli-0.2.0}/harness/models/task.py +0 -0
  103. {ags_cli-0.1.0 → ags_cli-0.2.0}/harness/models/types.py +0 -0
  104. {ags_cli-0.1.0 → ags_cli-0.2.0}/harness/models/workspace.py +0 -0
  105. {ags_cli-0.1.0 → ags_cli-0.2.0}/harness/observability/__init__.py +0 -0
  106. {ags_cli-0.1.0 → ags_cli-0.2.0}/harness/observability/audit.py +0 -0
  107. {ags_cli-0.1.0 → ags_cli-0.2.0}/harness/observability/logging.py +0 -0
  108. {ags_cli-0.1.0 → ags_cli-0.2.0}/harness/observability/metrics.py +0 -0
  109. {ags_cli-0.1.0 → ags_cli-0.2.0}/harness/observability/tracing.py +0 -0
  110. {ags_cli-0.1.0 → ags_cli-0.2.0}/harness/orchestrator/__init__.py +0 -0
  111. {ags_cli-0.1.0 → ags_cli-0.2.0}/harness/orchestrator/engine.py +0 -0
  112. {ags_cli-0.1.0 → ags_cli-0.2.0}/harness/orchestrator/handoff.py +0 -0
  113. {ags_cli-0.1.0 → ags_cli-0.2.0}/harness/orchestrator/lifecycle.py +0 -0
  114. {ags_cli-0.1.0 → ags_cli-0.2.0}/harness/orchestrator/native_resume.py +0 -0
  115. {ags_cli-0.1.0 → ags_cli-0.2.0}/harness/orchestrator/policies.py +0 -0
  116. {ags_cli-0.1.0 → ags_cli-0.2.0}/harness/orchestrator/reconcile.py +0 -0
  117. {ags_cli-0.1.0 → ags_cli-0.2.0}/harness/orchestrator/run_graph.py +0 -0
  118. {ags_cli-0.1.0 → ags_cli-0.2.0}/harness/orchestrator/snapshot.py +0 -0
  119. {ags_cli-0.1.0 → ags_cli-0.2.0}/harness/schemas/__init__.py +0 -0
  120. {ags_cli-0.1.0 → ags_cli-0.2.0}/harness/schemas/adapter.py +0 -0
  121. {ags_cli-0.1.0 → ags_cli-0.2.0}/harness/schemas/approval.py +0 -0
  122. {ags_cli-0.1.0 → ags_cli-0.2.0}/harness/schemas/artifact.py +0 -0
  123. {ags_cli-0.1.0 → ags_cli-0.2.0}/harness/schemas/common.py +0 -0
  124. {ags_cli-0.1.0 → ags_cli-0.2.0}/harness/schemas/memory.py +0 -0
  125. {ags_cli-0.1.0 → ags_cli-0.2.0}/harness/schemas/policy.py +0 -0
  126. {ags_cli-0.1.0 → ags_cli-0.2.0}/harness/schemas/run.py +0 -0
  127. {ags_cli-0.1.0 → ags_cli-0.2.0}/harness/schemas/session.py +0 -0
  128. {ags_cli-0.1.0 → ags_cli-0.2.0}/harness/schemas/task.py +0 -0
  129. {ags_cli-0.1.0 → ags_cli-0.2.0}/harness/security/__init__.py +0 -0
  130. {ags_cli-0.1.0 → ags_cli-0.2.0}/harness/security/approval_policy.py +0 -0
  131. {ags_cli-0.1.0 → ags_cli-0.2.0}/harness/security/auth.py +0 -0
  132. {ags_cli-0.1.0 → ags_cli-0.2.0}/harness/security/permissions.py +0 -0
  133. {ags_cli-0.1.0 → ags_cli-0.2.0}/harness/security/risk.py +0 -0
  134. {ags_cli-0.1.0 → ags_cli-0.2.0}/harness/security/secrets.py +0 -0
  135. {ags_cli-0.1.0 → ags_cli-0.2.0}/harness/services/__init__.py +0 -0
  136. {ags_cli-0.1.0 → ags_cli-0.2.0}/harness/services/adapters_health.py +0 -0
  137. {ags_cli-0.1.0 → ags_cli-0.2.0}/harness/services/approvals.py +0 -0
  138. {ags_cli-0.1.0 → ags_cli-0.2.0}/harness/services/artifacts.py +0 -0
  139. {ags_cli-0.1.0 → ags_cli-0.2.0}/harness/services/attachments.py +0 -0
  140. {ags_cli-0.1.0 → ags_cli-0.2.0}/harness/services/comparison.py +0 -0
  141. {ags_cli-0.1.0 → ags_cli-0.2.0}/harness/services/doctor.py +0 -0
  142. {ags_cli-0.1.0 → ags_cli-0.2.0}/harness/services/files.py +0 -0
  143. {ags_cli-0.1.0 → ags_cli-0.2.0}/harness/services/mcp_servers.py +0 -0
  144. {ags_cli-0.1.0 → ags_cli-0.2.0}/harness/services/project_git.py +0 -0
  145. {ags_cli-0.1.0 → ags_cli-0.2.0}/harness/services/retention.py +0 -0
  146. {ags_cli-0.1.0 → ags_cli-0.2.0}/harness/services/runs.py +0 -0
  147. {ags_cli-0.1.0 → ags_cli-0.2.0}/harness/services/runs_diff.py +0 -0
  148. {ags_cli-0.1.0 → ags_cli-0.2.0}/harness/services/sessions.py +0 -0
  149. {ags_cli-0.1.0 → ags_cli-0.2.0}/harness/services/ssh.py +0 -0
  150. {ags_cli-0.1.0 → ags_cli-0.2.0}/harness/services/stats.py +0 -0
  151. {ags_cli-0.1.0 → ags_cli-0.2.0}/harness/services/tasks.py +0 -0
  152. {ags_cli-0.1.0 → ags_cli-0.2.0}/harness/services/workspaces.py +0 -0
  153. {ags_cli-0.1.0 → ags_cli-0.2.0}/harness/services/worktrees.py +0 -0
  154. {ags_cli-0.1.0 → ags_cli-0.2.0}/harness/settings.py +0 -0
  155. {ags_cli-0.1.0 → ags_cli-0.2.0}/harness/skills/__init__.py +0 -0
  156. {ags_cli-0.1.0 → ags_cli-0.2.0}/harness/skills/manager.py +0 -0
  157. {ags_cli-0.1.0 → ags_cli-0.2.0}/harness/tools/__init__.py +0 -0
  158. {ags_cli-0.1.0 → ags_cli-0.2.0}/harness/tools/fs_tools.py +0 -0
  159. {ags_cli-0.1.0 → ags_cli-0.2.0}/harness/workers/__init__.py +0 -0
  160. {ags_cli-0.1.0 → ags_cli-0.2.0}/harness/workers/dispatch.py +0 -0
  161. {ags_cli-0.1.0 → ags_cli-0.2.0}/harness/workers/inprocess.py +0 -0
  162. {ags_cli-0.1.0 → ags_cli-0.2.0}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ags-cli
3
- Version: 0.1.0
3
+ Version: 0.2.0
4
4
  Summary: AgentS: one unified interface over Claude Code, Google CLI (agy), and OpenAI-compatible local LLMs.
5
5
  Author: Platform Engineering
6
6
  License: Apache-2.0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ags-cli
3
- Version: 0.1.0
3
+ Version: 0.2.0
4
4
  Summary: AgentS: one unified interface over Claude Code, Google CLI (agy), and OpenAI-compatible local LLMs.
5
5
  Author: Platform Engineering
6
6
  License: Apache-2.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 against
2
- v2.1.187).
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-fable-5",
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
- return # non-JSON noise (shouldn't happen with stream-json)
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
- _FLAG_RE = re.compile(r"(--[a-z][a-z0-9-]+)")
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
- key = f"{path}::{version}"
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, # 3-pro / 3.1-pro / 3.5-flash all 1M
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.1.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"