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
runAntigravityHarness (the `ax harness antigravity` path used by the
substrate ActorTemplate) forked the Python sidecar with only --host/--port.
The sidecar then fell back to its own default state dir, so on a substrate
actor per-conversation save_dir landed on a path AX does not control, which
breaks SDK-native resume across actor Run/Restore and snapshotting.
Derive the trajectory dir via antigravity.DefaultStateDir() and pass it as
--state-dir, mirroring the local path in antigravity.New and the interactions
harness in runAntigravityInteractionsHarness (both own the path internally
rather than taking it from ax.yaml/ActorTemplate).
Fixes#321
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).
* 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.
The AGY harness streams each contiguous block of assistant text as a separate TextContent message, with tool calls in between. The CLI display (cmd/ax/internal/display.go) tracks a state field to decide when to insert separating newlines but the Content_ToolCall case was a pure no-op that left state == stateText.
Fixes#233.
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
The AX Dashboard uses live polling (every 3 seconds) to fetch updated trace data for the active conversation. Each time new trace data is retrieved, the dashboard completely regenerates the HTML for the execution trace cards using innerHTML. Because the cards were hardcoded to render with the collapsed and hidden classes by default, any cards the user had expanded would collapse every time the poll triggered. Since users are usually scrolling while reading the expanded trace details, this gave the impression that the history was collapsing during scrolling.
Fixes#202.