gravity-cli 0.7.5__tar.gz → 0.8.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.7.5 → gravity_cli-0.8.0}/PKG-INFO +2 -2
- {gravity_cli-0.7.5 → gravity_cli-0.8.0}/packages/gravity-schema/gravity_schema/catalog.yaml +54 -0
- {gravity_cli-0.7.5 → gravity_cli-0.8.0}/packages/gravity-schema/gravity_schema/manifest.py +42 -0
- {gravity_cli-0.7.5 → gravity_cli-0.8.0}/packages/gravity-schema/gravity_schema/rules.py +15 -0
- {gravity_cli-0.7.5 → gravity_cli-0.8.0}/packages/gravity-schema/pyproject.toml +1 -1
- {gravity_cli-0.7.5 → gravity_cli-0.8.0}/packages/gravity-schema/schema/manifest.schema.json +98 -0
- gravity_cli-0.8.0/packages/gravity-schema/tests/test_slots.py +104 -0
- {gravity_cli-0.7.5 → gravity_cli-0.8.0}/pyproject.toml +2 -2
- {gravity_cli-0.7.5 → gravity_cli-0.8.0}/src/gravity_cli/BUILDER_GUIDE.md +30 -1
- {gravity_cli-0.7.5 → gravity_cli-0.8.0}/src/gravity_cli/CHANGELOG.md +10 -0
- {gravity_cli-0.7.5 → gravity_cli-0.8.0}/src/gravity_cli/docs/Gravity.agent.md +28 -3
- {gravity_cli-0.7.5 → gravity_cli-0.8.0}/uv.lock +2 -2
- gravity_cli-0.7.5/packages/gravity-schema/tests/test_slots.py +0 -52
- {gravity_cli-0.7.5 → gravity_cli-0.8.0}/.env.example +0 -0
- {gravity_cli-0.7.5 → gravity_cli-0.8.0}/.github/workflows/ci.yml +0 -0
- {gravity_cli-0.7.5 → gravity_cli-0.8.0}/.github/workflows/release.yml +0 -0
- {gravity_cli-0.7.5 → gravity_cli-0.8.0}/.gitignore +0 -0
- {gravity_cli-0.7.5 → gravity_cli-0.8.0}/AGENT_STANDARD.md +0 -0
- {gravity_cli-0.7.5 → gravity_cli-0.8.0}/LICENSE +0 -0
- {gravity_cli-0.7.5 → gravity_cli-0.8.0}/NOTICE +0 -0
- {gravity_cli-0.7.5 → gravity_cli-0.8.0}/README.md +0 -0
- {gravity_cli-0.7.5 → gravity_cli-0.8.0}/RELEASING.md +0 -0
- {gravity_cli-0.7.5 → gravity_cli-0.8.0}/TEMPLATES.md +0 -0
- {gravity_cli-0.7.5 → gravity_cli-0.8.0}/packages/gravity-schema/LICENSE +0 -0
- {gravity_cli-0.7.5 → gravity_cli-0.8.0}/packages/gravity-schema/NOTICE +0 -0
- {gravity_cli-0.7.5 → gravity_cli-0.8.0}/packages/gravity-schema/README.md +0 -0
- {gravity_cli-0.7.5 → gravity_cli-0.8.0}/packages/gravity-schema/gravity_schema/__init__.py +0 -0
- {gravity_cli-0.7.5 → gravity_cli-0.8.0}/packages/gravity-schema/gravity_schema/__main__.py +0 -0
- {gravity_cli-0.7.5 → gravity_cli-0.8.0}/packages/gravity-schema/gravity_schema/catalog.py +0 -0
- {gravity_cli-0.7.5 → gravity_cli-0.8.0}/packages/gravity-schema/gravity_schema/catalog_imported.yaml +0 -0
- {gravity_cli-0.7.5 → gravity_cli-0.8.0}/packages/gravity-schema/gravity_schema/contract.py +0 -0
- {gravity_cli-0.7.5 → gravity_cli-0.8.0}/packages/gravity-schema/gravity_schema/cron.py +0 -0
- {gravity_cli-0.7.5 → gravity_cli-0.8.0}/packages/gravity-schema/gravity_schema/errors.py +0 -0
- {gravity_cli-0.7.5 → gravity_cli-0.8.0}/packages/gravity-schema/gravity_schema/export.py +0 -0
- {gravity_cli-0.7.5 → gravity_cli-0.8.0}/packages/gravity-schema/gravity_schema/gen_catalog.py +0 -0
- {gravity_cli-0.7.5 → gravity_cli-0.8.0}/packages/gravity-schema/gravity_schema/rules_cases.json +0 -0
- {gravity_cli-0.7.5 → gravity_cli-0.8.0}/packages/gravity-schema/gravity_schema/source_scan.py +0 -0
- {gravity_cli-0.7.5 → gravity_cli-0.8.0}/packages/gravity-schema/gravity_schema/validation.py +0 -0
- {gravity_cli-0.7.5 → gravity_cli-0.8.0}/packages/gravity-schema/tests/conftest.py +0 -0
- {gravity_cli-0.7.5 → gravity_cli-0.8.0}/packages/gravity-schema/tests/test_accounts.py +0 -0
- {gravity_cli-0.7.5 → gravity_cli-0.8.0}/packages/gravity-schema/tests/test_actions_memory.py +0 -0
- {gravity_cli-0.7.5 → gravity_cli-0.8.0}/packages/gravity-schema/tests/test_catalog_parity.py +0 -0
- {gravity_cli-0.7.5 → gravity_cli-0.8.0}/packages/gravity-schema/tests/test_cron.py +0 -0
- {gravity_cli-0.7.5 → gravity_cli-0.8.0}/packages/gravity-schema/tests/test_errors.py +0 -0
- {gravity_cli-0.7.5 → gravity_cli-0.8.0}/packages/gravity-schema/tests/test_json_schema.py +0 -0
- {gravity_cli-0.7.5 → gravity_cli-0.8.0}/packages/gravity-schema/tests/test_kind_outputs.py +0 -0
- {gravity_cli-0.7.5 → gravity_cli-0.8.0}/packages/gravity-schema/tests/test_manifest.py +0 -0
- {gravity_cli-0.7.5 → gravity_cli-0.8.0}/packages/gravity-schema/tests/test_rules.py +0 -0
- {gravity_cli-0.7.5 → gravity_cli-0.8.0}/packages/gravity-schema/tests/test_source_scan.py +0 -0
- {gravity_cli-0.7.5 → gravity_cli-0.8.0}/packages/gravity-schema/tests/test_validation.py +0 -0
- {gravity_cli-0.7.5 → gravity_cli-0.8.0}/src/gravity_cli/__init__.py +0 -0
- {gravity_cli-0.7.5 → gravity_cli-0.8.0}/src/gravity_cli/__main__.py +0 -0
- {gravity_cli-0.7.5 → gravity_cli-0.8.0}/src/gravity_cli/_runner_script.py +0 -0
- {gravity_cli-0.7.5 → gravity_cli-0.8.0}/src/gravity_cli/api.py +0 -0
- {gravity_cli-0.7.5 → gravity_cli-0.8.0}/src/gravity_cli/changelog.py +0 -0
- {gravity_cli-0.7.5 → gravity_cli-0.8.0}/src/gravity_cli/cli.py +0 -0
- {gravity_cli-0.7.5 → gravity_cli-0.8.0}/src/gravity_cli/commands/__init__.py +0 -0
- {gravity_cli-0.7.5 → gravity_cli-0.8.0}/src/gravity_cli/commands/adapt.py +0 -0
- {gravity_cli-0.7.5 → gravity_cli-0.8.0}/src/gravity_cli/commands/build.py +0 -0
- {gravity_cli-0.7.5 → gravity_cli-0.8.0}/src/gravity_cli/commands/changelog.py +0 -0
- {gravity_cli-0.7.5 → gravity_cli-0.8.0}/src/gravity_cli/commands/connect.py +0 -0
- {gravity_cli-0.7.5 → gravity_cli-0.8.0}/src/gravity_cli/commands/dev.py +0 -0
- {gravity_cli-0.7.5 → gravity_cli-0.8.0}/src/gravity_cli/commands/docs.py +0 -0
- {gravity_cli-0.7.5 → gravity_cli-0.8.0}/src/gravity_cli/commands/guide.py +0 -0
- {gravity_cli-0.7.5 → gravity_cli-0.8.0}/src/gravity_cli/commands/init.py +0 -0
- {gravity_cli-0.7.5 → gravity_cli-0.8.0}/src/gravity_cli/commands/login.py +0 -0
- {gravity_cli-0.7.5 → gravity_cli-0.8.0}/src/gravity_cli/commands/memory.py +0 -0
- {gravity_cli-0.7.5 → gravity_cli-0.8.0}/src/gravity_cli/commands/push.py +0 -0
- {gravity_cli-0.7.5 → gravity_cli-0.8.0}/src/gravity_cli/commands/run.py +0 -0
- {gravity_cli-0.7.5 → gravity_cli-0.8.0}/src/gravity_cli/commands/runs.py +0 -0
- {gravity_cli-0.7.5 → gravity_cli-0.8.0}/src/gravity_cli/commands/schedule.py +0 -0
- {gravity_cli-0.7.5 → gravity_cli-0.8.0}/src/gravity_cli/commands/tools.py +0 -0
- {gravity_cli-0.7.5 → gravity_cli-0.8.0}/src/gravity_cli/commands/update.py +0 -0
- {gravity_cli-0.7.5 → gravity_cli-0.8.0}/src/gravity_cli/commands/validate.py +0 -0
- {gravity_cli-0.7.5 → gravity_cli-0.8.0}/src/gravity_cli/config.py +0 -0
- {gravity_cli-0.7.5 → gravity_cli-0.8.0}/src/gravity_cli/dev.py +0 -0
- {gravity_cli-0.7.5 → gravity_cli-0.8.0}/src/gravity_cli/dev_page/index.html +0 -0
- {gravity_cli-0.7.5 → gravity_cli-0.8.0}/src/gravity_cli/docs/problem-to-agent.md +0 -0
- {gravity_cli-0.7.5 → gravity_cli-0.8.0}/src/gravity_cli/inputs.py +0 -0
- {gravity_cli-0.7.5 → gravity_cli-0.8.0}/src/gravity_cli/localrun.py +0 -0
- {gravity_cli-0.7.5 → gravity_cli-0.8.0}/src/gravity_cli/manifest.py +0 -0
- {gravity_cli-0.7.5 → gravity_cli-0.8.0}/src/gravity_cli/project.py +0 -0
- {gravity_cli-0.7.5 → gravity_cli-0.8.0}/src/gravity_cli/templates/chat/.gitignore +0 -0
- {gravity_cli-0.7.5 → gravity_cli-0.8.0}/src/gravity_cli/templates/chat/manifest.yaml +0 -0
- {gravity_cli-0.7.5 → gravity_cli-0.8.0}/src/gravity_cli/templates/chat/requirements.txt +0 -0
- {gravity_cli-0.7.5 → gravity_cli-0.8.0}/src/gravity_cli/templates/chat/src/agent.py +0 -0
- {gravity_cli-0.7.5 → gravity_cli-0.8.0}/src/gravity_cli/templates/deepagent/.gitignore +0 -0
- {gravity_cli-0.7.5 → gravity_cli-0.8.0}/src/gravity_cli/templates/deepagent/manifest.yaml +0 -0
- {gravity_cli-0.7.5 → gravity_cli-0.8.0}/src/gravity_cli/templates/deepagent/requirements.txt +0 -0
- {gravity_cli-0.7.5 → gravity_cli-0.8.0}/src/gravity_cli/templates/deepagent/src/agent.py +0 -0
- {gravity_cli-0.7.5 → gravity_cli-0.8.0}/src/gravity_cli/templates/minimal/.gitignore +0 -0
- {gravity_cli-0.7.5 → gravity_cli-0.8.0}/src/gravity_cli/templates/minimal/manifest.yaml +0 -0
- {gravity_cli-0.7.5 → gravity_cli-0.8.0}/src/gravity_cli/templates/minimal/requirements.txt +0 -0
- {gravity_cli-0.7.5 → gravity_cli-0.8.0}/src/gravity_cli/templates/minimal/src/agent.py +0 -0
- {gravity_cli-0.7.5 → gravity_cli-0.8.0}/src/gravity_cli/templates/structured/.gitignore +0 -0
- {gravity_cli-0.7.5 → gravity_cli-0.8.0}/src/gravity_cli/templates/structured/manifest.yaml +0 -0
- {gravity_cli-0.7.5 → gravity_cli-0.8.0}/src/gravity_cli/templates/structured/requirements.txt +0 -0
- {gravity_cli-0.7.5 → gravity_cli-0.8.0}/src/gravity_cli/templates/structured/src/agent.py +0 -0
- {gravity_cli-0.7.5 → gravity_cli-0.8.0}/src/gravity_cli/templates/structured/src/gateway.py +0 -0
- {gravity_cli-0.7.5 → gravity_cli-0.8.0}/src/gravity_cli/templates/structured/src/nodes/agent.py +0 -0
- {gravity_cli-0.7.5 → gravity_cli-0.8.0}/src/gravity_cli/templates/structured/src/prompts.py +0 -0
- {gravity_cli-0.7.5 → gravity_cli-0.8.0}/src/gravity_cli/templates/structured/src/state.py +0 -0
- {gravity_cli-0.7.5 → gravity_cli-0.8.0}/src/gravity_cli/timeline.py +0 -0
- {gravity_cli-0.7.5 → gravity_cli-0.8.0}/src/gravity_cli/ui.py +0 -0
- {gravity_cli-0.7.5 → gravity_cli-0.8.0}/src/gravity_cli/updater.py +0 -0
- {gravity_cli-0.7.5 → gravity_cli-0.8.0}/src/gravity_cli/workspace.py +0 -0
- {gravity_cli-0.7.5 → gravity_cli-0.8.0}/starter/manifest.yaml +0 -0
- {gravity_cli-0.7.5 → gravity_cli-0.8.0}/starter/requirements.txt +0 -0
- {gravity_cli-0.7.5 → gravity_cli-0.8.0}/starter/src/agent.py +0 -0
- {gravity_cli-0.7.5 → gravity_cli-0.8.0}/starter/src/summary.py +0 -0
- {gravity_cli-0.7.5 → gravity_cli-0.8.0}/starter/tests/test_summary.py +0 -0
- {gravity_cli-0.7.5 → gravity_cli-0.8.0}/tests/conftest.py +0 -0
- {gravity_cli-0.7.5 → gravity_cli-0.8.0}/tests/test_adapt.py +0 -0
- {gravity_cli-0.7.5 → gravity_cli-0.8.0}/tests/test_agent_guide.py +0 -0
- {gravity_cli-0.7.5 → gravity_cli-0.8.0}/tests/test_agents.py +0 -0
- {gravity_cli-0.7.5 → gravity_cli-0.8.0}/tests/test_api.py +0 -0
- {gravity_cli-0.7.5 → gravity_cli-0.8.0}/tests/test_build_push.py +0 -0
- {gravity_cli-0.7.5 → gravity_cli-0.8.0}/tests/test_changelog.py +0 -0
- {gravity_cli-0.7.5 → gravity_cli-0.8.0}/tests/test_connect.py +0 -0
- {gravity_cli-0.7.5 → gravity_cli-0.8.0}/tests/test_dev.py +0 -0
- {gravity_cli-0.7.5 → gravity_cli-0.8.0}/tests/test_gateway_token.py +0 -0
- {gravity_cli-0.7.5 → gravity_cli-0.8.0}/tests/test_inputs_memory.py +0 -0
- {gravity_cli-0.7.5 → gravity_cli-0.8.0}/tests/test_localrun.py +0 -0
- {gravity_cli-0.7.5 → gravity_cli-0.8.0}/tests/test_manifest_integration.py +0 -0
- {gravity_cli-0.7.5 → gravity_cli-0.8.0}/tests/test_run.py +0 -0
- {gravity_cli-0.7.5 → gravity_cli-0.8.0}/tests/test_runs.py +0 -0
- {gravity_cli-0.7.5 → gravity_cli-0.8.0}/tests/test_schedule.py +0 -0
- {gravity_cli-0.7.5 → gravity_cli-0.8.0}/tests/test_session.py +0 -0
- {gravity_cli-0.7.5 → gravity_cli-0.8.0}/tests/test_templates.py +0 -0
- {gravity_cli-0.7.5 → gravity_cli-0.8.0}/tests/test_timeline.py +0 -0
- {gravity_cli-0.7.5 → gravity_cli-0.8.0}/tests/test_tools.py +0 -0
- {gravity_cli-0.7.5 → gravity_cli-0.8.0}/tests/test_ui.py +0 -0
- {gravity_cli-0.7.5 → gravity_cli-0.8.0}/tests/test_updater.py +0 -0
- {gravity_cli-0.7.5 → gravity_cli-0.8.0}/tests/test_validate.py +0 -0
- {gravity_cli-0.7.5 → gravity_cli-0.8.0}/tests/test_welcome.py +0 -0
- {gravity_cli-0.7.5 → gravity_cli-0.8.0}/tests/test_workspace.py +0 -0
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: gravity-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.8.0
|
|
4
4
|
Summary: gravity cli — init, run, and push python agents to the Gravity marketplace
|
|
5
5
|
License-Expression: Apache-2.0
|
|
6
6
|
License-File: LICENSE
|
|
7
7
|
License-File: NOTICE
|
|
8
8
|
Requires-Python: >=3.12
|
|
9
|
-
Requires-Dist: gravity-schema==0.
|
|
9
|
+
Requires-Dist: gravity-schema==0.8.0
|
|
10
10
|
Requires-Dist: httpx>=0.27.0
|
|
11
11
|
Requires-Dist: keyring>=25.0.0
|
|
12
12
|
Requires-Dist: python-dotenv>=1.0.0
|
|
@@ -327,6 +327,24 @@
|
|
|
327
327
|
guarantee: certified
|
|
328
328
|
side_effect: write
|
|
329
329
|
|
|
330
|
+
- name: x.user_lookup
|
|
331
|
+
slug: TWITTER_USER_LOOKUP_BY_USERNAME
|
|
332
|
+
toolkit: twitter
|
|
333
|
+
description: look up an X account by its @username (without the @) through the person's own X developer app; returns its id (what x.send_dm takes), name, bio and counts
|
|
334
|
+
scopes: [users.read]
|
|
335
|
+
breadth: generic
|
|
336
|
+
guarantee: certified
|
|
337
|
+
side_effect: read
|
|
338
|
+
|
|
339
|
+
- name: x.send_dm
|
|
340
|
+
slug: TWITTER_SEND_A_NEW_MESSAGE_TO_A_USER
|
|
341
|
+
toolkit: twitter
|
|
342
|
+
description: send a direct message on X to one account (participant_id from x.user_lookup) from the person's own X developer app, billed to their X API account; X delivers it only when the recipient accepts messages from them
|
|
343
|
+
scopes: [dm.write, dm.read, tweet.read, users.read]
|
|
344
|
+
breadth: generic
|
|
345
|
+
guarantee: certified
|
|
346
|
+
side_effect: write
|
|
347
|
+
|
|
330
348
|
- name: forms.create
|
|
331
349
|
slug: GOOGLEFORMS_CREATE_FORM
|
|
332
350
|
toolkit: googleforms
|
|
@@ -399,6 +417,42 @@
|
|
|
399
417
|
guarantee: community
|
|
400
418
|
side_effect: read
|
|
401
419
|
|
|
420
|
+
- name: byteforms.create_form
|
|
421
|
+
slug: BYTEFORMS_CREATE_FORM
|
|
422
|
+
toolkit: byteforms
|
|
423
|
+
description: create a public Byteforms form from text, email and phone fields, with an optional close date (stop_submissions_after) and submission cap; returns its id and public_id for the link; runs on Gravity's own Byteforms account where Gravity connected one
|
|
424
|
+
scopes: []
|
|
425
|
+
breadth: generic
|
|
426
|
+
guarantee: community
|
|
427
|
+
side_effect: write
|
|
428
|
+
|
|
429
|
+
- name: byteforms.get_form
|
|
430
|
+
slug: BYTEFORMS_GET_FORM_BY_ID
|
|
431
|
+
toolkit: byteforms
|
|
432
|
+
description: read one Byteforms form by its numeric id (fields, options, link)
|
|
433
|
+
scopes: []
|
|
434
|
+
breadth: generic
|
|
435
|
+
guarantee: community
|
|
436
|
+
side_effect: read
|
|
437
|
+
|
|
438
|
+
- name: byteforms.list_forms
|
|
439
|
+
slug: BYTEFORMS_GET_ALL_FORMS2
|
|
440
|
+
toolkit: byteforms
|
|
441
|
+
description: list the forms on the Byteforms account
|
|
442
|
+
scopes: []
|
|
443
|
+
breadth: generic
|
|
444
|
+
guarantee: community
|
|
445
|
+
side_effect: read
|
|
446
|
+
|
|
447
|
+
- name: byteforms.get_responses
|
|
448
|
+
slug: BYTEFORMS_GET_FORM_RESPONSES
|
|
449
|
+
toolkit: byteforms
|
|
450
|
+
description: read the responses to one Byteforms form by its numeric id, newest first, with a cursor for more
|
|
451
|
+
scopes: []
|
|
452
|
+
breadth: generic
|
|
453
|
+
guarantee: community
|
|
454
|
+
side_effect: read
|
|
455
|
+
|
|
402
456
|
- name: scrapecreators.linkedin_profile
|
|
403
457
|
slug: SCRAPECREATORS_GET_LINKEDIN_PROFILE
|
|
404
458
|
toolkit: scrapecreators
|
|
@@ -596,6 +596,15 @@ class OutputSpec(BaseModel):
|
|
|
596
596
|
return v
|
|
597
597
|
|
|
598
598
|
|
|
599
|
+
class AdapterOption(BaseModel):
|
|
600
|
+
"""How one pick reads to the person when the app's name alone does not say it."""
|
|
601
|
+
|
|
602
|
+
model_config = _STRICT
|
|
603
|
+
|
|
604
|
+
title: Annotated[str, Field(min_length=1, max_length=C.MAX_LABEL_LENGTH)]
|
|
605
|
+
about: Annotated[str, Field(max_length=C.MAX_LABEL_LENGTH)] | None = None
|
|
606
|
+
|
|
607
|
+
|
|
599
608
|
class SlotSpec(BaseModel):
|
|
600
609
|
"""A swappable part. Adapter <name> is src/adapters/<slot>/<name>.py; its tools are granted only when picked."""
|
|
601
610
|
|
|
@@ -606,9 +615,28 @@ class SlotSpec(BaseModel):
|
|
|
606
615
|
adapters: dict[str, list[str]] # adapter name -> the catalog tools it calls
|
|
607
616
|
# The consumer may pick several adapters; the pick is a list and every pick's tools are granted.
|
|
608
617
|
multiple: bool = False
|
|
618
|
+
# Asked when this action runs instead of at setup, after the action's own questions; the pick is kept for later runs.
|
|
619
|
+
ask_in: Annotated[str, Field(max_length=C.MAX_NAME_LENGTH)] | None = None
|
|
620
|
+
# With ask_in: asked only when this holds (names the action's inputs, setup inputs or other slots).
|
|
621
|
+
show_if: Annotated[ShowIf | None, Field(max_length=C.MAX_INPUTS)] = None
|
|
622
|
+
# Per adapter: the option's title and one line on what picking it means.
|
|
623
|
+
options: dict[str, AdapterOption] = Field(default_factory=dict)
|
|
609
624
|
|
|
610
625
|
@model_validator(mode="after")
|
|
611
626
|
def _check(self) -> SlotSpec:
|
|
627
|
+
unknown = [n for n in self.options if n not in self.adapters]
|
|
628
|
+
if unknown:
|
|
629
|
+
raise PydanticCustomError(
|
|
630
|
+
ErrorCode.INVALID_INPUT.value,
|
|
631
|
+
"Slot option '{value}' is not one of its adapters.",
|
|
632
|
+
{"value": unknown[0], "remediation": f"Describe adapters only: {', '.join(self.adapters)}."},
|
|
633
|
+
)
|
|
634
|
+
if self.show_if and not self.ask_in:
|
|
635
|
+
raise PydanticCustomError(
|
|
636
|
+
ErrorCode.INVALID_CONDITION.value,
|
|
637
|
+
"A slot's show_if needs ask_in: a setup choice is always asked.",
|
|
638
|
+
{"remediation": "Add ask_in: <action>, or drop show_if."},
|
|
639
|
+
)
|
|
612
640
|
for name, tools in self.adapters.items():
|
|
613
641
|
if not C.INPUT_NAME_RE.match(name):
|
|
614
642
|
raise PydanticCustomError(
|
|
@@ -1042,6 +1070,20 @@ class Manifest(BaseModel):
|
|
|
1042
1070
|
_check_show_if(self.inputs, self.slots, "inputs")
|
|
1043
1071
|
for a in self.actions:
|
|
1044
1072
|
_check_show_if(a.inputs, self.slots, f"actions.{a.name}.inputs", earlier=self.inputs)
|
|
1073
|
+
actions = {a.name: a for a in self.actions}
|
|
1074
|
+
for name, slot in self.slots.items():
|
|
1075
|
+
if slot.ask_in is None:
|
|
1076
|
+
continue
|
|
1077
|
+
if slot.ask_in not in actions:
|
|
1078
|
+
raise PydanticCustomError(
|
|
1079
|
+
ErrorCode.INVALID_ACTION.value,
|
|
1080
|
+
"Slot '{value}' is asked in action '{other}', which is not declared.",
|
|
1081
|
+
{"value": name, "other": slot.ask_in, "remediation": "Name an action under `actions`."},
|
|
1082
|
+
)
|
|
1083
|
+
# Checked as a question asked after the action's own: any of them, the setup inputs and the other slots.
|
|
1084
|
+
asked = InputSpec.model_construct(name=name, type=InputType.STATIC_DROPDOWN, show_if=slot.show_if)
|
|
1085
|
+
others = {k: v for k, v in self.slots.items() if k != name}
|
|
1086
|
+
_check_show_if([asked], others, f"slots.{name}", earlier=[*self.inputs, *actions[slot.ask_in].inputs])
|
|
1045
1087
|
names = {i.name: i for i in self.inputs}
|
|
1046
1088
|
grouped: set[str] = set()
|
|
1047
1089
|
for g in self.choose_one:
|
|
@@ -83,6 +83,21 @@ def visible_action_inputs(manifest: Manifest, action: ActionSpec, answers: dict[
|
|
|
83
83
|
return _walk(action.inputs, [], action_answers, known)
|
|
84
84
|
|
|
85
85
|
|
|
86
|
+
def action_slots(manifest: Manifest, action: ActionSpec, answers: dict[str, Any],
|
|
87
|
+
action_answers: dict[str, Any]) -> list[str]:
|
|
88
|
+
"""The setup choices this action asks after its own questions (slots with ask_in), in manifest order."""
|
|
89
|
+
known = _slot_picks(manifest, answers)
|
|
90
|
+
for name in visible_inputs(manifest, answers):
|
|
91
|
+
if name in answers:
|
|
92
|
+
known[name] = answers[name]
|
|
93
|
+
for name in visible_action_inputs(manifest, action, answers, action_answers):
|
|
94
|
+
if name in action_answers:
|
|
95
|
+
known[name] = action_answers[name]
|
|
96
|
+
return [name for name, slot in manifest.slots.items()
|
|
97
|
+
if slot.ask_in == action.name
|
|
98
|
+
and all(ref in known and _holds(known[ref], want) for ref, want in (slot.show_if or {}).items())]
|
|
99
|
+
|
|
100
|
+
|
|
86
101
|
def apply_rules(manifest: Manifest, answers: dict[str, Any]) -> dict[str, Any]:
|
|
87
102
|
"""The answers a run gets: slot picks and visible inputs; a hidden input's value is dropped."""
|
|
88
103
|
keep = set(visible_inputs(manifest, answers)) | set(manifest.slots)
|
|
@@ -110,6 +110,36 @@
|
|
|
110
110
|
"title": "ActionSpec",
|
|
111
111
|
"type": "object"
|
|
112
112
|
},
|
|
113
|
+
"AdapterOption": {
|
|
114
|
+
"additionalProperties": false,
|
|
115
|
+
"description": "How one pick reads to the person when the app's name alone does not say it.",
|
|
116
|
+
"properties": {
|
|
117
|
+
"about": {
|
|
118
|
+
"anyOf": [
|
|
119
|
+
{
|
|
120
|
+
"maxLength": 200,
|
|
121
|
+
"type": "string"
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
"type": "null"
|
|
125
|
+
}
|
|
126
|
+
],
|
|
127
|
+
"default": null,
|
|
128
|
+
"title": "About"
|
|
129
|
+
},
|
|
130
|
+
"title": {
|
|
131
|
+
"maxLength": 200,
|
|
132
|
+
"minLength": 1,
|
|
133
|
+
"title": "Title",
|
|
134
|
+
"type": "string"
|
|
135
|
+
}
|
|
136
|
+
},
|
|
137
|
+
"required": [
|
|
138
|
+
"title"
|
|
139
|
+
],
|
|
140
|
+
"title": "AdapterOption",
|
|
141
|
+
"type": "object"
|
|
142
|
+
},
|
|
113
143
|
"AgentKind": {
|
|
114
144
|
"description": "transform: input in, result out (the platform delivers). interactive: tools mid-flow,\nmaybe several turns. monitor: scheduled or event-driven, remembers the last run.",
|
|
115
145
|
"enum": [
|
|
@@ -693,6 +723,19 @@
|
|
|
693
723
|
"title": "Adapters",
|
|
694
724
|
"type": "object"
|
|
695
725
|
},
|
|
726
|
+
"ask_in": {
|
|
727
|
+
"anyOf": [
|
|
728
|
+
{
|
|
729
|
+
"maxLength": 64,
|
|
730
|
+
"type": "string"
|
|
731
|
+
},
|
|
732
|
+
{
|
|
733
|
+
"type": "null"
|
|
734
|
+
}
|
|
735
|
+
],
|
|
736
|
+
"default": null,
|
|
737
|
+
"title": "Ask In"
|
|
738
|
+
},
|
|
696
739
|
"default": {
|
|
697
740
|
"title": "Default",
|
|
698
741
|
"type": "string"
|
|
@@ -706,6 +749,61 @@
|
|
|
706
749
|
"default": false,
|
|
707
750
|
"title": "Multiple",
|
|
708
751
|
"type": "boolean"
|
|
752
|
+
},
|
|
753
|
+
"options": {
|
|
754
|
+
"additionalProperties": {
|
|
755
|
+
"$ref": "#/$defs/AdapterOption"
|
|
756
|
+
},
|
|
757
|
+
"title": "Options",
|
|
758
|
+
"type": "object"
|
|
759
|
+
},
|
|
760
|
+
"show_if": {
|
|
761
|
+
"anyOf": [
|
|
762
|
+
{
|
|
763
|
+
"additionalProperties": {
|
|
764
|
+
"anyOf": [
|
|
765
|
+
{
|
|
766
|
+
"type": "string"
|
|
767
|
+
},
|
|
768
|
+
{
|
|
769
|
+
"type": "boolean"
|
|
770
|
+
},
|
|
771
|
+
{
|
|
772
|
+
"type": "integer"
|
|
773
|
+
},
|
|
774
|
+
{
|
|
775
|
+
"type": "number"
|
|
776
|
+
},
|
|
777
|
+
{
|
|
778
|
+
"items": {
|
|
779
|
+
"anyOf": [
|
|
780
|
+
{
|
|
781
|
+
"type": "string"
|
|
782
|
+
},
|
|
783
|
+
{
|
|
784
|
+
"type": "boolean"
|
|
785
|
+
},
|
|
786
|
+
{
|
|
787
|
+
"type": "integer"
|
|
788
|
+
},
|
|
789
|
+
{
|
|
790
|
+
"type": "number"
|
|
791
|
+
}
|
|
792
|
+
]
|
|
793
|
+
},
|
|
794
|
+
"type": "array"
|
|
795
|
+
}
|
|
796
|
+
]
|
|
797
|
+
},
|
|
798
|
+
"maxProperties": 50,
|
|
799
|
+
"type": "object"
|
|
800
|
+
},
|
|
801
|
+
{
|
|
802
|
+
"type": "null"
|
|
803
|
+
}
|
|
804
|
+
],
|
|
805
|
+
"default": null,
|
|
806
|
+
"title": "Show If"
|
|
709
807
|
}
|
|
710
808
|
},
|
|
711
809
|
"required": [
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"""Slots: a swappable adapter per slot; only the picked adapter's tools are granted."""
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
from gravity_schema import dev_catalog, validate_manifest
|
|
5
|
+
from gravity_schema.errors import ManifestValidationError
|
|
6
|
+
|
|
7
|
+
BASE = {
|
|
8
|
+
"name": "digest",
|
|
9
|
+
"runtime": {"entrypoint": "src/agent.py:graph"},
|
|
10
|
+
"permissions": {"tools": ["gmail.read"]},
|
|
11
|
+
"slots": {
|
|
12
|
+
"deliver": {
|
|
13
|
+
"label": "Where to send it",
|
|
14
|
+
"default": "gmail",
|
|
15
|
+
"adapters": {"gmail": ["gmail.draft"], "linear": ["linear.list_teams", "linear.create_issue"]},
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def test_only_the_picked_adapter_is_granted():
|
|
22
|
+
m = validate_manifest(BASE, catalog=dev_catalog())
|
|
23
|
+
assert m.tools_for({}) == ["gmail.read", "gmail.draft"]
|
|
24
|
+
assert m.tools_for({"deliver": "linear"}) == ["gmail.read", "linear.list_teams", "linear.create_issue"]
|
|
25
|
+
assert m.default_inputs()["deliver"] == "gmail"
|
|
26
|
+
with pytest.raises(ValueError):
|
|
27
|
+
m.tools_for({"deliver": "../../etc"}) # a pick is never a free path
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def test_bad_slots_are_rejected():
|
|
31
|
+
for patch in (
|
|
32
|
+
{"default": "slack"}, # default not an adapter
|
|
33
|
+
{"adapters": {"gmail": ["GMAIL_SEND_EMAIL"]}, "default": "gmail"}, # vendor slug
|
|
34
|
+
):
|
|
35
|
+
bad = {**BASE, "slots": {"deliver": {**BASE["slots"]["deliver"], **patch}}}
|
|
36
|
+
with pytest.raises(ManifestValidationError):
|
|
37
|
+
validate_manifest(bad)
|
|
38
|
+
clash = {**BASE, "inputs": [{"name": "deliver", "type": "text", "label": "x"}]}
|
|
39
|
+
with pytest.raises(ManifestValidationError):
|
|
40
|
+
validate_manifest(clash)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def test_adapter_tools_are_checked_against_the_catalog():
|
|
44
|
+
bad = {**BASE, "slots": {"deliver": {**BASE["slots"]["deliver"], "adapters": {"gmail": ["gmail.nope"]}}}}
|
|
45
|
+
with pytest.raises(ManifestValidationError) as e:
|
|
46
|
+
validate_manifest(bad, catalog=dev_catalog())
|
|
47
|
+
assert "slots.deliver.adapters.gmail[0]" in str(e.value.issues[0].location)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def test_approvals_may_name_an_adapter_tool():
|
|
51
|
+
m = validate_manifest({**BASE, "approvals": {"required_for": ["gmail.draft"]}})
|
|
52
|
+
assert "gmail.draft" in m.declared_tools
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
# A choice that only one step needs: asked when that action runs, under the action's own answers.
|
|
56
|
+
ASKED = {
|
|
57
|
+
**BASE,
|
|
58
|
+
"actions": [
|
|
59
|
+
{"name": "find", "label": "Find", "default": True},
|
|
60
|
+
{"name": "reach", "label": "Reach out", "requires": ["find"], "inputs": [
|
|
61
|
+
{"name": "channels", "type": "multi-select", "label": "Where?", "options": ["Email", "X message"], "min": 1}]},
|
|
62
|
+
],
|
|
63
|
+
"slots": {
|
|
64
|
+
**BASE["slots"],
|
|
65
|
+
"x_send": {
|
|
66
|
+
"label": "How should I send your X messages?",
|
|
67
|
+
"default": "manual",
|
|
68
|
+
"ask_in": "reach",
|
|
69
|
+
"show_if": {"channels": "X message"},
|
|
70
|
+
"adapters": {"x_api": ["gmail.draft"], "manual": []},
|
|
71
|
+
"options": {"x_api": {"title": "My X developer app", "about": "Uses your own keys"},
|
|
72
|
+
"manual": {"title": "I'll send them myself"}},
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def test_an_action_asks_its_own_setup_choice():
|
|
79
|
+
from gravity_schema.rules import action_slots, visible_inputs
|
|
80
|
+
|
|
81
|
+
m = validate_manifest(ASKED)
|
|
82
|
+
reach = next(a for a in m.actions if a.name == "reach")
|
|
83
|
+
assert action_slots(m, reach, {}, {"channels": ["Email", "X message"]}) == ["x_send"]
|
|
84
|
+
assert action_slots(m, reach, {}, {"channels": ["Email"]}) == []
|
|
85
|
+
assert action_slots(m, m.actions[0], {}, {}) == []
|
|
86
|
+
assert visible_inputs(m, {}) == [] # never a setup question
|
|
87
|
+
assert m.tools_for({}) == ["gmail.read", "gmail.draft"] # the default pick grants nothing more
|
|
88
|
+
assert m.tools_for({"x_send": "x_api"}) == ["gmail.read", "gmail.draft"]
|
|
89
|
+
assert m.slots["x_send"].options["manual"].about is None
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def test_bad_action_asked_slots_are_rejected():
|
|
93
|
+
slot = ASKED["slots"]["x_send"]
|
|
94
|
+
for patch in (
|
|
95
|
+
{"ask_in": "nowhere"}, # not an action
|
|
96
|
+
{"show_if": {"channels": "Fax"}}, # a value channels cannot take
|
|
97
|
+
{"show_if": {"later": "x"}}, # names nothing asked before
|
|
98
|
+
{"ask_in": None}, # show_if without ask_in
|
|
99
|
+
{"options": {"carrier_pigeon": {"title": "Pigeon"}}}, # describes no adapter
|
|
100
|
+
{"options": {"manual": {"title": ""}}}, # an empty title
|
|
101
|
+
):
|
|
102
|
+
bad = {**ASKED, "slots": {**ASKED["slots"], "x_send": {**slot, **patch}}}
|
|
103
|
+
with pytest.raises(ManifestValidationError):
|
|
104
|
+
validate_manifest(bad)
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "gravity-cli"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.8.0"
|
|
4
4
|
description = "gravity cli — init, run, and push python agents to the Gravity marketplace"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
license = "Apache-2.0"
|
|
7
7
|
license-files = ["LICENSE", "NOTICE"]
|
|
8
8
|
requires-python = ">=3.12"
|
|
9
9
|
dependencies = [
|
|
10
|
-
"gravity-schema==0.
|
|
10
|
+
"gravity-schema==0.8.0", # released in lockstep: always the same version as this package
|
|
11
11
|
"typer>=0.15.0",
|
|
12
12
|
"httpx>=0.27.0",
|
|
13
13
|
"rich>=13.9.0",
|
|
@@ -90,6 +90,9 @@ inputs: # becomes the user's form; arrives as state[
|
|
|
90
90
|
type: email # text | textarea | number | boolean | static-dropdown | email | url | date | secret | file
|
|
91
91
|
label: "Where to send the digest"
|
|
92
92
|
required: true
|
|
93
|
+
# number, email, url, date and boolean answers are checked on every hosted run: a wrong
|
|
94
|
+
# form is refused on the question ("Enter one email address"); a url typed as
|
|
95
|
+
# example.com arrives as https://example.com
|
|
93
96
|
|
|
94
97
|
permissions:
|
|
95
98
|
tools: # EVERY tool your code calls, by Gravity name (§7)
|
|
@@ -457,7 +460,10 @@ that only works for tools loaded with `await gravity.tools()`: a gated call made
|
|
|
457
460
|
fails there with `APPROVAL_REQUIRED`. The example's `load_tools()` does this, and falls back to the MCP client
|
|
458
461
|
when `gravity` is not installed (`gravity run`, `gravity dev`, tests). After the decision the node that made
|
|
459
462
|
the call runs again from its start, so keep the gated call last in its node. If you wrap the call in a broad
|
|
460
|
-
`except`, re-raise `langgraph.errors.GraphBubbleUp` first (`except GraphBubbleUp: raise`)
|
|
463
|
+
`except`, re-raise `langgraph.errors.GraphBubbleUp` first (`except GraphBubbleUp: raise`). If you forget, the
|
|
464
|
+
hosted platform raises the caught pause again when the node ends, so the person is still asked; but the code after
|
|
465
|
+
your `except` has already run once (it thought the call failed) and runs again after the decision. Under
|
|
466
|
+
`gravity run` nothing raises it again, so test the approval path locally.
|
|
461
467
|
|
|
462
468
|
### 6.2 Platform adapters: let the user choose where results go
|
|
463
469
|
|
|
@@ -489,6 +495,27 @@ slots:
|
|
|
489
495
|
linear: [linear.list_teams, linear.create_issue, linear.get_issue]
|
|
490
496
|
```
|
|
491
497
|
|
|
498
|
+
A choice that only one step needs is asked by that step instead of at setup: give the slot
|
|
499
|
+
`ask_in: <action>` (and a `show_if` on that action's answers if it applies only sometimes). The
|
|
500
|
+
step asks it after its own questions, the run gets the pick's tools, and the pick is kept for later
|
|
501
|
+
runs. Until then the `default` applies, so default to the pick that needs no account. `options`
|
|
502
|
+
says what each pick means:
|
|
503
|
+
|
|
504
|
+
```yaml
|
|
505
|
+
slots:
|
|
506
|
+
x_send:
|
|
507
|
+
label: "How should I send your X messages?"
|
|
508
|
+
default: manual
|
|
509
|
+
ask_in: reach_out
|
|
510
|
+
show_if: { channels: "X message" }
|
|
511
|
+
adapters:
|
|
512
|
+
x_api: [x.user_lookup, x.send_dm]
|
|
513
|
+
manual: []
|
|
514
|
+
options:
|
|
515
|
+
x_api: { title: "My X developer app", about: "Sends through your own X app; X bills your developer account." }
|
|
516
|
+
manual: { title: "I'll send them myself", about: "I write each message; you send it." }
|
|
517
|
+
```
|
|
518
|
+
|
|
492
519
|
`src/adapters/deliver/__init__.py`: the contract every adapter follows:
|
|
493
520
|
|
|
494
521
|
```python
|
|
@@ -657,6 +684,8 @@ memory.previous() # the last finished run of this conversation: action, inputs
|
|
|
657
684
|
- There is no user id in the API: the run decides whose memory it is, so your code can never reach
|
|
658
685
|
another user's. A write over a limit raises `MemoryQuotaError` and writes nothing.
|
|
659
686
|
- Items expire `keep_days` after their last write. The user sees and deletes what the agent keeps.
|
|
687
|
+
- Each call is one blocking request. Read a collection once (`find(limit=500)`) and reuse the list instead of
|
|
688
|
+
calling `get` per record; check many keys with one `has_many`.
|
|
660
689
|
- Under `gravity run` the same calls use `.gravity/memory/`; `gravity memory ls` shows it.
|
|
661
690
|
|
|
662
691
|
### 6.5 Actions: more than one thing to do
|
|
@@ -4,6 +4,16 @@ Shown by `gravity update` and `gravity changelog`, and after an update. Each rel
|
|
|
4
4
|
then one bullet per change: `- new|improved|fixed: A one-line headline. The detail behind it.`
|
|
5
5
|
Write it for a builder, not for a maintainer. `tests/test_changelog.py` fails a release that has no entry.
|
|
6
6
|
|
|
7
|
+
## 0.8.0 - 2026-10-25
|
|
8
|
+
|
|
9
|
+
- new: A step can ask its own setup choice. A slot with `ask_in: <action>` is asked when that action runs (after its own questions, under its own `show_if`), not at setup; the run gets the pick's tools and asks for its app if needed. `options` gives each pick a title and a line on what it means.
|
|
10
|
+
- new: X through the person's own X developer app. x.user_lookup finds an account by @username and x.send_dm sends it a message, billed to their X API account (Composio's X toolkit has no shared app, so each person connects their own).
|
|
11
|
+
- improved: Docs on what the platform does with a caught approval pause, and on keeping memory calls few.
|
|
12
|
+
|
|
13
|
+
## 0.7.6 - 2026-10-24
|
|
14
|
+
|
|
15
|
+
- new: Byteforms tools. byteforms.create_form builds a public form from text, email and phone fields with a close date, and byteforms.get_responses reads what people sent (plus get_form and list_forms). Where Gravity has connected its own Byteforms account, every user's runs use it and nobody connects anything.
|
|
16
|
+
|
|
7
17
|
## 0.7.5 - 2026-10-24
|
|
8
18
|
|
|
9
19
|
- new: ScrapeCreators tools on Gravity's account. scrapecreators.linkedin_profile reads a public LinkedIn profile in full (experience with descriptions, education, recent activity) where LinkedIn refuses a plain read, plus linkedin_post, twitter_profile, twitter_tweets, reddit_search (no Reddit API key) and social_profiles. They run through Composio Instant, charged to the run's budget.
|
|
@@ -291,6 +291,17 @@ slots: # swappable adapters (section 10)
|
|
|
291
291
|
adapters:
|
|
292
292
|
gravity: [] # kept in Gravity
|
|
293
293
|
gmail: [gmail.draft]
|
|
294
|
+
x_send: # a choice only one step needs: asked when that step runs, not at setup
|
|
295
|
+
label: "How should I send your X messages?"
|
|
296
|
+
default: manual
|
|
297
|
+
ask_in: reply_drafts # the action that asks it, after its own questions; the pick is kept for later runs
|
|
298
|
+
# show_if: { channels: "X message" } # only when that step's answers (or setup answers, or other slots) match
|
|
299
|
+
adapters:
|
|
300
|
+
x_api: [x.user_lookup, x.send_dm]
|
|
301
|
+
manual: []
|
|
302
|
+
options: # how each pick reads, when the app's name alone does not say it
|
|
303
|
+
x_api: { title: "My X developer app", about: "Sends through your own X app; X bills your developer account." }
|
|
304
|
+
manual: { title: "I'll send them myself", about: "I write each message; you send it." }
|
|
294
305
|
|
|
295
306
|
actions: # what the agent can do (section 10); leave out for one entry point
|
|
296
307
|
- name: digest
|
|
@@ -342,7 +353,11 @@ Rules the validator enforces beyond field types:
|
|
|
342
353
|
scheduled run from a manual one: no trigger field is sent.
|
|
343
354
|
- **Triggers**: something must be able to start the agent; `kind: monitor` needs a schedule or a webhook.
|
|
344
355
|
- **Slots**: the pick arrives as `state["inputs"][<slot>]` (a list for `multiple: true`). A run is granted
|
|
345
|
-
`permissions.tools` plus only the picked adapters' tools.
|
|
356
|
+
`permissions.tools` plus only the picked adapters' tools. A slot with `ask_in: <action>` is not asked at setup: that
|
|
357
|
+
action asks it after its own questions (when its `show_if` holds; it may name that action's inputs), the run gets the
|
|
358
|
+
pick's tools and asks to connect the pick's app if needed, and the pick is kept for later runs. Until it is asked, the
|
|
359
|
+
`default` applies, so make the default the pick that needs nothing (no account, no tools). `options` gives each
|
|
360
|
+
adapter a `title` and an `about` line for the person; without it the app's name is shown.
|
|
346
361
|
- **Conditions** (`INVALID_CONDITION`): a `show_if` names a slot or an input declared above it, with values that
|
|
347
362
|
input can take (an option, an adapter, `true`/`false` for a boolean). Every condition in one `show_if` must hold;
|
|
348
363
|
a multi-select or multiple slot holds when any pick matches. A hidden question is never asked, never required,
|
|
@@ -459,7 +474,8 @@ async def graph():
|
|
|
459
474
|
| No checkpointer or store passed to `compile()` | `wf.compile()` | passing one: it is replaced |
|
|
460
475
|
| `messages` only for conversations, with `add_messages` | section 3 | a plain list reducer (replaces the history) |
|
|
461
476
|
| No `interrupt()` of your own | gated tools pause the run for you; `gravity.ask()` asks a question | a bare `interrupt()`: `UNSUPPORTED_INTERRUPT` fails the run |
|
|
462
|
-
| A gated call is never wrapped in a broad `except` | call it plainly; if you must catch, `except GraphBubbleUp: raise` first (`from langgraph.errors import GraphBubbleUp`) | `except Exception` around it: that
|
|
477
|
+
| A gated call is never wrapped in a broad `except` | call it plainly; if you must catch, `except GraphBubbleUp: raise` first (`from langgraph.errors import GraphBubbleUp`) | `except Exception` around it: that catches the pause for approval. Hosted, the platform raises a caught pause again when the node ends, so the person is still asked, but everything after your `except` already ran once and runs again on resume; under `gravity run` nothing catches it for you |
|
|
478
|
+
| Memory calls are few per run | `find(limit=500)` once and reuse the list; `has_many(keys)` for many keys | `get(key)` inside a loop over every record: each call is one blocking request, and hundreds of them stall the run |
|
|
463
479
|
|
|
464
480
|
Also:
|
|
465
481
|
|
|
@@ -578,13 +594,18 @@ one at a time.
|
|
|
578
594
|
| `static-dropdown` | One of `options` | string |
|
|
579
595
|
| `multi-select` | Any of `options`, between `min` and `max` | list of strings |
|
|
580
596
|
| `action-choice` | One of the agent's `actions` (that action runs) | string, and `state["actions"]` is that action |
|
|
581
|
-
| `email`, `url`, `date` | A validated field | string |
|
|
597
|
+
| `email`, `url`, `date` | A validated field: a wrong form is refused on the question itself ("Enter a web address, like example.com") | string; a `url` typed as `example.com` arrives as `https://example.com` |
|
|
582
598
|
| `secret` | A masked field, sealed, never shown again | string, for that run only |
|
|
583
599
|
| `file` | An upload limited by `accept` | a signed https link: accept a link, a path or text in your loader |
|
|
584
600
|
|
|
585
601
|
- Hosted, **every run is checked server-side**: a visible `required` input must be there for manual, chat, webhook
|
|
586
602
|
and scheduled runs alike, and a hidden one (its `show_if` fails, or the unfilled half of a `choose_one`) is
|
|
587
603
|
dropped. Write `show_if: { deliver: gmail }` instead of "only when delivering to Gmail" in a label.
|
|
604
|
+
- The **type is checked on every run** too: a `number`, `email`, `url`, `date` or `boolean` answer of the wrong
|
|
605
|
+
form ("fifty", "not-an-email", "my notion page") is refused with what to type, so your code never sees it. Pick
|
|
606
|
+
the type that matches: a website or a page link is `url`, not `text`. A number cannot carry `min`/`max` (only a
|
|
607
|
+
`multi-select` can), so when your agent keeps a number in a range, say so in the label and in the result ("You
|
|
608
|
+
asked for 50 leads; one run looks for 3 to 20") instead of changing it silently.
|
|
588
609
|
- A **required** `file` input on its own is refused by the hosted build (`FILE_INPUT_UNSUPPORTED`) although
|
|
589
610
|
`gravity validate` accepts it. For "upload or paste", put the file and a textarea in a `choose_one` group with
|
|
590
611
|
`required: true`: either one satisfies it. Scheduled runs cannot carry a file input.
|
|
@@ -712,6 +733,10 @@ memory.previous() # this conversation's last fin
|
|
|
712
733
|
and deletes what an agent keeps on My agents; the builder sees counts per collection, never content.
|
|
713
734
|
- `previous()` is `{"action", "actions", "inputs" (no secrets), "result", "finished_at", "truncated"}`; a result
|
|
714
735
|
over 256 KB is left out and `summary` describes its shape instead.
|
|
736
|
+
- Every call is one blocking request to the platform (about a third of a second), even inside an `async` node.
|
|
737
|
+
Read a collection once with `find(limit=...)` and keep the list, check many keys with `has_many`, and write only
|
|
738
|
+
what changed. A run that makes hundreds of calls in a row is slow; the platform keeps it alive, but the person
|
|
739
|
+
waits.
|
|
715
740
|
- Update memory **after** the side effect succeeded, so a failed delivery is retried. Keep the notebook small.
|
|
716
741
|
A follow-up such as "compare 1 and 2" can be answered from memory with no search.
|
|
717
742
|
- Under `gravity run` the same calls use `.gravity/memory/` with the same limits, if the `gravity` SDK is installed
|
|
@@ -164,7 +164,7 @@ wheels = [
|
|
|
164
164
|
|
|
165
165
|
[[package]]
|
|
166
166
|
name = "gravity-cli"
|
|
167
|
-
version = "0.
|
|
167
|
+
version = "0.8.0"
|
|
168
168
|
source = { editable = "." }
|
|
169
169
|
dependencies = [
|
|
170
170
|
{ name = "gravity-schema" },
|
|
@@ -201,7 +201,7 @@ dev = [
|
|
|
201
201
|
|
|
202
202
|
[[package]]
|
|
203
203
|
name = "gravity-schema"
|
|
204
|
-
version = "0.
|
|
204
|
+
version = "0.8.0"
|
|
205
205
|
source = { editable = "packages/gravity-schema" }
|
|
206
206
|
dependencies = [
|
|
207
207
|
{ name = "pydantic" },
|
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
"""Slots: a swappable adapter per slot; only the picked adapter's tools are granted."""
|
|
2
|
-
|
|
3
|
-
import pytest
|
|
4
|
-
from gravity_schema import dev_catalog, validate_manifest
|
|
5
|
-
from gravity_schema.errors import ManifestValidationError
|
|
6
|
-
|
|
7
|
-
BASE = {
|
|
8
|
-
"name": "digest",
|
|
9
|
-
"runtime": {"entrypoint": "src/agent.py:graph"},
|
|
10
|
-
"permissions": {"tools": ["gmail.read"]},
|
|
11
|
-
"slots": {
|
|
12
|
-
"deliver": {
|
|
13
|
-
"label": "Where to send it",
|
|
14
|
-
"default": "gmail",
|
|
15
|
-
"adapters": {"gmail": ["gmail.draft"], "linear": ["linear.list_teams", "linear.create_issue"]},
|
|
16
|
-
}
|
|
17
|
-
},
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
def test_only_the_picked_adapter_is_granted():
|
|
22
|
-
m = validate_manifest(BASE, catalog=dev_catalog())
|
|
23
|
-
assert m.tools_for({}) == ["gmail.read", "gmail.draft"]
|
|
24
|
-
assert m.tools_for({"deliver": "linear"}) == ["gmail.read", "linear.list_teams", "linear.create_issue"]
|
|
25
|
-
assert m.default_inputs()["deliver"] == "gmail"
|
|
26
|
-
with pytest.raises(ValueError):
|
|
27
|
-
m.tools_for({"deliver": "../../etc"}) # a pick is never a free path
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
def test_bad_slots_are_rejected():
|
|
31
|
-
for patch in (
|
|
32
|
-
{"default": "slack"}, # default not an adapter
|
|
33
|
-
{"adapters": {"gmail": ["GMAIL_SEND_EMAIL"]}, "default": "gmail"}, # vendor slug
|
|
34
|
-
):
|
|
35
|
-
bad = {**BASE, "slots": {"deliver": {**BASE["slots"]["deliver"], **patch}}}
|
|
36
|
-
with pytest.raises(ManifestValidationError):
|
|
37
|
-
validate_manifest(bad)
|
|
38
|
-
clash = {**BASE, "inputs": [{"name": "deliver", "type": "text", "label": "x"}]}
|
|
39
|
-
with pytest.raises(ManifestValidationError):
|
|
40
|
-
validate_manifest(clash)
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
def test_adapter_tools_are_checked_against_the_catalog():
|
|
44
|
-
bad = {**BASE, "slots": {"deliver": {**BASE["slots"]["deliver"], "adapters": {"gmail": ["gmail.nope"]}}}}
|
|
45
|
-
with pytest.raises(ManifestValidationError) as e:
|
|
46
|
-
validate_manifest(bad, catalog=dev_catalog())
|
|
47
|
-
assert "slots.deliver.adapters.gmail[0]" in str(e.value.issues[0].location)
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
def test_approvals_may_name_an_adapter_tool():
|
|
51
|
-
m = validate_manifest({**BASE, "approvals": {"required_for": ["gmail.draft"]}})
|
|
52
|
-
assert "gmail.draft" in m.declared_tools
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{gravity_cli-0.7.5 → gravity_cli-0.8.0}/packages/gravity-schema/gravity_schema/catalog_imported.yaml
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{gravity_cli-0.7.5 → gravity_cli-0.8.0}/packages/gravity-schema/gravity_schema/gen_catalog.py
RENAMED
|
File without changes
|
{gravity_cli-0.7.5 → gravity_cli-0.8.0}/packages/gravity-schema/gravity_schema/rules_cases.json
RENAMED
|
File without changes
|
{gravity_cli-0.7.5 → gravity_cli-0.8.0}/packages/gravity-schema/gravity_schema/source_scan.py
RENAMED
|
File without changes
|
{gravity_cli-0.7.5 → gravity_cli-0.8.0}/packages/gravity-schema/gravity_schema/validation.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{gravity_cli-0.7.5 → gravity_cli-0.8.0}/packages/gravity-schema/tests/test_actions_memory.py
RENAMED
|
File without changes
|
{gravity_cli-0.7.5 → gravity_cli-0.8.0}/packages/gravity-schema/tests/test_catalog_parity.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{gravity_cli-0.7.5 → gravity_cli-0.8.0}/src/gravity_cli/templates/deepagent/requirements.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{gravity_cli-0.7.5 → gravity_cli-0.8.0}/src/gravity_cli/templates/structured/requirements.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{gravity_cli-0.7.5 → gravity_cli-0.8.0}/src/gravity_cli/templates/structured/src/nodes/agent.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|