Make Task manifests fully immutable once created, removing `UpdateTask`
in favor of `CreateTask` and driving lifecycle transitions (suspend,
resume, delete) through dedicated RPCs and event stream processing.
- Proto: Replace `UpdateTask` with `CreateTask` in AX service.
- Proto: Remove mutable `suspend` field from `TaskSpec` and reserve field 2.
- API / Server: Implement `CreateTask` with duplicate name rejection
(`codes.AlreadyExists`). Update `SuspendTask`, `ResumeTask`, and `DeleteTask`
to transition status phase and publish lifecycle events.
- Store: Remove redundant `SetTaskSuspended` and `MarkTaskDeleting` methods;
standardize on `CreateTask`, `UpdateTaskStatus`, `DeleteTask`, and
`PublishEvent`.
- Controller: Decouple spec reconciliation from task suspension; introduce
`ReconcileSuspend` to handle checkpointing and pausing actors on Substrate.
- CLI: Update `ax apply` to submit tasks via `CreateTask`.
- Docs: Update design docs and specifications to reflect task immutability.
AX is undergoing a significant redesign, moving away from a single CLI with
an embedded Python harness toward a general-purpose orchestration layer for
agentic tasks. The repository is reorganized around three binaries: ax-server
exposes a gRPC API for submitting Task manifests, ax-controller runs as a
horizontally scaled reconciler that consumes work from Redis Streams, and
ax-task-runner executes tasks inside sandboxed workers. Task state now lives
in Redis rather than Kubernetes CRDs so the system can handle millions of
short-lived tasks without straining etcd. The legacy Python harness, ATE
client, SQL event log, and skill examples are removed, and new design,
concepts, manifest, networking, runner, and sandbox docs plus Kubernetes
deploy manifests describe the new architecture.
We want AX to be compatible with the Interactions API protocol as much as possible. This PR aligns the proto with Steps and with the Interactions semantics. We will follow up with more changes to make it more compatible with Interactions while preserving a `conversation_id` based history model.
`ConversationService` is removed for now until we know what kind of data operations we want to provide for event log management.
Introduce a local skills source parallel to the Gemini Enterprise Skill
Registry integration. A local source points at a directory already on disk
(a parent dir holding <skill-id>/SKILL.md subfolders, the same layout as
examples/skills and a registry's target_dir); nothing is fetched and the
directory is used as-is.
- New mode-agnostic result shape (internal/skills: Available/Group/Skill) so
both sources feed the same consumers without either depending on the other.
- internal/skills/local.Discover enumerates enabled local paths, treating
immediate subfolders with a SKILL.md as skills (subfolder name = id). It is
fail-safe like registry materialization: a missing/unreadable/empty path is
logged and skipped, never blocking harness creation.
- config.SkillsConfig gains Local []LocalSkillsConfig (enabled + path) with
validation; geminienterprise.Materialize now returns skills.Available.
- cliutil combines registry and local groups into one system-instruction
pointer for the Antigravity Interactions harness. The Antigravity SDK
harness (SKILLS_DIR-based) is left as a TODO.
Only the interactions harness is wired; tests cover discovery, fail-safe
degradation, config validation, and the examples/skills fixture.
google-antigravity 0.1.7's _connect_websocket retries both "localhost"
and "127.0.0.1" when dialing localharness (local_connection.py:993), so
the sidecar no longer needs to patch /etc/hosts for substrate/gVisor
environments where localhost may not resolve.
Ref #325
antigravity: guard conversation_id for safe save_dir use (#312)
harness_server uses the caller-supplied conversation_id directly as a
per-conversation storage directory name (save_dir = state_dir /
conversation_id) before the Antigravity harness ever sees it, so a value
like "../escape" could traverse outside state_dir. Reject an empty id or
one containing a path separator or a "." / ".." component at the boundary
in _run_turn, returning an INVALID_ARGUMENT end frame.
This is intentionally only a path-safety guard, not the id-format
contract. The Antigravity harness owns that: it validates the forwarded
cascade_id (length and character set) per cl/948016352. Keeping the
format rule in one place avoids the two layers drifting.
chore: remove stale docs, gitignore, and Makefile entries (#329)
- CONTRIBUTING.md: drop 'make run-remote' (no such Makefile target)
- .gitignore: remove orphaned entries (local/remote_agent, sandbox-router,
tasklog, axepp, google-cloud-sdk, test-sandbox-config.yaml)
- Makefile: add missing deps, clean-logs to .PHONY
antigravity: remove vertex env-var override (superseded by AGY 0.1.7) (#331)
* antigravity: remove vertex env-var override (superseded by AGY 0.1.7)
google-antigravity 0.1.7 reads GOOGLE_GENAI_USE_VERTEXAI /
GOOGLE_GENAI_USE_ENTERPRISE + GOOGLE_CLOUD_{PROJECT,LOCATION} natively,
so the sidecar no longer needs _vertex_kwargs_from_env to thread them
into LocalAgentConfig.
- Delete _vertex_kwargs_from_env + VertexKwargs.
- _build_default_config: bare LocalAgentConfig(system_instructions=...).
- _has_credentials: read project/location from env (SDK hydrates them
onto VertexEndpoint, not LocalAgentConfig).
- Update tests accordingly.
Requires the 0.1.7 bump. Credential resolution is startup-only and
does not touch the save_dir/state_dir resume path.
Ref #325
* antigravity: drop stale _has_credentials docstring note about vertex env override
Interactions: persist interactions harness with durable dir and resume from it (#327)
interactions: make WorkDir authoritative for run_command (#335)
The interactions harness executed run_command relative to the process's
ambient working directory, which is not reliably the agent's workspace: with
no Cwd argument the command ran wherever the process cwd happened to be, and a
model-supplied Cwd (including "/") was honored blindly. As a result tool calls
often ran from "/" instead of the configured workspace (#332).
Make the workspace directory authoritative in the executor rather than relying
on ambient process state:
- Add AntigravityInteractionsConfig.WorkDir (defaults from AX_HARNESS_WORKDIR).
- run_command now resolves its directory via resolveRunDir: no Cwd -> WorkDir;
relative Cwd -> joined under WorkDir; absolute Cwd -> honored as-is. Empty
WorkDir preserves the previous process-cwd behavior.
- Add WorkspaceSystemInstruction to orient the agent about its working
directory so it emits workspace-relative paths, and wire it in cmd/ax.
Fixes#332
Remove the binary (#334)
* Remove the binary
* chore: add e2e directory to .gitignore
* refactor: migrate binary file check to a dedicated workflow file
* fix: restrict binary file check to added, copied, or modified files
* Honor view_file line range and cap result size
execViewFile read the whole file and ignored the agent's StartLine/EndLine,
so viewing a large file returned its entire content as the tool result --
a ~900KB blob for a 3k-line CSV stalled the following turn.
Implement the Antigravity view_file contract (1-indexed inclusive line range
with slice-notation windowing) plus defensive caps:
- StartLine/EndLine window (neither=first N lines; start-only=next N forward;
end-only=previous N backward; both=precise range, capped to N).
- viewFileMaxLines / viewFileMaxBytes caps so a large file can never blob.
- ContentOffset honored as the read position within the windowed content.
The result payload is just {"content": ...}; the view_file result schema is
defined server-side, so no extra fields are added.
Adds intArg/intArgOK helpers and tests for windowing, byte cap, offset read,
and range honoring.
* view_file: UTF-8-safe byte cap + pagination metadata
Address review feedback on the byte cap:
- applyByteWindow backs the cut off to the last complete UTF-8 rune so a
multi-byte character straddling viewFileMaxBytes is never split (raw byte
slicing could produce invalid UTF-8, corrupting the last char and JSON
serialization).
- execViewFile returns the metadata the server needs to distinguish a
complete read from a paginated/byte-truncated one: content, start_line/
end_line (0-indexed inclusive served range), content_offset,
line_range_bytes (total bytes of the line range pre-byte-cap), and
num_lines/num_bytes. The server detects truncation via
content_offset+len(content) < line_range_bytes and prompts the model to
resume from that offset.
A paired server change reads these fields instead of hardcoding
start_line=0/end_line=last.
Adds tests for UTF-8 boundary capping, pagination metadata, and resume.
Materialize agentskills.io skills from the Gemini Enterprise Skill Registry
(Vertex AI v1beta1) into on-disk folders before the harness starts, so the
built-in harnesses can use registry-hosted skills on the local `ax exec` /
`ax serve` path.
- internal/skills/geminienterprise: harness-agnostic package that reads
config.SkillsConfig, drives the registry client (ListSkills / GetSkill /
GetSkillRevision / skills:retrieve), safe-unzips payloads to
<target_dir>/<skill-id>/, and reports what it wrote. First-wins on
duplicate ids with a warning; fail-safe (a registry error never blocks
harness startup).
- config: top-level `skills.registries[]` (harness-agnostic -- each actor
runs a single harness that consumes the materialized folder). Per-registry
selection (skills / query / all), required target_dir, and a validated
"exactly one selection mode" rule. Also wires the interactions harness's
system_instruction from ax.yaml.
- cliutil: materializes skills once, up front, at controller construction;
for the interactions harness (no SKILLS_DIR concept) it appends a discovery
pointer to the system instruction.
Scope: local path only; the substrate/pod path does not yet read ax.yaml.
Verified end-to-end against a live registry (by-id and by-query).
state_dir is an AGY SDK implementation detail (where trajectory /
resume-cursor storage lives), not something AX users should configure.
Remove the yaml surface for both built-in harnesses and derive the path
internally from config.AXAssetsDir().
- config: drop StateDir from AntigravityHarnessConfig and
AntigravityInteractionsHarnessConfig.
- antigravity: add DefaultStateDir() -> ~/.ax/antigravity/conversations,
mirroring antigravityinteractions.DefaultStateDir(). cliutil now passes
this into antigravity.New instead of the yaml value.
- cliutil: interactions harness always uses DefaultStateDir(); drop the
yaml read + fallback.
The internal APIs (antigravity.New's stateDir arg, the required
AntigravityInteractionsConfig.StateDir field, and the Python sidecar's
--state-dir default) are unchanged -- only the user-facing ax.yaml knob
is removed.
Addresses rakyll's follow-up on #292.
* antigravity: plumb sidecar state_dir from ax.yaml
Sidecar's trajectory storage directory was hardcoded in Python. This
plumbs it through as a yaml config that users can override, matching
the pattern already used for the Interactions harness's state_dir.
Flow:
- ax.yaml: harnesses.antigravity.state_dir (optional; empty = default)
- config.go: AntigravityHarnessConfig.StateDir field (yaml surface only,
no default logic on the Go side)
- cliutil.go: passes yaml value as-is (empty string when unset)
- antigravity Go client: appends --state-dir arg only when non-empty
- Python sidecar: argparse --state-dir with default
~/.ax/antigravity/conversations. Default is a transitional fallback
for substrate mode (where ActorTemplate doesn't inject the flag yet).
Removable once substrate can set the field via ActorTemplate.
Servicer's state_dir is required (no fallback) -- production path always
receives it from argparse; tests pass tmp_path.
Verified end-to-end with ax exec:
- No yaml state_dir -> ~/.ax/antigravity/conversations/{conv_id}/
- yaml state_dir: X -> X/{conv_id}/
- Different conversations remain isolated across both paths.
Closes part of the local-mode gap in issue #269 (harness_config
contract design); a follow-up will do the same for substrate via
ActorTemplate.
* antigravity: address PR review comments (P1/P2/P3)
- P1: two Python tests referenced tmp_path without declaring the fixture;
add tmp_path to their signatures.
- P2: state_dir CLI arg was not tilde-expanded, so ~/.ax/... became a
literal ~ dir. Add .expanduser().
- P3: no Go-side test asserted --state-dir forwarding. Extract
buildSidecarArgs() as a small pure helper and add table test for the
empty/non-empty cases, plus a yaml parse test for
AntigravityHarnessConfig.StateDir.
* antigravity: inline sidecar args build instead of separate helper
Follow-up to review feedback: buildSidecarArgs was overkill for a 3-line
conditional. Move it back into New() inline. Also drops the associated
TestBuildSidecarArgs (the arg-forwarding rule is now covered only by
end-to-end verification).
parseStreamedTurn only handled interaction.* and step.* SSE events; a
server-emitted "error" event fell through the switch and was silently
dropped. The turn then returned as completed-but-empty, which:
- made a failure (e.g. INVALID_ARGUMENT for a malformed client tool
result) look like a blank "no response" turn, and
- persisted the failing interaction's id as a poisoned resume cursor.
Add an "error" case that returns a real error, plus a serverErrorMessage
helper that extracts message/status from the error payload. Now a
turn-level server failure aborts the turn with a descriptive error
instead of being hidden.
Adds tests for the error-event path and serverErrorMessage formatting.
* Harden Antigravity Interactions harness HTTP client
Fix a substrate suspend/resume failure and tighten the config surface:
- Disable HTTP keep-alives on the harness's default client. On substrate the
actor is suspended after a turn and resumed for the next (with a new routable
IP), which leaves any pooled keep-alive connection stale; reusing it made the
next turn's request fail. Opening a fresh connection per request avoids that
at the cost of an extra handshake.
- Remove the public HTTPClient override from AntigravityInteractionsConfig.
External callers don't configure the harness's transport, and exposing it
risked silently reintroducing the keep-alive bug. New now always owns its
(keep-alive-disabled) client; tests inject a fake transport via an unexported
newWithHTTPClient seam.
- Reorder AntigravityInteractionsConfig fields to list required (Agent,
StateDir) before optional, with clearer doc comments.
Build and package tests pass.
* Explain why the HTTP transport is cloned, not newly created
Address review feedback: document that cloning DefaultTransport (rather than a
bare &http.Transport{}) is intentional -- it inherits the production defaults
(env proxy, dial/TLS/handshake timeouts, HTTP/2) and only overrides keep-alives.
A fresh empty transport would silently drop those.
* Consolidate .ax directory path resolution into a centralized config helper
* refactor: remove unused TargetDir field from SetupOptions in python sidecar tests
* test: update setup test to use HOME directory and verify .ax subdirectory extraction
* refactor: simplify directory path resolution and variable naming in python sidecar setup
* perf: skip redundant file copying and pip installations when dependencies are up to date
* fix: remove redundant check for non-existent requirements file in setup
* refactor: track file updates during extraction to skip unnecessary pip installs
* feat: add progress notification when installing Antigravity SDK
* fix: ensure site-packages directory exists before returning path in setup
* feat: add pypi simple index to pip install command for environment compatibility
* Embed python artifacts into ax
And setup the AGY harness service at runtime.
* refactor: remove directory and file filtering logic from setup walk function
* refactor: rename installRequirements to install in python sidecar setup
* refactor: simplify Sidecar configuration by passing PYTHONPATH directly to Start method and removing unnecessary environment and binary options
* feat: configure pip install to use local site-packages and filter unwanted files during setup
* feat: support appending to PYTHONPATH and update Python setup path resolution
* refactor: remove redundant file filtering in sidecar setup directory traversal
* refactor: simplify python package installation and add PyPI index URL to pip command
* fix: remove extra index URL from pip install command
* refactor: remove Sidecar.Setup method and move asset extraction logic to external standalone function
* refactor: remove PythonPath configuration field from sidecar.Config in favor of direct global pythonPath usage
* test: remove integration test for antigravity sidecar auto-start logic
* fix: add pypi.org extra-index-url to python package installation command
* refactor: remove Stdin support from pythonsidecar configuration and cleanup redundant harness definitions
Local mode (the default for ax exec) now forks the Antigravity Python
sidecar via the pythonsidecar package (introduced in #273) so users
don't need to launch the harness server out of band. Modeled after
cmd/ax/harness.go.
AntigravityHarness.New takes an autoStart bool:
- true (cliutil, local mode): fork the sidecar, wait for TCPReady,
and install a signal handler that stops the sidecar on
ctrl-C / SIGTERM. Full lifecycle stays inside antigravity.go so
callers don't need a registry -> controller -> antigravity
teardown chain.
- false (e2e demo, unit tests against harnesstest.MockHarnessServer):
pure gRPC-client constructor, no fork.
Close stops the forked sidecar; the signal-handler goroutine is the
primary caller today.
Tests:
- internal/harness/antigravity/antigravity_test.go covers both
autoStart paths via TestNew_AutoStartTrue_StubServer_ForksSidecar
(drives the real fork against a minimal stub Python module on
PYTHONPATH) and TestNew_AutoStartFalse_NilSidecar.
- cmd/ax/internal/cliutil/cliutil_test.go's DefaultHarness test uses
the same stub-Python trick to exercise the end-to-end wiring
through NewControllerFromConfig.
e2e demo (internal/cmd/e2e/main.go) still uses autoStart=false; TODO
notes a companion autoStart=true demo as follow-up.
* Serve the Antigravity Interactions harness over HarnessService
Add a Go HarnessService server for the Antigravity Interactions harness so
it can run as a substrate actor, alongside the existing Python sidecar path.
- internal/harness/antigravityinteractions/server.go: Serve(...) stands up
the gRPC HarnessService (plus gRPC health) and an HTTP /readyz endpoint
reflecting this server's own serving state, with graceful shutdown on
ctx-cancel or SIGINT/SIGTERM. Connect adapts the harness onto the wire
contract: a start frame drives one turn (Start -> Queue -> Run), each agent
message is streamed as a HarnessResponse{outputs} frame, and the stream
terminates with exactly one HarnessResponse{end} (COMPLETED/FAILED/
CANCELED); a mid-stream cancel frame cancels the run.
- cmd/ax/harness.go: add a thin runAntigravityInteractionsHarness(ctx,
systemInstructions) entrypoint that builds the config and calls Serve; the
dispatch layer decides when to invoke it.
- server_test.go: bufconn tests for the start->end contract and rejection of
a non-start first frame.
* Persist resume cursors under .ax/cursors in the work dir
Place the Antigravity Interactions harness resume-cursor StateDir in a
dedicated ".ax/cursors" subdirectory under the actor working directory,
rather than the work dir root. This keeps AX internal state out of the
agent-visible workspace (so it isn't surfaced by list_dir and can't collide
with the agent's files) while staying within the snapshotted filesystem so
it survives snapshot/restore.
* Document why steering is not supported over Connect
Add a comment in the HarnessService Connect loop explaining that mid-run
steering is intentionally unsupported: the execution can accept steering via
Queue, but the HarnessRequest oneof only defines {start, cancel}, so there is
no wire frame to deliver input after start. Multi-turn conversation is
expressed as separate Connect executions that resume via the persisted cursor.
* Persist resume cursors under ~/.ax/cursors
Move the resume-cursor StateDir out of the agent's working directory to a
dedicated ~/.ax/cursors under the user's home directory. The working directory
is the agent's operating surface (it reads and edits files there, including
hidden ones), so keeping AX internal state separate avoids the agent seeing or
clobbering it. If the home directory cannot be resolved, the harness now fails
with an error rather than silently falling back to the current directory.
* Add durable conversation resumability to the Antigravity Interactions harness
Introduce a small durable key-value store and use it to persist each
conversation's interaction-chain cursor (the last interaction id), so a
conversation can resume after a process restart instead of starting a new chain.
- internal/storage: a minimal Store interface (Get/Put/Delete + ErrNotFound)
with documented semantics (atomicity, read-after-write, not-found-vs-error,
durability) and a single-writer concurrency model. Includes a filesystem
implementation (FileStore) that writes atomically via temp-file + rename.
- harness: add AntigravityInteractionsConfig.StateStore; Start loads any
persisted cursor and Run persists it after each successful turn, so a fresh
Execution for the same conversation continues the existing interaction chain.
- harness: document the single-writer-per-conversation expectation on the
Harness interface (the controller guarantees it), which is what makes the
last-write-wins store correct.
Also includes related harness improvements:
- Retry HTTP 429 (rate limit) with exponential backoff + jitter, honoring
Retry-After; only 429 is retried since it is rejected before any interaction
is created.
- Terminology cleanup: the within-Run FC/FR loop is the "interaction loop"
(continuation turns chained via previous_interaction_id), distinct from an
AX-level resume.
* Scope resume-cursor persistence into the Antigravity Interactions harness
Address PR review feedback: instead of a repo-level internal/storage
key-value abstraction, keep resume-cursor persistence local to the
Antigravity Interactions harness.
- Remove the internal/storage package (Store interface + FileStore).
- Add a harness-local, file-based cursorStore (cursorstore.go) with
load/save of the per-conversation resumeCursor. resumeCursor stays a
struct so it can grow (e.g. partial function-call results for
mid-tool-loop recovery) later.
- Replace Config.StateStore with Config.StateDir. StateDir is now
required: NewAntigravityInteractionsHarness returns an error if it is
empty, and the constructor now returns (*Harness, error).
- Add tests with a fake Interactions API (an http.RoundTripper) that
records each request and returns a canned SSE stream, covering the
resume-across-restart CUJ (a fresh harness over the same StateDir sends
the persisted previous_interaction_id), same-harness resume, the
required-StateDir error, and a cursorStore load/save round-trip.
* Clarify single-writer as a caller expectation, not a controller guarantee
Address PR review: the controller does not currently enforce one Execution
per conversation, so the StateDir doc comment shouldn't claim it does. Phrase
single-writer (last-write-wins, no CAS) as an expectation the caller/deployment
must satisfy -- e.g. sticky routing of a conversation id to one worker, or
resuming a worker from that conversation's snapshot.
Refactor telemetry configuration and resolve deprecated GCP metadata calls:
- Replace deprecated `metadata.ProjectID()` and `metadata.OnGCE()` with context-aware versions.
- Update `telemetry.SetTraceProvider` to accept variadic `otlptracegrpc.Option`s and remove the hardcoded endpoint parameter.
- Add `telemetry.WithGCPCredentials()` helper to encapsulate GCP-specific credentials.
- Isolate GCP-specific server telemetry configuration into a new `serve_gcp.go` file.
Fix premature gRPC stream cancellation in the harness execution clients:
- Update the client receive loop to drain the stream until `io.EOF` after receiving the `HarnessEnd` frame, preventing `CANCELLED` errors in telemetry.
- Encapsulate the stream draining and message dispatching logic into a shared `drainStream` helper in a new `stream.go` file.
- Clean up unused `io` imports in `antigravity.go` and `substrate.go`.
Follow-up:
- End-to-end trace context propagation to HarnessService deployments
- End-to-end trace context propagation to Substrate calls
* Remove dead code from the earlier controller
Remove the agent package, AgentService related proto messages, and Gemini agents in favor of the new universal agent harness.
* feat: update proto definitions to use google.protobuf.Struct for harness_config and replace agent fields with harness fields in ExecRequest