gravity-cli 0.1.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.
- gravity_cli-0.1.0/.env.example +21 -0
- gravity_cli-0.1.0/.github/workflows/ci.yml +70 -0
- gravity_cli-0.1.0/.github/workflows/release.yml +110 -0
- gravity_cli-0.1.0/.gitignore +12 -0
- gravity_cli-0.1.0/AGENT_STANDARD.md +339 -0
- gravity_cli-0.1.0/BUILDER_GUIDE.md +735 -0
- gravity_cli-0.1.0/LICENSE +202 -0
- gravity_cli-0.1.0/NOTICE +4 -0
- gravity_cli-0.1.0/PKG-INFO +494 -0
- gravity_cli-0.1.0/README.md +477 -0
- gravity_cli-0.1.0/TEMPLATES.md +173 -0
- gravity_cli-0.1.0/packages/gravity-schema/LICENSE +202 -0
- gravity_cli-0.1.0/packages/gravity-schema/NOTICE +4 -0
- gravity_cli-0.1.0/packages/gravity-schema/README.md +3 -0
- gravity_cli-0.1.0/packages/gravity-schema/gravity_schema/__init__.py +88 -0
- gravity_cli-0.1.0/packages/gravity-schema/gravity_schema/__main__.py +6 -0
- gravity_cli-0.1.0/packages/gravity-schema/gravity_schema/catalog.py +93 -0
- gravity_cli-0.1.0/packages/gravity-schema/gravity_schema/catalog.yaml +221 -0
- gravity_cli-0.1.0/packages/gravity-schema/gravity_schema/catalog_imported.yaml +7802 -0
- gravity_cli-0.1.0/packages/gravity-schema/gravity_schema/contract.py +87 -0
- gravity_cli-0.1.0/packages/gravity-schema/gravity_schema/cron.py +47 -0
- gravity_cli-0.1.0/packages/gravity-schema/gravity_schema/errors.py +171 -0
- gravity_cli-0.1.0/packages/gravity-schema/gravity_schema/export.py +67 -0
- gravity_cli-0.1.0/packages/gravity-schema/gravity_schema/gen_catalog.py +129 -0
- gravity_cli-0.1.0/packages/gravity-schema/gravity_schema/manifest.py +627 -0
- gravity_cli-0.1.0/packages/gravity-schema/gravity_schema/validation.py +152 -0
- gravity_cli-0.1.0/packages/gravity-schema/pyproject.toml +28 -0
- gravity_cli-0.1.0/packages/gravity-schema/schema/manifest.schema.json +450 -0
- gravity_cli-0.1.0/packages/gravity-schema/tests/conftest.py +47 -0
- gravity_cli-0.1.0/packages/gravity-schema/tests/test_catalog_parity.py +56 -0
- gravity_cli-0.1.0/packages/gravity-schema/tests/test_cron.py +31 -0
- gravity_cli-0.1.0/packages/gravity-schema/tests/test_errors.py +165 -0
- gravity_cli-0.1.0/packages/gravity-schema/tests/test_json_schema.py +87 -0
- gravity_cli-0.1.0/packages/gravity-schema/tests/test_kind_outputs.py +69 -0
- gravity_cli-0.1.0/packages/gravity-schema/tests/test_manifest.py +626 -0
- gravity_cli-0.1.0/packages/gravity-schema/tests/test_slots.py +52 -0
- gravity_cli-0.1.0/packages/gravity-schema/tests/test_validation.py +172 -0
- gravity_cli-0.1.0/pyproject.toml +53 -0
- gravity_cli-0.1.0/src/gravity_cli/__init__.py +11 -0
- gravity_cli-0.1.0/src/gravity_cli/__main__.py +4 -0
- gravity_cli-0.1.0/src/gravity_cli/_runner_script.py +195 -0
- gravity_cli-0.1.0/src/gravity_cli/api.py +88 -0
- gravity_cli-0.1.0/src/gravity_cli/cli.py +124 -0
- gravity_cli-0.1.0/src/gravity_cli/commands/__init__.py +0 -0
- gravity_cli-0.1.0/src/gravity_cli/commands/adapt.py +155 -0
- gravity_cli-0.1.0/src/gravity_cli/commands/build.py +83 -0
- gravity_cli-0.1.0/src/gravity_cli/commands/init.py +78 -0
- gravity_cli-0.1.0/src/gravity_cli/commands/login.py +58 -0
- gravity_cli-0.1.0/src/gravity_cli/commands/push.py +98 -0
- gravity_cli-0.1.0/src/gravity_cli/commands/run.py +281 -0
- gravity_cli-0.1.0/src/gravity_cli/commands/runs.py +49 -0
- gravity_cli-0.1.0/src/gravity_cli/commands/schedule.py +74 -0
- gravity_cli-0.1.0/src/gravity_cli/commands/tools.py +27 -0
- gravity_cli-0.1.0/src/gravity_cli/commands/validate.py +195 -0
- gravity_cli-0.1.0/src/gravity_cli/config.py +91 -0
- gravity_cli-0.1.0/src/gravity_cli/manifest.py +19 -0
- gravity_cli-0.1.0/src/gravity_cli/project.py +42 -0
- gravity_cli-0.1.0/src/gravity_cli/templates/chat/.gitignore +6 -0
- gravity_cli-0.1.0/src/gravity_cli/templates/chat/manifest.yaml +28 -0
- gravity_cli-0.1.0/src/gravity_cli/templates/chat/requirements.txt +3 -0
- gravity_cli-0.1.0/src/gravity_cli/templates/chat/src/agent.py +80 -0
- gravity_cli-0.1.0/src/gravity_cli/templates/deepagent/.gitignore +6 -0
- gravity_cli-0.1.0/src/gravity_cli/templates/deepagent/manifest.yaml +28 -0
- gravity_cli-0.1.0/src/gravity_cli/templates/deepagent/requirements.txt +4 -0
- gravity_cli-0.1.0/src/gravity_cli/templates/deepagent/src/agent.py +54 -0
- gravity_cli-0.1.0/src/gravity_cli/templates/minimal/.gitignore +6 -0
- gravity_cli-0.1.0/src/gravity_cli/templates/minimal/manifest.yaml +30 -0
- gravity_cli-0.1.0/src/gravity_cli/templates/minimal/requirements.txt +3 -0
- gravity_cli-0.1.0/src/gravity_cli/templates/minimal/src/agent.py +58 -0
- gravity_cli-0.1.0/src/gravity_cli/templates/structured/.gitignore +6 -0
- gravity_cli-0.1.0/src/gravity_cli/templates/structured/manifest.yaml +33 -0
- gravity_cli-0.1.0/src/gravity_cli/templates/structured/requirements.txt +3 -0
- gravity_cli-0.1.0/src/gravity_cli/templates/structured/src/agent.py +15 -0
- gravity_cli-0.1.0/src/gravity_cli/templates/structured/src/gateway.py +26 -0
- gravity_cli-0.1.0/src/gravity_cli/templates/structured/src/nodes/agent.py +20 -0
- gravity_cli-0.1.0/src/gravity_cli/templates/structured/src/prompts.py +6 -0
- gravity_cli-0.1.0/src/gravity_cli/templates/structured/src/state.py +10 -0
- gravity_cli-0.1.0/src/gravity_cli/timeline.py +254 -0
- gravity_cli-0.1.0/starter/manifest.yaml +50 -0
- gravity_cli-0.1.0/starter/requirements.txt +3 -0
- gravity_cli-0.1.0/starter/src/agent.py +89 -0
- gravity_cli-0.1.0/starter/src/summary.py +8 -0
- gravity_cli-0.1.0/starter/tests/test_summary.py +11 -0
- gravity_cli-0.1.0/tests/test_adapt.py +71 -0
- gravity_cli-0.1.0/tests/test_api.py +66 -0
- gravity_cli-0.1.0/tests/test_build_push.py +144 -0
- gravity_cli-0.1.0/tests/test_manifest_integration.py +130 -0
- gravity_cli-0.1.0/tests/test_run.py +344 -0
- gravity_cli-0.1.0/tests/test_templates.py +118 -0
- gravity_cli-0.1.0/tests/test_timeline.py +86 -0
- gravity_cli-0.1.0/tests/test_welcome.py +25 -0
- gravity_cli-0.1.0/uv.lock +699 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# Copy to ~/.gravity/.env (Windows: %USERPROFILE%\.gravity\.env) and fill in.
|
|
2
|
+
# Every gravity command loads that file. Never commit the filled-in copy.
|
|
3
|
+
# What each variable does: README.md, "Environment variables".
|
|
4
|
+
|
|
5
|
+
# --- gravity run / schedule / adapt -------------------------------------
|
|
6
|
+
# Your gateway dev token. Required to run an agent. A local gateway prints
|
|
7
|
+
# one at startup, or uses its LOCAL_DEV_TOKEN.
|
|
8
|
+
GRAVITY_DEV_TOKEN=
|
|
9
|
+
|
|
10
|
+
# The gateway's MCP endpoint. Leave unset for a local gateway on port 8000.
|
|
11
|
+
# GRAVITY_GATEWAY_URL=https://<hosted gateway host>/mcp
|
|
12
|
+
|
|
13
|
+
# The model endpoint. Leave unset: it follows the gateway (<gateway>/v1).
|
|
14
|
+
# GRAVITY_LLM_URL=
|
|
15
|
+
|
|
16
|
+
# --- push / tools / agents / runs / logs / whoami ------------------------
|
|
17
|
+
# The control plane. Leave unset for a local one at http://localhost:3010.
|
|
18
|
+
# GRAVITY_API_URL=https://<control plane host>
|
|
19
|
+
|
|
20
|
+
# Development only: the control plane's dev token, instead of `gravity login`.
|
|
21
|
+
# GRAVITY_API_TOKEN=
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
workflow_call: # release.yml runs the same checks before publishing
|
|
8
|
+
|
|
9
|
+
permissions:
|
|
10
|
+
contents: read
|
|
11
|
+
|
|
12
|
+
concurrency:
|
|
13
|
+
group: ci-${{ github.ref }}
|
|
14
|
+
cancel-in-progress: true
|
|
15
|
+
|
|
16
|
+
jobs:
|
|
17
|
+
lint:
|
|
18
|
+
runs-on: ubuntu-latest
|
|
19
|
+
steps:
|
|
20
|
+
- uses: actions/checkout@v4
|
|
21
|
+
- uses: astral-sh/setup-uv@v6
|
|
22
|
+
with:
|
|
23
|
+
enable-cache: true
|
|
24
|
+
- run: uv sync --locked # fails if uv.lock is out of date with pyproject.toml
|
|
25
|
+
- run: uv run ruff check .
|
|
26
|
+
|
|
27
|
+
test:
|
|
28
|
+
name: test (${{ matrix.os }}, py${{ matrix.python }})
|
|
29
|
+
runs-on: ${{ matrix.os }}
|
|
30
|
+
strategy:
|
|
31
|
+
fail-fast: false
|
|
32
|
+
matrix:
|
|
33
|
+
os: [ubuntu-latest, windows-latest]
|
|
34
|
+
python: ["3.12", "3.13"]
|
|
35
|
+
steps:
|
|
36
|
+
- uses: actions/checkout@v4
|
|
37
|
+
- uses: astral-sh/setup-uv@v6
|
|
38
|
+
with:
|
|
39
|
+
python-version: ${{ matrix.python }}
|
|
40
|
+
enable-cache: true
|
|
41
|
+
- run: uv sync --locked
|
|
42
|
+
- name: CLI tests
|
|
43
|
+
run: uv run pytest
|
|
44
|
+
- name: gravity-schema tests
|
|
45
|
+
working-directory: packages/gravity-schema
|
|
46
|
+
run: uv run pytest
|
|
47
|
+
|
|
48
|
+
package:
|
|
49
|
+
# Build both packages, install them into a clean environment the way a builder
|
|
50
|
+
# would, and use them: catches files missing from the wheel before PyPI does.
|
|
51
|
+
runs-on: ubuntu-latest
|
|
52
|
+
steps:
|
|
53
|
+
- uses: actions/checkout@v4
|
|
54
|
+
- uses: astral-sh/setup-uv@v6
|
|
55
|
+
- run: uv build --all-packages
|
|
56
|
+
- name: Smoke test the built wheels
|
|
57
|
+
env:
|
|
58
|
+
GRAVITY_CONFIG_DIR: ${{ runner.temp }}/gravity-config
|
|
59
|
+
run: |
|
|
60
|
+
uv venv "$RUNNER_TEMP/smoke"
|
|
61
|
+
uv pip install --python "$RUNNER_TEMP/smoke/bin/python" --find-links dist gravity-cli
|
|
62
|
+
cd "$RUNNER_TEMP"
|
|
63
|
+
"$RUNNER_TEMP/smoke/bin/gravity" --help > /dev/null
|
|
64
|
+
"$RUNNER_TEMP/smoke/bin/gravity" init smoke-agent
|
|
65
|
+
"$RUNNER_TEMP/smoke/bin/gravity" validate smoke-agent
|
|
66
|
+
- uses: actions/upload-artifact@v4
|
|
67
|
+
with:
|
|
68
|
+
name: ci-dist
|
|
69
|
+
path: dist/
|
|
70
|
+
if-no-files-found: error
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
name: Release
|
|
2
|
+
|
|
3
|
+
# Push a tag to release both packages at the same version:
|
|
4
|
+
# v0.1.0rc1 -> TestPyPI (any tag containing "rc")
|
|
5
|
+
# v0.1.0 -> PyPI
|
|
6
|
+
# Publishing uses PyPI trusted publishing: no API token is stored anywhere.
|
|
7
|
+
|
|
8
|
+
on:
|
|
9
|
+
push:
|
|
10
|
+
tags: ["v*"]
|
|
11
|
+
|
|
12
|
+
permissions:
|
|
13
|
+
contents: read
|
|
14
|
+
|
|
15
|
+
jobs:
|
|
16
|
+
ci:
|
|
17
|
+
uses: ./.github/workflows/ci.yml
|
|
18
|
+
|
|
19
|
+
build:
|
|
20
|
+
needs: ci
|
|
21
|
+
runs-on: ubuntu-latest
|
|
22
|
+
steps:
|
|
23
|
+
- uses: actions/checkout@v4
|
|
24
|
+
- uses: astral-sh/setup-uv@v6
|
|
25
|
+
- name: Tag matches both package versions and the schema pin
|
|
26
|
+
run: |
|
|
27
|
+
python3 - <<'EOF'
|
|
28
|
+
import os, sys, tomllib
|
|
29
|
+
tag = os.environ["GITHUB_REF_NAME"].removeprefix("v")
|
|
30
|
+
cli = tomllib.load(open("pyproject.toml", "rb"))["project"]
|
|
31
|
+
schema = tomllib.load(open("packages/gravity-schema/pyproject.toml", "rb"))["project"]
|
|
32
|
+
pin = next(d for d in cli["dependencies"] if d.startswith("gravity-schema"))
|
|
33
|
+
problems = [
|
|
34
|
+
f"{name} is {version}, the tag says {tag}"
|
|
35
|
+
for name, version in [("gravity-cli", cli["version"]), ("gravity-schema", schema["version"])]
|
|
36
|
+
if version != tag
|
|
37
|
+
]
|
|
38
|
+
if pin != f"gravity-schema=={tag}":
|
|
39
|
+
problems.append(f"gravity-cli depends on {pin!r}, expected gravity-schema=={tag}")
|
|
40
|
+
for p in problems:
|
|
41
|
+
print(f"::error::{p}")
|
|
42
|
+
sys.exit(1 if problems else 0)
|
|
43
|
+
EOF
|
|
44
|
+
- run: uv build --all-packages
|
|
45
|
+
- uses: actions/upload-artifact@v4
|
|
46
|
+
with:
|
|
47
|
+
name: dist
|
|
48
|
+
path: dist/
|
|
49
|
+
if-no-files-found: error
|
|
50
|
+
|
|
51
|
+
# One job per package: a pending trusted publisher can create only one new project per
|
|
52
|
+
# login, so each package logs in on its own, under its own environment.
|
|
53
|
+
testpypi:
|
|
54
|
+
if: contains(github.ref_name, 'rc')
|
|
55
|
+
needs: build
|
|
56
|
+
strategy:
|
|
57
|
+
matrix:
|
|
58
|
+
include:
|
|
59
|
+
- { package: gravity_schema, environment: testpypi-schema }
|
|
60
|
+
- { package: gravity_cli, environment: testpypi }
|
|
61
|
+
runs-on: ubuntu-latest
|
|
62
|
+
environment: ${{ matrix.environment }}
|
|
63
|
+
permissions:
|
|
64
|
+
id-token: write # trusted publishing
|
|
65
|
+
steps:
|
|
66
|
+
- uses: actions/download-artifact@v4
|
|
67
|
+
with:
|
|
68
|
+
name: dist
|
|
69
|
+
path: dist/
|
|
70
|
+
- run: mkdir upload && mv dist/${{ matrix.package }}-* upload/
|
|
71
|
+
- uses: pypa/gh-action-pypi-publish@release/v1
|
|
72
|
+
with:
|
|
73
|
+
repository-url: https://test.pypi.org/legacy/
|
|
74
|
+
packages-dir: upload/
|
|
75
|
+
|
|
76
|
+
pypi:
|
|
77
|
+
if: ${{ !contains(github.ref_name, 'rc') }}
|
|
78
|
+
needs: build
|
|
79
|
+
strategy:
|
|
80
|
+
matrix:
|
|
81
|
+
include:
|
|
82
|
+
- { package: gravity_schema, environment: pypi-schema }
|
|
83
|
+
- { package: gravity_cli, environment: pypi }
|
|
84
|
+
runs-on: ubuntu-latest
|
|
85
|
+
environment: ${{ matrix.environment }}
|
|
86
|
+
permissions:
|
|
87
|
+
id-token: write # trusted publishing
|
|
88
|
+
steps:
|
|
89
|
+
- uses: actions/download-artifact@v4
|
|
90
|
+
with:
|
|
91
|
+
name: dist
|
|
92
|
+
path: dist/
|
|
93
|
+
- run: mkdir upload && mv dist/${{ matrix.package }}-* upload/
|
|
94
|
+
- uses: pypa/gh-action-pypi-publish@release/v1
|
|
95
|
+
with:
|
|
96
|
+
packages-dir: upload/
|
|
97
|
+
|
|
98
|
+
github-release:
|
|
99
|
+
needs: pypi
|
|
100
|
+
runs-on: ubuntu-latest
|
|
101
|
+
permissions:
|
|
102
|
+
contents: write
|
|
103
|
+
steps:
|
|
104
|
+
- uses: actions/download-artifact@v4
|
|
105
|
+
with:
|
|
106
|
+
name: dist
|
|
107
|
+
path: dist/
|
|
108
|
+
- env:
|
|
109
|
+
GH_TOKEN: ${{ github.token }}
|
|
110
|
+
run: gh release create "$GITHUB_REF_NAME" dist/* --repo "$GITHUB_REPOSITORY" --generate-notes
|
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
# Agent submission standard — `contract: v1`
|
|
2
|
+
|
|
3
|
+
What a valid agent directory looks like, exactly what ships, and which check
|
|
4
|
+
catches which mistake. Follow this and `gravity build` passes, the deploy
|
|
5
|
+
succeeds, and the runtime loads it. Deviate and the failure happens at a stage
|
|
6
|
+
you can't see — after upload, on our infrastructure.
|
|
7
|
+
|
|
8
|
+
Start from `gravity init <name>`; every template already conforms
|
|
9
|
+
(`minimal`, `structured`, `deepagent`, `chat`).
|
|
10
|
+
|
|
11
|
+
The manifest rules below are enforced by `python/gravity-schema`, the one
|
|
12
|
+
model shared by the CLI, the runner and the server. Where this document and
|
|
13
|
+
that package disagree, the package wins and this document has a bug.
|
|
14
|
+
|
|
15
|
+
## 1. Layout
|
|
16
|
+
|
|
17
|
+
```
|
|
18
|
+
my-agent/
|
|
19
|
+
├── manifest.yaml required the contract (§2)
|
|
20
|
+
├── requirements.txt required your direct deps — may be empty, must exist
|
|
21
|
+
├── requirements.lock generated by `gravity validate`/`build`, for the TARGET platform (§5)
|
|
22
|
+
├── src/ required all your code; the entrypoint lives here
|
|
23
|
+
│ └── agent.py `graph` is defined here (§3)
|
|
24
|
+
└── tests/ optional shipped if present
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
**Only these are bundled.** `gravity build` zips exactly `manifest.yaml`,
|
|
28
|
+
`requirements.txt`, `requirements.lock`, and the contents of `src/` and
|
|
29
|
+
`tests/` (minus `__pycache__`). Anything else — `.venv/`, `build/`, notebooks,
|
|
30
|
+
a `data/` folder — is not shipped, and code that reads it will fail at runtime.
|
|
31
|
+
|
|
32
|
+
## 2. `manifest.yaml`
|
|
33
|
+
|
|
34
|
+
```yaml
|
|
35
|
+
contract: v1 # only value accepted
|
|
36
|
+
name: my-agent # lowercase slug, ≤64 chars — appears in URLs
|
|
37
|
+
version: 1.0.0 # strict semver; bump to republish — a version can't be overwritten
|
|
38
|
+
description: One line. # ≤500 chars
|
|
39
|
+
|
|
40
|
+
runtime:
|
|
41
|
+
framework: langgraph # only value accepted
|
|
42
|
+
python: "3.13" # 3.10–3.13; the lockfile is compiled for it
|
|
43
|
+
entrypoint: src/agent.py:graph # file:object — see §3
|
|
44
|
+
timeout_seconds: 300 # 10–900
|
|
45
|
+
|
|
46
|
+
dependencies:
|
|
47
|
+
lockfile: requirements.lock # leave as-is
|
|
48
|
+
|
|
49
|
+
inputs: # becomes the consumer's form; arrives as state["inputs"]
|
|
50
|
+
- name: query # the key in state["inputs"]
|
|
51
|
+
type: text # text | textarea | number | boolean | static-dropdown | email | url | date | file
|
|
52
|
+
label: "What should I do?" # what the consumer sees
|
|
53
|
+
required: true
|
|
54
|
+
# options: [a, b] # static-dropdown only
|
|
55
|
+
# accept: [pdf, csv] # file only — extensions without the dot
|
|
56
|
+
# default: ... # never on a file input
|
|
57
|
+
|
|
58
|
+
permissions:
|
|
59
|
+
tools: # every tool your code calls, by Gravity name (§4), ≤50
|
|
60
|
+
- example.echo
|
|
61
|
+
models:
|
|
62
|
+
tier: standard # standard | premium
|
|
63
|
+
|
|
64
|
+
resources:
|
|
65
|
+
memory_mb: 512 # 128–4096; a request the platform clamps
|
|
66
|
+
|
|
67
|
+
triggers: # optional — what starts a run
|
|
68
|
+
manual: true # default
|
|
69
|
+
# schedule:
|
|
70
|
+
# cron: "0 9 * * 1" # 5 fields: digits, * / , - only (no @daily, no MON)
|
|
71
|
+
# consumer_can_change: true
|
|
72
|
+
|
|
73
|
+
approvals: # optional — tools that pause the run for the consumer's yes
|
|
74
|
+
required_for: [] # each must also be in permissions.tools
|
|
75
|
+
|
|
76
|
+
# state: # optional — memory across runs, per consumer
|
|
77
|
+
# max_kb: 256 # 1–256; leave out and the agent gets none
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
**`approvals` is enforced by the gateway.** A listed tool is held until a
|
|
81
|
+
human answers: in `gravity run` you're asked (`Approve? [y/N]`, default no;
|
|
82
|
+
no answer is a no); in the product it's the consumer's approval inbox. Not
|
|
83
|
+
approved, the call returns `APPROVAL_REJECTED` (or `APPROVAL_TIMEOUT` after 4
|
|
84
|
+
minutes) and never runs — handle that and fall back, as `meeting-notes`
|
|
85
|
+
keeps the recap as a draft. Your code can't skip the question: the gateway
|
|
86
|
+
holds the call, not the agent. Hosted, with tools from `gravity.tools()`, the
|
|
87
|
+
run pauses durably instead of holding the call (§8).
|
|
88
|
+
|
|
89
|
+
`triggers` and `state` are part of the contract so a published agent never
|
|
90
|
+
has to change to use them. `gravity schedule` fires `triggers.schedule` locally, and an agent
|
|
91
|
+
that declares `state` gets `state["memory"]` from its last finished run (saved only when a run
|
|
92
|
+
finishes, never over `max_kb`); hosted, EventBridge fires the schedule and the gateway keeps the memory. An agent
|
|
93
|
+
that nothing can start (`manual: false` with no schedule) is rejected.
|
|
94
|
+
|
|
95
|
+
`slots` (optional) declares swappable adapters: `slots.<slot>.adapters.<name>: [tools]`, one file
|
|
96
|
+
per adapter at `src/adapters/<slot>/<name>.py`. A run is granted `permissions.tools` plus only the
|
|
97
|
+
picked adapter's tools; the pick arrives as `state["inputs"][<slot>]`. See `BUILDER_GUIDE.md` §9.
|
|
98
|
+
|
|
99
|
+
The same schema is validated twice — by `gravity validate` on your machine
|
|
100
|
+
and again by the server on push — because an attacker won't use the CLI.
|
|
101
|
+
|
|
102
|
+
## 3. The entrypoint
|
|
103
|
+
|
|
104
|
+
`runtime.entrypoint` names a file under `src/` and a module-level object in
|
|
105
|
+
it. The runtime accepts two forms:
|
|
106
|
+
|
|
107
|
+
```python
|
|
108
|
+
async def graph(): # ✓ recommended — an async factory
|
|
109
|
+
...
|
|
110
|
+
return workflow.compile()
|
|
111
|
+
|
|
112
|
+
graph = asyncio.run(_build()) # ✓ a graph compiled at import
|
|
113
|
+
graph = _build() # ✓ also fine — a coroutine is awaited
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
**Prefer the factory**, which is what the templates do. The runner calls it
|
|
117
|
+
fresh for every run, on that run's own event loop, after the run's env vars
|
|
118
|
+
are set. Read the env vars *inside* it: helper modules under `src/` are
|
|
119
|
+
imported once and cached, so an env var read at their top level can outlive
|
|
120
|
+
the run it belonged to.
|
|
121
|
+
|
|
122
|
+
A graph compiled at import also works: the platform imports your module on a
|
|
123
|
+
worker thread precisely so `asyncio.run()` gets its own event loop. If you
|
|
124
|
+
ever see *"asyncio.run() cannot be called from a running event loop,"* the
|
|
125
|
+
runner is too old, not your agent.
|
|
126
|
+
|
|
127
|
+
**State.** The runtime invokes `graph.astream({"inputs": {...}}, stream_mode="updates")`
|
|
128
|
+
— the keyword is passed only when your `astream` accepts it, so a hand-rolled
|
|
129
|
+
object with a plain `astream(state)` works too (accepting `**kwargs` is the
|
|
130
|
+
safe habit). The dict is the
|
|
131
|
+
dict is the consumer's form answers keyed by `inputs[].name`. Your state
|
|
132
|
+
therefore needs:
|
|
133
|
+
|
|
134
|
+
```python
|
|
135
|
+
class State(TypedDict):
|
|
136
|
+
inputs: dict # required — the consumer's form answers
|
|
137
|
+
result: Any # required in the FINAL state — what the consumer gets
|
|
138
|
+
... # anything else is yours
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
**Result.** When the graph finishes, `state["result"]` is what the platform
|
|
142
|
+
returns to the consumer. Any JSON-serialisable value — a string, a dict. A
|
|
143
|
+
graph that finishes without setting `result` returns nothing.
|
|
144
|
+
|
|
145
|
+
**Conversation.** Declaring a `messages` key in your state opts the agent
|
|
146
|
+
into conversation: the platform passes the transcript in on every turn and
|
|
147
|
+
stores what comes back. Don't declare it in a one-shot agent — see the
|
|
148
|
+
`chat` template.
|
|
149
|
+
|
|
150
|
+
Optional convention the platform surfaces if present: `source: "live" | "stub"`
|
|
151
|
+
— whether real tool calls happened or a mock answered.
|
|
152
|
+
|
|
153
|
+
## 4. Imports, tools, models
|
|
154
|
+
|
|
155
|
+
**Imports: absolute, rooted at `src/`.** The runtime puts `src/` on
|
|
156
|
+
`sys.path` and loads your entrypoint as a top-level module. So:
|
|
157
|
+
|
|
158
|
+
```python
|
|
159
|
+
from tools.scoring import score # ✓ src/tools/scoring.py
|
|
160
|
+
from nodes.agent import build # ✓ src/nodes/agent.py
|
|
161
|
+
from .nodes.agent import build # ✗ relative imports don't resolve
|
|
162
|
+
from src.nodes.agent import build # ✗ src/ is the root, not a package
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
`gravity run` loads the same way, so a local run is a faithful preview.
|
|
166
|
+
|
|
167
|
+
**Tools** come only through the gateway, over MCP, and only the ones in
|
|
168
|
+
`permissions.tools`:
|
|
169
|
+
|
|
170
|
+
```python
|
|
171
|
+
client = MultiServerMCPClient({"gravity": {
|
|
172
|
+
"transport": "streamable_http",
|
|
173
|
+
"url": os.environ["GRAVITY_GATEWAY_URL"],
|
|
174
|
+
"headers": {"Authorization": f"Bearer {os.environ['GRAVITY_RUN_TOKEN']}"},
|
|
175
|
+
}})
|
|
176
|
+
tools = await client.get_tools() # exactly permissions.tools, nothing else
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
Tool names are `<app>.<action>`, lowercase, from `gravity tools` — Gravity's
|
|
180
|
+
names, never a vendor's (`gmail.read`, not `GMAIL_FETCH_EMAILS`). The gateway
|
|
181
|
+
hands them to your model with `_` for `.` (`gmail_read`), because
|
|
182
|
+
OpenAI-family models reject dots in function names. The sandbox allows no
|
|
183
|
+
other egress — `requests.get(...)` to anywhere times out. Declare what you
|
|
184
|
+
need; don't reach around it.
|
|
185
|
+
|
|
186
|
+
**Models** go through the LLM gateway with the same token:
|
|
187
|
+
|
|
188
|
+
```python
|
|
189
|
+
model = ChatOpenAI(model="openai/gpt-6-luna",
|
|
190
|
+
base_url=os.environ["GRAVITY_LLM_URL"],
|
|
191
|
+
api_key=os.environ["GRAVITY_RUN_TOKEN"])
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
Those three env vars — `GRAVITY_GATEWAY_URL`, `GRAVITY_LLM_URL`,
|
|
195
|
+
`GRAVITY_RUN_TOKEN` — are the entire Gravity surface, plus the `gravity`
|
|
196
|
+
package for tools on the platform (§8). No `OPENAI_API_KEY`: locally and in
|
|
197
|
+
production, models go through the gateway.
|
|
198
|
+
|
|
199
|
+
## 5. Dependencies
|
|
200
|
+
|
|
201
|
+
List direct deps in `requirements.txt` with version ranges. `gravity validate`
|
|
202
|
+
compiles `requirements.lock` **for the target** — `aarch64-manylinux2014`,
|
|
203
|
+
Python `runtime.python`, binary wheels only — not for your laptop. The deploy
|
|
204
|
+
installs from the lock and nothing else.
|
|
205
|
+
|
|
206
|
+
Don't hand-edit the lock. Don't compile it yourself; a native compile on
|
|
207
|
+
Windows or macOS pins the wrong wheels and only fails at deploy.
|
|
208
|
+
|
|
209
|
+
## 6. Local runs
|
|
210
|
+
|
|
211
|
+
`gravity run --input k=v` runs your graph in your own venv against a real
|
|
212
|
+
gateway, exactly as production does: tools over `GRAVITY_GATEWAY_URL`,
|
|
213
|
+
models over `GRAVITY_LLM_URL`, both with `GRAVITY_RUN_TOKEN`. You set a
|
|
214
|
+
different token once, `GRAVITY_DEV_TOKEN`, in `~/.gravity/.env`: each run
|
|
215
|
+
trades it for a run token scoped to your `permissions.tools`, so an
|
|
216
|
+
undeclared tool is refused locally exactly as it will be when hosted. Your
|
|
217
|
+
agent only ever sees the run token. `GRAVITY_GATEWAY_URL` defaults to
|
|
218
|
+
`http://127.0.0.1:8000/mcp` and `GRAVITY_LLM_URL` to the same host's `/v1`.
|
|
219
|
+
|
|
220
|
+
## 7. What's checked where
|
|
221
|
+
|
|
222
|
+
| Stage | Catches |
|
|
223
|
+
|---|---|
|
|
224
|
+
| `gravity validate` / `build` (local) | manifest schema · entrypoint file + object exist (static) · `requirements.txt` present · lockfile compiles for aarch64 · undeclared network imports (warning) |
|
|
225
|
+
| `gravity push` (server) | manifest re-validated with the same rules · `permissions.tools` all exist in the catalog · version not already published |
|
|
226
|
+
| deploy | every pin in the lock installs on ARM64 |
|
|
227
|
+
| first run | module imports · `graph` resolves to something with `astream()` · state has `inputs` · final state has `result` |
|
|
228
|
+
|
|
229
|
+
The further right a failure lands, the longer you wait to see it. `build`
|
|
230
|
+
passing means everything in the first column is fine; it does not run your
|
|
231
|
+
code. `gravity run` does.
|
|
232
|
+
|
|
233
|
+
## 8. On the platform: `import gravity`
|
|
234
|
+
|
|
235
|
+
Hosted runs are durable: they pause for approvals, survive a lost session,
|
|
236
|
+
and remember the thread between runs. The platform does that around your
|
|
237
|
+
graph. `gravity` is preinstalled in the image; don't put it in
|
|
238
|
+
`requirements.txt`.
|
|
239
|
+
|
|
240
|
+
**Tools: `await gravity.tools()`** instead of your own `MultiServerMCPClient`.
|
|
241
|
+
The same LangChain tools under the same names (`gmail_read`), from the same env
|
|
242
|
+
vars. Off the platform (`gravity run`, tests) it is exactly the client in §4,
|
|
243
|
+
but only if your venv has it: it stays out of `requirements.txt` and `gravity run`
|
|
244
|
+
doesn't add it, so install it yourself (`uv pip install -e <repo>/code-first/python/gravity-sdk`)
|
|
245
|
+
or `gravity run` fails with `No module named 'gravity'`.
|
|
246
|
+
On the platform, each tool in `approvals.required_for` is wrapped: calling it
|
|
247
|
+
checkpoints the run and stops it until the consumer decides, with no
|
|
248
|
+
connection held open. Approved, the call runs with the arguments the consumer
|
|
249
|
+
saw. Declined, it returns
|
|
250
|
+
`APPROVAL_REJECTED: '<tool>' was not approved, so it did not run`, the same
|
|
251
|
+
text as today, so keep your fallback.
|
|
252
|
+
|
|
253
|
+
```python
|
|
254
|
+
tools = {t.name: t for t in await gravity.tools()}
|
|
255
|
+
await tools["slack_send"].ainvoke({"channel": cid, "markdown_text": text})
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
When the run resumes, the node that made the call **runs again from its
|
|
259
|
+
start**: everything before the call happens twice.
|
|
260
|
+
|
|
261
|
+
- Put a gated call at the end of its own node, after nothing expensive or side-effecting.
|
|
262
|
+
- Several gated calls in one node work, each approved in turn. On each resume the
|
|
263
|
+
earlier calls are repeated, and the gateway answers a repeat with the stored result
|
|
264
|
+
instead of doing it again, so each side effect still happens once. Work *between*
|
|
265
|
+
the calls repeats too, so keep it cheap.
|
|
266
|
+
- Two approvals at once: make them parallel nodes. The consumer sees both together.
|
|
267
|
+
- Any `interrupt()` of your own fails the run with `UNSUPPORTED_INTERRUPT` (for now).
|
|
268
|
+
|
|
269
|
+
**Persistence is the platform's.** Compile with no checkpointer and no store
|
|
270
|
+
(`workflow.compile()`); the platform injects its own, and one you pass is
|
|
271
|
+
replaced. State is kept per thread. A key the next run doesn't send keeps its
|
|
272
|
+
value (a monitor's `seen`, a draft). For memory shared by one consumer's
|
|
273
|
+
threads, use `langgraph.config.get_store()` (`aget` / `aput` / `asearch` by
|
|
274
|
+
namespace and filter; no semantic search).
|
|
275
|
+
|
|
276
|
+
**Conversation.** `messages` must use the `add_messages` reducer:
|
|
277
|
+
`messages: Annotated[list, add_messages]`. Each turn the platform sends only
|
|
278
|
+
the new user turn. The history comes from the thread's checkpoint, and a
|
|
279
|
+
plain `list` would replace it with that one message.
|
|
280
|
+
|
|
281
|
+
**`kind` and `outputs`.**
|
|
282
|
+
|
|
283
|
+
```yaml
|
|
284
|
+
kind: transform # transform | interactive (default) | monitor
|
|
285
|
+
outputs: # what the consumer may ask the result to become
|
|
286
|
+
deck: {renderer: pptx, label: "Pitch deck"}
|
|
287
|
+
readout: {renderer: docx, label: "Read-out document"}
|
|
288
|
+
summary: {renderer: markdown, label: "Summary", destinations: [download, gmail.draft, gmail.send]}
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
- `transform`: all input at the start, the result at the end. The platform
|
|
292
|
+
delivers it, so the agent needs no write tools.
|
|
293
|
+
- `interactive`: calls tools mid-flow and may converse over several turns.
|
|
294
|
+
- `monitor`: runs on a schedule or a webhook, on one thread, remembering the
|
|
295
|
+
last run. It needs `triggers.schedule` or a webhook.
|
|
296
|
+
|
|
297
|
+
`destinations` is where an output may go: `download` (the default),
|
|
298
|
+
`gmail.draft` or `gmail.send`. With `gmail.send` the consumer decides when:
|
|
299
|
+
on the Start or Schedule form, or by asking in chat and confirming. A
|
|
300
|
+
scheduled agent can email every run after that one confirm. The email goes
|
|
301
|
+
to one address, with the output's content in the message body and no
|
|
302
|
+
attachment; a thread sends at most 200 a day. The platform sends it, not the
|
|
303
|
+
agent, so the agent needs no Gmail tool and no approval for it.
|
|
304
|
+
|
|
305
|
+
Renderers: `markdown`, `docx`, `pptx`, `rows` (the first table, as CSV). Each
|
|
306
|
+
consumer picks outputs and destinations per run. The agent writes one
|
|
307
|
+
**corpus** into its final state, and every output is rendered from it:
|
|
308
|
+
|
|
309
|
+
```python
|
|
310
|
+
corpus = gravity.Corpus(
|
|
311
|
+
title="Acme: seed round", subtitle="2026", summary="**Why now**: ...",
|
|
312
|
+
sections=[gravity.Section(
|
|
313
|
+
heading="Market", body="markdown", bullets=["..."],
|
|
314
|
+
facts=[gravity.Fact(label="TAM", value="$18B", source="s1")], # value: text or a number
|
|
315
|
+
table=gravity.Table(columns=["Segment", "Teams"], rows=[["Founders", "1.2M"]]),
|
|
316
|
+
notes="speaker notes / read-out text",
|
|
317
|
+
sections=[gravity.Section(heading="one level of nesting, same shape")],
|
|
318
|
+
)],
|
|
319
|
+
sources=[gravity.Source(id="s1", title="Report", url="https://example.com")],
|
|
320
|
+
)
|
|
321
|
+
return {**corpus.to_state(), "result": {"title": corpus.title}}
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
A plain dict of the same shape works too. The models catch a typo when the
|
|
325
|
+
corpus is built rather than when it's rendered. Set `result` as well: it's
|
|
326
|
+
what the run returns.
|
|
327
|
+
|
|
328
|
+
## 9. Checklist
|
|
329
|
+
|
|
330
|
+
- [ ] `manifest.yaml` with `contract: v1`, every tool you call under `permissions.tools`
|
|
331
|
+
- [ ] `requirements.txt` exists (empty is fine)
|
|
332
|
+
- [ ] `src/agent.py` defines module-level `graph`: an `async def` factory (recommended) or a compiled graph
|
|
333
|
+
- [ ] env vars read inside `graph()`, not at module level
|
|
334
|
+
- [ ] state has `inputs`; final state sets `result`; `messages` only if the agent is conversational, with `add_messages`
|
|
335
|
+
- [ ] tools from `await gravity.tools()`; compiled with no checkpointer; nothing expensive before a gated call (§8)
|
|
336
|
+
- [ ] all imports under `src/` are absolute from `src/`
|
|
337
|
+
- [ ] no direct network calls — tools via the gateway, models via the LLM URL
|
|
338
|
+
- [ ] nothing your code needs lives outside `src/` or `tests/`
|
|
339
|
+
- [ ] `gravity build` passes; `gravity run` produces a `result`
|