gravity-cli 0.6.0__tar.gz → 0.7.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.6.0 → gravity_cli-0.7.0}/PKG-INFO +2 -2
- {gravity_cli-0.6.0 → gravity_cli-0.7.0}/packages/gravity-schema/gravity_schema/__init__.py +18 -0
- {gravity_cli-0.6.0 → gravity_cli-0.7.0}/packages/gravity-schema/gravity_schema/contract.py +16 -0
- {gravity_cli-0.6.0 → gravity_cli-0.7.0}/packages/gravity-schema/gravity_schema/errors.py +3 -0
- {gravity_cli-0.6.0 → gravity_cli-0.7.0}/packages/gravity-schema/gravity_schema/manifest.py +403 -10
- gravity_cli-0.7.0/packages/gravity-schema/gravity_schema/rules.py +143 -0
- gravity_cli-0.7.0/packages/gravity-schema/gravity_schema/rules_cases.json +67 -0
- {gravity_cli-0.6.0 → gravity_cli-0.7.0}/packages/gravity-schema/pyproject.toml +1 -1
- {gravity_cli-0.6.0 → gravity_cli-0.7.0}/packages/gravity-schema/schema/manifest.schema.json +331 -1
- gravity_cli-0.7.0/packages/gravity-schema/tests/test_actions_memory.py +78 -0
- gravity_cli-0.7.0/packages/gravity-schema/tests/test_rules.py +88 -0
- {gravity_cli-0.6.0 → gravity_cli-0.7.0}/pyproject.toml +2 -2
- {gravity_cli-0.6.0 → gravity_cli-0.7.0}/src/gravity_cli/BUILDER_GUIDE.md +51 -0
- {gravity_cli-0.6.0 → gravity_cli-0.7.0}/src/gravity_cli/CHANGELOG.md +8 -0
- {gravity_cli-0.6.0 → gravity_cli-0.7.0}/src/gravity_cli/cli.py +2 -0
- gravity_cli-0.7.0/src/gravity_cli/commands/memory.py +63 -0
- {gravity_cli-0.6.0 → gravity_cli-0.7.0}/src/gravity_cli/commands/run.py +52 -16
- {gravity_cli-0.6.0 → gravity_cli-0.7.0}/src/gravity_cli/commands/schedule.py +3 -8
- {gravity_cli-0.6.0 → gravity_cli-0.7.0}/src/gravity_cli/dev.py +7 -3
- {gravity_cli-0.6.0 → gravity_cli-0.7.0}/src/gravity_cli/dev_page/index.html +63 -13
- {gravity_cli-0.6.0 → gravity_cli-0.7.0}/src/gravity_cli/docs/Gravity.agent.md +211 -58
- gravity_cli-0.7.0/src/gravity_cli/inputs.py +56 -0
- gravity_cli-0.7.0/tests/test_inputs_memory.py +72 -0
- {gravity_cli-0.6.0 → gravity_cli-0.7.0}/uv.lock +2 -2
- {gravity_cli-0.6.0 → gravity_cli-0.7.0}/.env.example +0 -0
- {gravity_cli-0.6.0 → gravity_cli-0.7.0}/.github/workflows/ci.yml +0 -0
- {gravity_cli-0.6.0 → gravity_cli-0.7.0}/.github/workflows/release.yml +0 -0
- {gravity_cli-0.6.0 → gravity_cli-0.7.0}/.gitignore +0 -0
- {gravity_cli-0.6.0 → gravity_cli-0.7.0}/AGENT_STANDARD.md +0 -0
- {gravity_cli-0.6.0 → gravity_cli-0.7.0}/LICENSE +0 -0
- {gravity_cli-0.6.0 → gravity_cli-0.7.0}/NOTICE +0 -0
- {gravity_cli-0.6.0 → gravity_cli-0.7.0}/README.md +0 -0
- {gravity_cli-0.6.0 → gravity_cli-0.7.0}/RELEASING.md +0 -0
- {gravity_cli-0.6.0 → gravity_cli-0.7.0}/TEMPLATES.md +0 -0
- {gravity_cli-0.6.0 → gravity_cli-0.7.0}/packages/gravity-schema/LICENSE +0 -0
- {gravity_cli-0.6.0 → gravity_cli-0.7.0}/packages/gravity-schema/NOTICE +0 -0
- {gravity_cli-0.6.0 → gravity_cli-0.7.0}/packages/gravity-schema/README.md +0 -0
- {gravity_cli-0.6.0 → gravity_cli-0.7.0}/packages/gravity-schema/gravity_schema/__main__.py +0 -0
- {gravity_cli-0.6.0 → gravity_cli-0.7.0}/packages/gravity-schema/gravity_schema/catalog.py +0 -0
- {gravity_cli-0.6.0 → gravity_cli-0.7.0}/packages/gravity-schema/gravity_schema/catalog.yaml +0 -0
- {gravity_cli-0.6.0 → gravity_cli-0.7.0}/packages/gravity-schema/gravity_schema/catalog_imported.yaml +0 -0
- {gravity_cli-0.6.0 → gravity_cli-0.7.0}/packages/gravity-schema/gravity_schema/cron.py +0 -0
- {gravity_cli-0.6.0 → gravity_cli-0.7.0}/packages/gravity-schema/gravity_schema/export.py +0 -0
- {gravity_cli-0.6.0 → gravity_cli-0.7.0}/packages/gravity-schema/gravity_schema/gen_catalog.py +0 -0
- {gravity_cli-0.6.0 → gravity_cli-0.7.0}/packages/gravity-schema/gravity_schema/source_scan.py +0 -0
- {gravity_cli-0.6.0 → gravity_cli-0.7.0}/packages/gravity-schema/gravity_schema/validation.py +0 -0
- {gravity_cli-0.6.0 → gravity_cli-0.7.0}/packages/gravity-schema/tests/conftest.py +0 -0
- {gravity_cli-0.6.0 → gravity_cli-0.7.0}/packages/gravity-schema/tests/test_catalog_parity.py +0 -0
- {gravity_cli-0.6.0 → gravity_cli-0.7.0}/packages/gravity-schema/tests/test_cron.py +0 -0
- {gravity_cli-0.6.0 → gravity_cli-0.7.0}/packages/gravity-schema/tests/test_errors.py +0 -0
- {gravity_cli-0.6.0 → gravity_cli-0.7.0}/packages/gravity-schema/tests/test_json_schema.py +0 -0
- {gravity_cli-0.6.0 → gravity_cli-0.7.0}/packages/gravity-schema/tests/test_kind_outputs.py +0 -0
- {gravity_cli-0.6.0 → gravity_cli-0.7.0}/packages/gravity-schema/tests/test_manifest.py +0 -0
- {gravity_cli-0.6.0 → gravity_cli-0.7.0}/packages/gravity-schema/tests/test_slots.py +0 -0
- {gravity_cli-0.6.0 → gravity_cli-0.7.0}/packages/gravity-schema/tests/test_source_scan.py +0 -0
- {gravity_cli-0.6.0 → gravity_cli-0.7.0}/packages/gravity-schema/tests/test_validation.py +0 -0
- {gravity_cli-0.6.0 → gravity_cli-0.7.0}/src/gravity_cli/__init__.py +0 -0
- {gravity_cli-0.6.0 → gravity_cli-0.7.0}/src/gravity_cli/__main__.py +0 -0
- {gravity_cli-0.6.0 → gravity_cli-0.7.0}/src/gravity_cli/_runner_script.py +0 -0
- {gravity_cli-0.6.0 → gravity_cli-0.7.0}/src/gravity_cli/api.py +0 -0
- {gravity_cli-0.6.0 → gravity_cli-0.7.0}/src/gravity_cli/changelog.py +0 -0
- {gravity_cli-0.6.0 → gravity_cli-0.7.0}/src/gravity_cli/commands/__init__.py +0 -0
- {gravity_cli-0.6.0 → gravity_cli-0.7.0}/src/gravity_cli/commands/adapt.py +0 -0
- {gravity_cli-0.6.0 → gravity_cli-0.7.0}/src/gravity_cli/commands/build.py +0 -0
- {gravity_cli-0.6.0 → gravity_cli-0.7.0}/src/gravity_cli/commands/changelog.py +0 -0
- {gravity_cli-0.6.0 → gravity_cli-0.7.0}/src/gravity_cli/commands/connect.py +0 -0
- {gravity_cli-0.6.0 → gravity_cli-0.7.0}/src/gravity_cli/commands/dev.py +0 -0
- {gravity_cli-0.6.0 → gravity_cli-0.7.0}/src/gravity_cli/commands/docs.py +0 -0
- {gravity_cli-0.6.0 → gravity_cli-0.7.0}/src/gravity_cli/commands/guide.py +0 -0
- {gravity_cli-0.6.0 → gravity_cli-0.7.0}/src/gravity_cli/commands/init.py +0 -0
- {gravity_cli-0.6.0 → gravity_cli-0.7.0}/src/gravity_cli/commands/login.py +0 -0
- {gravity_cli-0.6.0 → gravity_cli-0.7.0}/src/gravity_cli/commands/push.py +0 -0
- {gravity_cli-0.6.0 → gravity_cli-0.7.0}/src/gravity_cli/commands/runs.py +0 -0
- {gravity_cli-0.6.0 → gravity_cli-0.7.0}/src/gravity_cli/commands/tools.py +0 -0
- {gravity_cli-0.6.0 → gravity_cli-0.7.0}/src/gravity_cli/commands/update.py +0 -0
- {gravity_cli-0.6.0 → gravity_cli-0.7.0}/src/gravity_cli/commands/validate.py +0 -0
- {gravity_cli-0.6.0 → gravity_cli-0.7.0}/src/gravity_cli/config.py +0 -0
- {gravity_cli-0.6.0 → gravity_cli-0.7.0}/src/gravity_cli/docs/problem-to-agent.md +0 -0
- {gravity_cli-0.6.0 → gravity_cli-0.7.0}/src/gravity_cli/localrun.py +0 -0
- {gravity_cli-0.6.0 → gravity_cli-0.7.0}/src/gravity_cli/manifest.py +0 -0
- {gravity_cli-0.6.0 → gravity_cli-0.7.0}/src/gravity_cli/project.py +0 -0
- {gravity_cli-0.6.0 → gravity_cli-0.7.0}/src/gravity_cli/templates/chat/.gitignore +0 -0
- {gravity_cli-0.6.0 → gravity_cli-0.7.0}/src/gravity_cli/templates/chat/manifest.yaml +0 -0
- {gravity_cli-0.6.0 → gravity_cli-0.7.0}/src/gravity_cli/templates/chat/requirements.txt +0 -0
- {gravity_cli-0.6.0 → gravity_cli-0.7.0}/src/gravity_cli/templates/chat/src/agent.py +0 -0
- {gravity_cli-0.6.0 → gravity_cli-0.7.0}/src/gravity_cli/templates/deepagent/.gitignore +0 -0
- {gravity_cli-0.6.0 → gravity_cli-0.7.0}/src/gravity_cli/templates/deepagent/manifest.yaml +0 -0
- {gravity_cli-0.6.0 → gravity_cli-0.7.0}/src/gravity_cli/templates/deepagent/requirements.txt +0 -0
- {gravity_cli-0.6.0 → gravity_cli-0.7.0}/src/gravity_cli/templates/deepagent/src/agent.py +0 -0
- {gravity_cli-0.6.0 → gravity_cli-0.7.0}/src/gravity_cli/templates/minimal/.gitignore +0 -0
- {gravity_cli-0.6.0 → gravity_cli-0.7.0}/src/gravity_cli/templates/minimal/manifest.yaml +0 -0
- {gravity_cli-0.6.0 → gravity_cli-0.7.0}/src/gravity_cli/templates/minimal/requirements.txt +0 -0
- {gravity_cli-0.6.0 → gravity_cli-0.7.0}/src/gravity_cli/templates/minimal/src/agent.py +0 -0
- {gravity_cli-0.6.0 → gravity_cli-0.7.0}/src/gravity_cli/templates/structured/.gitignore +0 -0
- {gravity_cli-0.6.0 → gravity_cli-0.7.0}/src/gravity_cli/templates/structured/manifest.yaml +0 -0
- {gravity_cli-0.6.0 → gravity_cli-0.7.0}/src/gravity_cli/templates/structured/requirements.txt +0 -0
- {gravity_cli-0.6.0 → gravity_cli-0.7.0}/src/gravity_cli/templates/structured/src/agent.py +0 -0
- {gravity_cli-0.6.0 → gravity_cli-0.7.0}/src/gravity_cli/templates/structured/src/gateway.py +0 -0
- {gravity_cli-0.6.0 → gravity_cli-0.7.0}/src/gravity_cli/templates/structured/src/nodes/agent.py +0 -0
- {gravity_cli-0.6.0 → gravity_cli-0.7.0}/src/gravity_cli/templates/structured/src/prompts.py +0 -0
- {gravity_cli-0.6.0 → gravity_cli-0.7.0}/src/gravity_cli/templates/structured/src/state.py +0 -0
- {gravity_cli-0.6.0 → gravity_cli-0.7.0}/src/gravity_cli/timeline.py +0 -0
- {gravity_cli-0.6.0 → gravity_cli-0.7.0}/src/gravity_cli/ui.py +0 -0
- {gravity_cli-0.6.0 → gravity_cli-0.7.0}/src/gravity_cli/updater.py +0 -0
- {gravity_cli-0.6.0 → gravity_cli-0.7.0}/src/gravity_cli/workspace.py +0 -0
- {gravity_cli-0.6.0 → gravity_cli-0.7.0}/starter/manifest.yaml +0 -0
- {gravity_cli-0.6.0 → gravity_cli-0.7.0}/starter/requirements.txt +0 -0
- {gravity_cli-0.6.0 → gravity_cli-0.7.0}/starter/src/agent.py +0 -0
- {gravity_cli-0.6.0 → gravity_cli-0.7.0}/starter/src/summary.py +0 -0
- {gravity_cli-0.6.0 → gravity_cli-0.7.0}/starter/tests/test_summary.py +0 -0
- {gravity_cli-0.6.0 → gravity_cli-0.7.0}/tests/conftest.py +0 -0
- {gravity_cli-0.6.0 → gravity_cli-0.7.0}/tests/test_adapt.py +0 -0
- {gravity_cli-0.6.0 → gravity_cli-0.7.0}/tests/test_agent_guide.py +0 -0
- {gravity_cli-0.6.0 → gravity_cli-0.7.0}/tests/test_agents.py +0 -0
- {gravity_cli-0.6.0 → gravity_cli-0.7.0}/tests/test_api.py +0 -0
- {gravity_cli-0.6.0 → gravity_cli-0.7.0}/tests/test_build_push.py +0 -0
- {gravity_cli-0.6.0 → gravity_cli-0.7.0}/tests/test_changelog.py +0 -0
- {gravity_cli-0.6.0 → gravity_cli-0.7.0}/tests/test_connect.py +0 -0
- {gravity_cli-0.6.0 → gravity_cli-0.7.0}/tests/test_dev.py +0 -0
- {gravity_cli-0.6.0 → gravity_cli-0.7.0}/tests/test_gateway_token.py +0 -0
- {gravity_cli-0.6.0 → gravity_cli-0.7.0}/tests/test_localrun.py +0 -0
- {gravity_cli-0.6.0 → gravity_cli-0.7.0}/tests/test_manifest_integration.py +0 -0
- {gravity_cli-0.6.0 → gravity_cli-0.7.0}/tests/test_run.py +0 -0
- {gravity_cli-0.6.0 → gravity_cli-0.7.0}/tests/test_runs.py +0 -0
- {gravity_cli-0.6.0 → gravity_cli-0.7.0}/tests/test_schedule.py +0 -0
- {gravity_cli-0.6.0 → gravity_cli-0.7.0}/tests/test_session.py +0 -0
- {gravity_cli-0.6.0 → gravity_cli-0.7.0}/tests/test_templates.py +0 -0
- {gravity_cli-0.6.0 → gravity_cli-0.7.0}/tests/test_timeline.py +0 -0
- {gravity_cli-0.6.0 → gravity_cli-0.7.0}/tests/test_tools.py +0 -0
- {gravity_cli-0.6.0 → gravity_cli-0.7.0}/tests/test_ui.py +0 -0
- {gravity_cli-0.6.0 → gravity_cli-0.7.0}/tests/test_updater.py +0 -0
- {gravity_cli-0.6.0 → gravity_cli-0.7.0}/tests/test_validate.py +0 -0
- {gravity_cli-0.6.0 → gravity_cli-0.7.0}/tests/test_welcome.py +0 -0
- {gravity_cli-0.6.0 → gravity_cli-0.7.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.7.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.7.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
|
|
@@ -22,13 +22,19 @@ from .errors import (
|
|
|
22
22
|
)
|
|
23
23
|
from .export import manifest_json_schema
|
|
24
24
|
from .manifest import (
|
|
25
|
+
ActionSpec,
|
|
25
26
|
ApprovalsSpec,
|
|
27
|
+
ChooseOneSpec,
|
|
28
|
+
CollectionSpec,
|
|
26
29
|
DependenciesSpec,
|
|
27
30
|
InputSpec,
|
|
28
31
|
InputType,
|
|
29
32
|
Manifest,
|
|
33
|
+
MemoryScope,
|
|
34
|
+
MemorySpec,
|
|
30
35
|
ModelsSpec,
|
|
31
36
|
ModelTier,
|
|
37
|
+
OfferAs,
|
|
32
38
|
PermissionsSpec,
|
|
33
39
|
ResourcesSpec,
|
|
34
40
|
RuntimeSpec,
|
|
@@ -36,6 +42,7 @@ from .manifest import (
|
|
|
36
42
|
StateSpec,
|
|
37
43
|
TriggersSpec,
|
|
38
44
|
)
|
|
45
|
+
from .rules import apply_rules, chosen_actions, problems, visible_inputs
|
|
39
46
|
from .source_scan import is_agent_source, undeclared_write_tools
|
|
40
47
|
from .validation import (
|
|
41
48
|
check_imported_write_approvals,
|
|
@@ -70,6 +77,17 @@ __all__ = [ # noqa: RUF022 - grouped by concern, not alphabetical
|
|
|
70
77
|
"ScheduleSpec",
|
|
71
78
|
"ApprovalsSpec",
|
|
72
79
|
"StateSpec",
|
|
80
|
+
"MemorySpec",
|
|
81
|
+
"MemoryScope",
|
|
82
|
+
"CollectionSpec",
|
|
83
|
+
"ChooseOneSpec",
|
|
84
|
+
"ActionSpec",
|
|
85
|
+
"OfferAs",
|
|
86
|
+
# input rules
|
|
87
|
+
"visible_inputs",
|
|
88
|
+
"apply_rules",
|
|
89
|
+
"problems",
|
|
90
|
+
"chosen_actions",
|
|
73
91
|
# validation
|
|
74
92
|
"validate_manifest",
|
|
75
93
|
"load_manifest_text",
|
|
@@ -67,6 +67,22 @@ MAX_MEMORY_MB: Final[int] = 4096
|
|
|
67
67
|
MIN_STATE_KB: Final[int] = 1
|
|
68
68
|
MAX_STATE_KB: Final[int] = 256
|
|
69
69
|
|
|
70
|
+
# Memory collections (manifest `memory.collections`): keyed records, per user or per thread.
|
|
71
|
+
MAX_COLLECTIONS: Final[int] = 20
|
|
72
|
+
MAX_ITEM_KB: Final[int] = 64
|
|
73
|
+
DEFAULT_ITEM_KB: Final[int] = 16
|
|
74
|
+
MAX_ITEMS_PER_COLLECTION: Final[int] = 10_000
|
|
75
|
+
MAX_KEEP_DAYS: Final[int] = 730
|
|
76
|
+
DEFAULT_KEEP_DAYS: Final[int] = 30
|
|
77
|
+
# Across every collection and the notebook, one user's memory of one agent.
|
|
78
|
+
MAX_MEMORY_BYTES_PER_USER: Final[int] = 25 * 1024 * 1024
|
|
79
|
+
MAX_MEMORY_WRITES_PER_RUN: Final[int] = 5_000
|
|
80
|
+
# memory.previous(): the last finished run's result, passed whole up to this size.
|
|
81
|
+
MAX_PREVIOUS_KB: Final[int] = 256
|
|
82
|
+
|
|
83
|
+
# Declared actions (manifest `actions`).
|
|
84
|
+
MAX_ACTIONS: Final[int] = 20
|
|
85
|
+
|
|
70
86
|
|
|
71
87
|
# Generous bounds, so a pathological manifest can't tie up the validator.
|
|
72
88
|
|
|
@@ -36,6 +36,9 @@ class ErrorCode(str, Enum):
|
|
|
36
36
|
INVALID_STATE = "INVALID_STATE"
|
|
37
37
|
INVALID_KIND = "INVALID_KIND"
|
|
38
38
|
INVALID_OUTPUT = "INVALID_OUTPUT"
|
|
39
|
+
INVALID_CONDITION = "INVALID_CONDITION"
|
|
40
|
+
INVALID_ACTION = "INVALID_ACTION"
|
|
41
|
+
INVALID_MEMORY = "INVALID_MEMORY"
|
|
39
42
|
LOCKFILE_MISMATCH = "LOCKFILE_MISMATCH"
|
|
40
43
|
MISSING_FIELD = "MISSING_FIELD"
|
|
41
44
|
UNKNOWN_FIELD = "UNKNOWN_FIELD"
|
|
@@ -34,6 +34,10 @@ class InputType(str, Enum):
|
|
|
34
34
|
SECRET = "secret"
|
|
35
35
|
# The agent gets a short-lived URL it fetches through the gateway.
|
|
36
36
|
FILE = "file"
|
|
37
|
+
# Several of `options`; the value is a list.
|
|
38
|
+
MULTI_SELECT = "multi-select"
|
|
39
|
+
# One of this agent's declared `actions`; the chosen action runs (rules.py).
|
|
40
|
+
ACTION_CHOICE = "action-choice"
|
|
37
41
|
|
|
38
42
|
|
|
39
43
|
# Which Python types each input's `default` may be.
|
|
@@ -48,8 +52,14 @@ _PYTHON_TYPES: dict[InputType, tuple[type, ...]] = {
|
|
|
48
52
|
InputType.DATE: (str,),
|
|
49
53
|
InputType.SECRET: (str,),
|
|
50
54
|
InputType.FILE: (), # never has a default
|
|
55
|
+
InputType.MULTI_SELECT: (list,),
|
|
56
|
+
InputType.ACTION_CHOICE: (str,),
|
|
51
57
|
}
|
|
52
58
|
|
|
59
|
+
# A `show_if` value: what the named input or slot must be for this question to be asked.
|
|
60
|
+
Scalar = str | bool | int | float
|
|
61
|
+
ShowIf = dict[str, Scalar | list[Scalar]]
|
|
62
|
+
|
|
53
63
|
|
|
54
64
|
class ModelTier(str, Enum):
|
|
55
65
|
"""Capability tier, not a model name - the LLM gateway resolves it."""
|
|
@@ -189,6 +199,13 @@ class InputSpec(BaseModel):
|
|
|
189
199
|
# File inputs only: extensions without the dot. None accepts any file.
|
|
190
200
|
accept: Annotated[list[str] | None, Field(max_length=C.MAX_FILE_EXTENSIONS)] = None
|
|
191
201
|
default: Any | None = None
|
|
202
|
+
# Asked only when every named earlier input or slot has one of the given values (rules.py).
|
|
203
|
+
show_if: Annotated[ShowIf | None, Field(max_length=C.MAX_INPUTS)] = None
|
|
204
|
+
# multi-select only: how many options may be picked.
|
|
205
|
+
min: Annotated[int | None, Field(ge=0, le=C.MAX_OPTIONS)] = None
|
|
206
|
+
max: Annotated[int | None, Field(ge=1, le=C.MAX_OPTIONS)] = None
|
|
207
|
+
# action-choice only: the declared actions offered, in order.
|
|
208
|
+
actions: Annotated[list[str] | None, Field(max_length=C.MAX_ACTIONS)] = None
|
|
192
209
|
|
|
193
210
|
@field_validator("name")
|
|
194
211
|
@classmethod
|
|
@@ -210,28 +227,69 @@ class InputSpec(BaseModel):
|
|
|
210
227
|
|
|
211
228
|
@model_validator(mode="after")
|
|
212
229
|
def _check_options(self) -> InputSpec:
|
|
213
|
-
is_dropdown = self.type
|
|
230
|
+
is_dropdown = self.type in (InputType.STATIC_DROPDOWN, InputType.MULTI_SELECT)
|
|
214
231
|
|
|
215
232
|
if is_dropdown and not self.options:
|
|
216
233
|
raise PydanticCustomError(
|
|
217
234
|
ErrorCode.INVALID_INPUT.value,
|
|
218
|
-
"Input '{value}' is a
|
|
235
|
+
"Input '{value}' is a {type} but declares no options.",
|
|
219
236
|
{
|
|
220
237
|
"value": self.name,
|
|
238
|
+
"type": self.type.value,
|
|
221
239
|
"remediation": "Add an `options:` list, or use type 'text'.",
|
|
222
240
|
},
|
|
223
241
|
)
|
|
224
242
|
if self.options and not is_dropdown:
|
|
225
243
|
raise PydanticCustomError(
|
|
226
244
|
ErrorCode.INVALID_INPUT.value,
|
|
227
|
-
"Input '{value}' declares options but is not a static-dropdown.",
|
|
245
|
+
"Input '{value}' declares options but is not a static-dropdown or multi-select.",
|
|
228
246
|
{
|
|
229
247
|
"value": self.name,
|
|
230
|
-
"remediation": "Set type to 'static-dropdown', or remove `options`.",
|
|
248
|
+
"remediation": "Set type to 'static-dropdown' or 'multi-select', or remove `options`.",
|
|
231
249
|
},
|
|
232
250
|
)
|
|
233
251
|
return self
|
|
234
252
|
|
|
253
|
+
@model_validator(mode="after")
|
|
254
|
+
def _check_choice_fields(self) -> InputSpec:
|
|
255
|
+
multi = self.type is InputType.MULTI_SELECT
|
|
256
|
+
if (self.min is not None or self.max is not None) and not multi:
|
|
257
|
+
raise PydanticCustomError(
|
|
258
|
+
ErrorCode.INVALID_INPUT.value,
|
|
259
|
+
"Input '{value}' sets min or max but is not a multi-select.",
|
|
260
|
+
{"value": self.name, "remediation": "Set type to 'multi-select', or remove min and max."},
|
|
261
|
+
)
|
|
262
|
+
if multi:
|
|
263
|
+
count = len(self.options or [])
|
|
264
|
+
lo, hi = self.min or 0, self.max if self.max is not None else count
|
|
265
|
+
if lo > hi or hi > count:
|
|
266
|
+
raise PydanticCustomError(
|
|
267
|
+
ErrorCode.INVALID_INPUT.value,
|
|
268
|
+
"Input '{value}' asks for between {lo} and {hi} picks of {count} options.",
|
|
269
|
+
{"value": self.name, "lo": lo, "hi": hi, "count": count,
|
|
270
|
+
"remediation": "Keep min <= max <= the number of options."},
|
|
271
|
+
)
|
|
272
|
+
choice = self.type is InputType.ACTION_CHOICE
|
|
273
|
+
if choice and not self.actions:
|
|
274
|
+
raise PydanticCustomError(
|
|
275
|
+
ErrorCode.INVALID_INPUT.value,
|
|
276
|
+
"Input '{value}' is an action-choice but lists no actions.",
|
|
277
|
+
{"value": self.name, "remediation": "Add `actions:` with the names of declared actions."},
|
|
278
|
+
)
|
|
279
|
+
if self.actions is not None and not choice:
|
|
280
|
+
raise PydanticCustomError(
|
|
281
|
+
ErrorCode.INVALID_INPUT.value,
|
|
282
|
+
"Input '{value}' lists actions but is not an action-choice.",
|
|
283
|
+
{"value": self.name, "remediation": "Set type to 'action-choice', or remove `actions`."},
|
|
284
|
+
)
|
|
285
|
+
if self.actions and len(set(self.actions)) != len(self.actions):
|
|
286
|
+
raise PydanticCustomError(
|
|
287
|
+
ErrorCode.INVALID_INPUT.value,
|
|
288
|
+
"Input '{value}' lists an action twice.",
|
|
289
|
+
{"value": self.name, "remediation": "List each action once."},
|
|
290
|
+
)
|
|
291
|
+
return self
|
|
292
|
+
|
|
235
293
|
@model_validator(mode="after")
|
|
236
294
|
def _check_accept(self) -> InputSpec:
|
|
237
295
|
if self.accept is None:
|
|
@@ -304,13 +362,15 @@ class InputSpec(BaseModel):
|
|
|
304
362
|
},
|
|
305
363
|
)
|
|
306
364
|
|
|
307
|
-
|
|
365
|
+
picks = self.default if isinstance(self.default, list) else [self.default]
|
|
366
|
+
allowed = self.options if self.options else self.actions
|
|
367
|
+
if allowed and any(p not in allowed for p in picks):
|
|
308
368
|
raise PydanticCustomError(
|
|
309
369
|
ErrorCode.INVALID_INPUT.value,
|
|
310
370
|
"Default for input '{value}' is not one of its options.",
|
|
311
371
|
{
|
|
312
372
|
"value": self.name,
|
|
313
|
-
"remediation": f"Use one of: {', '.join(
|
|
373
|
+
"remediation": f"Use one of: {', '.join(allowed)}.",
|
|
314
374
|
},
|
|
315
375
|
)
|
|
316
376
|
return self
|
|
@@ -462,6 +522,51 @@ class StateSpec(BaseModel):
|
|
|
462
522
|
max_kb: Annotated[int, Field(ge=C.MIN_STATE_KB, le=C.MAX_STATE_KB)] = C.MAX_STATE_KB
|
|
463
523
|
|
|
464
524
|
|
|
525
|
+
class MemoryScope(str, Enum):
|
|
526
|
+
"""Whose memory: shared by all of one user's conversations with the agent, or one conversation's."""
|
|
527
|
+
|
|
528
|
+
USER = "user"
|
|
529
|
+
THREAD = "thread"
|
|
530
|
+
|
|
531
|
+
|
|
532
|
+
class CollectionSpec(BaseModel):
|
|
533
|
+
"""Keyed records the agent keeps across runs. Every item expires `keep_days` after its last write."""
|
|
534
|
+
|
|
535
|
+
model_config = _STRICT
|
|
536
|
+
|
|
537
|
+
per: MemoryScope = MemoryScope.USER
|
|
538
|
+
keep_days: Annotated[int, Field(ge=1, le=C.MAX_KEEP_DAYS)] = C.DEFAULT_KEEP_DAYS
|
|
539
|
+
max_items: Annotated[int, Field(ge=1, le=C.MAX_ITEMS_PER_COLLECTION)] = C.MAX_ITEMS_PER_COLLECTION
|
|
540
|
+
max_item_kb: Annotated[int, Field(ge=1, le=C.MAX_ITEM_KB)] = C.DEFAULT_ITEM_KB
|
|
541
|
+
|
|
542
|
+
|
|
543
|
+
class MemorySpec(BaseModel):
|
|
544
|
+
"""Memory across runs: the small notebook (what `state.max_kb` was) and named collections."""
|
|
545
|
+
|
|
546
|
+
model_config = _STRICT
|
|
547
|
+
|
|
548
|
+
notebook_kb: Annotated[int, Field(ge=C.MIN_STATE_KB, le=C.MAX_STATE_KB)] | None = None
|
|
549
|
+
notebook_per: MemoryScope = MemoryScope.USER
|
|
550
|
+
collections: dict[str, CollectionSpec] = Field(default_factory=dict)
|
|
551
|
+
|
|
552
|
+
@model_validator(mode="after")
|
|
553
|
+
def _check_collections(self) -> MemorySpec:
|
|
554
|
+
if len(self.collections) > C.MAX_COLLECTIONS:
|
|
555
|
+
raise PydanticCustomError(
|
|
556
|
+
ErrorCode.LIMIT_EXCEEDED.value,
|
|
557
|
+
"At most {limit} memory collections.",
|
|
558
|
+
{"limit": C.MAX_COLLECTIONS, "remediation": "Merge collections, or keep several kinds of record in one."},
|
|
559
|
+
)
|
|
560
|
+
for name in self.collections:
|
|
561
|
+
if not C.INPUT_NAME_RE.match(name):
|
|
562
|
+
raise PydanticCustomError(
|
|
563
|
+
ErrorCode.INVALID_MEMORY.value,
|
|
564
|
+
"Collection name '{value}' is not a valid identifier.",
|
|
565
|
+
{"value": name, "remediation": "Use lowercase letters, digits and underscores."},
|
|
566
|
+
)
|
|
567
|
+
return self
|
|
568
|
+
|
|
569
|
+
|
|
465
570
|
class OutputSpec(BaseModel):
|
|
466
571
|
"""One rendering of the run's corpus a consumer may pick; the platform delivers it, not the agent."""
|
|
467
572
|
|
|
@@ -499,6 +604,8 @@ class SlotSpec(BaseModel):
|
|
|
499
604
|
label: Annotated[str, Field(max_length=C.MAX_LABEL_LENGTH)]
|
|
500
605
|
default: str
|
|
501
606
|
adapters: dict[str, list[str]] # adapter name -> the catalog tools it calls
|
|
607
|
+
# The consumer may pick several adapters; the pick is a list and every pick's tools are granted.
|
|
608
|
+
multiple: bool = False
|
|
502
609
|
|
|
503
610
|
@model_validator(mode="after")
|
|
504
611
|
def _check(self) -> SlotSpec:
|
|
@@ -550,6 +657,82 @@ class DisplaySpec(BaseModel):
|
|
|
550
657
|
chart: ChartHint | None = None
|
|
551
658
|
|
|
552
659
|
|
|
660
|
+
class ChooseOneSpec(BaseModel):
|
|
661
|
+
"""An "either / or" group: once one of these inputs is filled the others are hidden.
|
|
662
|
+
With `required`, one of them must be filled."""
|
|
663
|
+
|
|
664
|
+
model_config = _STRICT
|
|
665
|
+
|
|
666
|
+
label: Annotated[str, Field(max_length=C.MAX_LABEL_LENGTH)]
|
|
667
|
+
inputs: Annotated[list[str], Field(min_length=2, max_length=C.MAX_INPUTS)]
|
|
668
|
+
required: bool = False
|
|
669
|
+
|
|
670
|
+
|
|
671
|
+
class ActionSpec(BaseModel):
|
|
672
|
+
"""One thing the agent can do. The runner passes the chosen ones in state["actions"]."""
|
|
673
|
+
|
|
674
|
+
model_config = _STRICT
|
|
675
|
+
|
|
676
|
+
name: Annotated[str, Field(max_length=C.MAX_NAME_LENGTH)]
|
|
677
|
+
label: Annotated[str, Field(max_length=C.MAX_LABEL_LENGTH)]
|
|
678
|
+
# Runs on the first run of a conversation, with the other defaults, in declaration order.
|
|
679
|
+
default: bool = False
|
|
680
|
+
# Offered only once these actions have completed in the conversation.
|
|
681
|
+
requires: Annotated[list[str], Field(max_length=C.MAX_ACTIONS)] = Field(default_factory=list)
|
|
682
|
+
# Works on the rows the person selects in the result; their keys arrive as action_inputs["rows"].
|
|
683
|
+
per_row: bool = False
|
|
684
|
+
# Extra questions asked only for this action, under the same rules as top-level inputs.
|
|
685
|
+
inputs: Annotated[list[InputSpec], Field(max_length=C.MAX_INPUTS)] = Field(default_factory=list)
|
|
686
|
+
# Its own recurring run; the consumer turns it on.
|
|
687
|
+
schedule: ScheduleSpec | None = None
|
|
688
|
+
# Never scheduled and never repeated by a whole-flow schedule after it once succeeded.
|
|
689
|
+
run_once: bool = False
|
|
690
|
+
# Run these next, each as its own run on the same conversation, when this one completes.
|
|
691
|
+
then: Annotated[list[str], Field(max_length=C.MAX_ACTIONS)] = Field(default_factory=list)
|
|
692
|
+
|
|
693
|
+
@field_validator("name")
|
|
694
|
+
@classmethod
|
|
695
|
+
def _check_name(cls, v: str) -> str:
|
|
696
|
+
if not C.INPUT_NAME_RE.match(v):
|
|
697
|
+
raise PydanticCustomError(
|
|
698
|
+
ErrorCode.INVALID_ACTION.value,
|
|
699
|
+
"Action name '{value}' is not a valid identifier.",
|
|
700
|
+
{"value": v, "remediation": "Use lowercase letters, digits and underscores."},
|
|
701
|
+
)
|
|
702
|
+
return v
|
|
703
|
+
|
|
704
|
+
@model_validator(mode="after")
|
|
705
|
+
def _check(self) -> ActionSpec:
|
|
706
|
+
if self.run_once and self.schedule is not None:
|
|
707
|
+
raise PydanticCustomError(
|
|
708
|
+
ErrorCode.INVALID_ACTION.value,
|
|
709
|
+
"Action '{value}' is run_once, so it cannot have a schedule.",
|
|
710
|
+
{"value": self.name, "remediation": "Remove `schedule` or `run_once`."},
|
|
711
|
+
)
|
|
712
|
+
names = [i.name for i in self.inputs]
|
|
713
|
+
if len(set(names)) != len(names):
|
|
714
|
+
raise PydanticCustomError(
|
|
715
|
+
ErrorCode.DUPLICATE_INPUT.value,
|
|
716
|
+
"Action '{value}' declares an input twice.",
|
|
717
|
+
{"value": self.name, "remediation": "Rename or remove the duplicate input."},
|
|
718
|
+
)
|
|
719
|
+
for spec in self.inputs:
|
|
720
|
+
if spec.type is InputType.ACTION_CHOICE:
|
|
721
|
+
raise PydanticCustomError(
|
|
722
|
+
ErrorCode.INVALID_ACTION.value,
|
|
723
|
+
"Action '{value}' asks an action-choice question.",
|
|
724
|
+
{"value": self.name, "remediation": "Offer actions at the top level or under the output."},
|
|
725
|
+
)
|
|
726
|
+
return self
|
|
727
|
+
|
|
728
|
+
|
|
729
|
+
class OfferAs(str, Enum):
|
|
730
|
+
"""How the actions under a result are shown."""
|
|
731
|
+
|
|
732
|
+
BUTTONS = "buttons"
|
|
733
|
+
QUESTION = "question"
|
|
734
|
+
|
|
735
|
+
|
|
553
736
|
class Manifest(BaseModel):
|
|
554
737
|
"""A complete agent declaration. Structural only: tool existence is checked in validation.py."""
|
|
555
738
|
|
|
@@ -576,6 +759,14 @@ class Manifest(BaseModel):
|
|
|
576
759
|
outputs: dict[str, OutputSpec] = Field(default_factory=dict)
|
|
577
760
|
# How the result shows on screen; absent, the page decides from the result itself.
|
|
578
761
|
display: DisplaySpec | None = None
|
|
762
|
+
# "Either / or" input groups (rules.py).
|
|
763
|
+
choose_one: Annotated[list[ChooseOneSpec], Field(max_length=C.MAX_INPUTS)] = Field(default_factory=list)
|
|
764
|
+
# Memory across runs; `state` is the older spelling of the notebook alone.
|
|
765
|
+
memory: MemorySpec | None = None
|
|
766
|
+
# What the agent can do. Absent: one entry point, as always.
|
|
767
|
+
actions: Annotated[list[ActionSpec], Field(max_length=C.MAX_ACTIONS)] = Field(default_factory=list)
|
|
768
|
+
offer_as: OfferAs = OfferAs.BUTTONS
|
|
769
|
+
offer_label: Annotated[str, Field(max_length=C.MAX_LABEL_LENGTH)] | None = None
|
|
579
770
|
|
|
580
771
|
@field_validator("contract")
|
|
581
772
|
@classmethod
|
|
@@ -706,6 +897,133 @@ class Manifest(BaseModel):
|
|
|
706
897
|
)
|
|
707
898
|
return self
|
|
708
899
|
|
|
900
|
+
@model_validator(mode="after")
|
|
901
|
+
def _check_memory(self) -> Manifest:
|
|
902
|
+
if self.state is not None and self.memory is not None:
|
|
903
|
+
raise PydanticCustomError(
|
|
904
|
+
ErrorCode.INVALID_MEMORY.value,
|
|
905
|
+
"Both `state` and `memory` are declared.",
|
|
906
|
+
{"remediation": "Move state.max_kb to memory.notebook_kb and remove `state`."},
|
|
907
|
+
)
|
|
908
|
+
return self
|
|
909
|
+
|
|
910
|
+
@model_validator(mode="after")
|
|
911
|
+
def _check_actions(self) -> Manifest:
|
|
912
|
+
by_name: dict[str, ActionSpec] = {}
|
|
913
|
+
for a in self.actions:
|
|
914
|
+
if a.name in by_name:
|
|
915
|
+
raise PydanticCustomError(
|
|
916
|
+
ErrorCode.INVALID_ACTION.value,
|
|
917
|
+
"Action '{value}' is declared more than once.",
|
|
918
|
+
{"value": a.name, "remediation": "Rename or remove the duplicate action."},
|
|
919
|
+
)
|
|
920
|
+
by_name[a.name] = a
|
|
921
|
+
for a in self.actions:
|
|
922
|
+
for field in ("requires", "then"):
|
|
923
|
+
unknown = [n for n in getattr(a, field) if n not in by_name or n == a.name]
|
|
924
|
+
if unknown:
|
|
925
|
+
raise PydanticCustomError(
|
|
926
|
+
ErrorCode.INVALID_ACTION.value,
|
|
927
|
+
"Action '{value}' {field} '{other}', which is not another declared action.",
|
|
928
|
+
{"value": a.name, "field": field, "other": unknown[0],
|
|
929
|
+
"remediation": f"Use one of: {', '.join(n for n in by_name if n != a.name) or '(none)'}."},
|
|
930
|
+
)
|
|
931
|
+
for n in a.then:
|
|
932
|
+
if by_name[n].run_once:
|
|
933
|
+
raise PydanticCustomError(
|
|
934
|
+
ErrorCode.INVALID_ACTION.value,
|
|
935
|
+
"Action '{value}' chains into '{other}', which is run_once.",
|
|
936
|
+
{"value": a.name, "other": n, "remediation": "Chain only actions that may run again."},
|
|
937
|
+
)
|
|
938
|
+
for field in ("requires", "then"):
|
|
939
|
+
loop = _cycle({a.name: getattr(a, field) for a in self.actions})
|
|
940
|
+
if loop:
|
|
941
|
+
raise PydanticCustomError(
|
|
942
|
+
ErrorCode.INVALID_ACTION.value,
|
|
943
|
+
"Actions loop through `{field}`: {value}.",
|
|
944
|
+
{"field": field, "value": " -> ".join(loop), "remediation": "Break the loop."},
|
|
945
|
+
)
|
|
946
|
+
# Default actions run first, in order, so each one's requirements must be earlier defaults.
|
|
947
|
+
done: set[str] = set()
|
|
948
|
+
for a in self.actions:
|
|
949
|
+
if not a.default:
|
|
950
|
+
continue
|
|
951
|
+
missing = [n for n in a.requires if n not in done]
|
|
952
|
+
if missing:
|
|
953
|
+
raise PydanticCustomError(
|
|
954
|
+
ErrorCode.INVALID_ACTION.value,
|
|
955
|
+
"Default action '{value}' requires '{other}', which does not run before it.",
|
|
956
|
+
{"value": a.name, "other": missing[0],
|
|
957
|
+
"remediation": "Make that action a default declared earlier, or drop `default`."},
|
|
958
|
+
)
|
|
959
|
+
done.add(a.name)
|
|
960
|
+
if self.actions and not done:
|
|
961
|
+
raise PydanticCustomError(
|
|
962
|
+
ErrorCode.INVALID_ACTION.value,
|
|
963
|
+
"No action is marked `default: true`, so a first run would do nothing.",
|
|
964
|
+
{"remediation": "Mark the first action(s) default: true."},
|
|
965
|
+
)
|
|
966
|
+
for i in [*self.inputs, *(x for a in self.actions for x in a.inputs)]:
|
|
967
|
+
unknown = [n for n in i.actions or [] if n not in by_name]
|
|
968
|
+
if unknown:
|
|
969
|
+
raise PydanticCustomError(
|
|
970
|
+
ErrorCode.INVALID_ACTION.value,
|
|
971
|
+
"Input '{value}' offers action '{other}', which is not declared.",
|
|
972
|
+
{"value": i.name, "other": unknown[0], "remediation": "Declare it under `actions`."},
|
|
973
|
+
)
|
|
974
|
+
return self
|
|
975
|
+
|
|
976
|
+
@model_validator(mode="after")
|
|
977
|
+
def _check_conditions(self) -> Manifest:
|
|
978
|
+
# Slots are asked first, so any slot counts as earlier; an input only after it is declared.
|
|
979
|
+
_check_show_if(self.inputs, self.slots, "inputs")
|
|
980
|
+
for a in self.actions:
|
|
981
|
+
_check_show_if(a.inputs, self.slots, f"actions.{a.name}.inputs", earlier=self.inputs)
|
|
982
|
+
names = {i.name: i for i in self.inputs}
|
|
983
|
+
grouped: set[str] = set()
|
|
984
|
+
for g in self.choose_one:
|
|
985
|
+
for n in g.inputs:
|
|
986
|
+
if n not in names:
|
|
987
|
+
raise PydanticCustomError(
|
|
988
|
+
ErrorCode.INVALID_CONDITION.value,
|
|
989
|
+
"choose_one '{value}' names '{other}', which is not an input.",
|
|
990
|
+
{"value": g.label, "other": n, "remediation": "Name declared inputs only."},
|
|
991
|
+
)
|
|
992
|
+
if n in grouped:
|
|
993
|
+
raise PydanticCustomError(
|
|
994
|
+
ErrorCode.INVALID_CONDITION.value,
|
|
995
|
+
"Input '{value}' is in more than one choose_one group.",
|
|
996
|
+
{"value": n, "remediation": "Keep each input in one group."},
|
|
997
|
+
)
|
|
998
|
+
if names[n].required:
|
|
999
|
+
raise PydanticCustomError(
|
|
1000
|
+
ErrorCode.INVALID_CONDITION.value,
|
|
1001
|
+
"Input '{value}' is in a choose_one group and also required on its own.",
|
|
1002
|
+
{"value": n, "remediation": "Remove its `required`; set `required: true` on the group."},
|
|
1003
|
+
)
|
|
1004
|
+
grouped.add(n)
|
|
1005
|
+
if len(set(g.inputs)) != len(g.inputs):
|
|
1006
|
+
raise PydanticCustomError(
|
|
1007
|
+
ErrorCode.INVALID_CONDITION.value,
|
|
1008
|
+
"choose_one '{value}' names an input twice.",
|
|
1009
|
+
{"value": g.label, "remediation": "List each input once."},
|
|
1010
|
+
)
|
|
1011
|
+
return self
|
|
1012
|
+
|
|
1013
|
+
@property
|
|
1014
|
+
def notebook_kb(self) -> int | None:
|
|
1015
|
+
"""The notebook's cap, None when the agent keeps no notebook."""
|
|
1016
|
+
if self.memory is not None:
|
|
1017
|
+
return self.memory.notebook_kb
|
|
1018
|
+
return self.state.max_kb if self.state is not None else None
|
|
1019
|
+
|
|
1020
|
+
@property
|
|
1021
|
+
def default_actions(self) -> list[str]:
|
|
1022
|
+
return [a.name for a in self.actions if a.default]
|
|
1023
|
+
|
|
1024
|
+
def action(self, name: str) -> ActionSpec | None:
|
|
1025
|
+
return next((a for a in self.actions if a.name == name), None)
|
|
1026
|
+
|
|
709
1027
|
@property
|
|
710
1028
|
def declared_tools(self) -> frozenset[str]:
|
|
711
1029
|
"""Every tool this agent could ever be granted, across all adapters."""
|
|
@@ -718,9 +1036,11 @@ class Manifest(BaseModel):
|
|
|
718
1036
|
tools = list(self.permissions.tools)
|
|
719
1037
|
for name, slot in self.slots.items():
|
|
720
1038
|
pick = inputs.get(name, slot.default)
|
|
721
|
-
if pick
|
|
722
|
-
|
|
723
|
-
|
|
1039
|
+
picks = pick if slot.multiple and isinstance(pick, list) and pick else [pick]
|
|
1040
|
+
for p in picks:
|
|
1041
|
+
if not isinstance(p, str) or p not in slot.adapters:
|
|
1042
|
+
raise ValueError(f"{name}={p!r}: not an adapter of this agent ({', '.join(slot.adapters)})")
|
|
1043
|
+
tools += [t for t in slot.adapters[p] if t not in tools]
|
|
724
1044
|
return tools
|
|
725
1045
|
|
|
726
1046
|
def entrypoint_parts(self) -> tuple[str, str]:
|
|
@@ -728,5 +1048,78 @@ class Manifest(BaseModel):
|
|
|
728
1048
|
|
|
729
1049
|
def default_inputs(self) -> dict[str, Any]:
|
|
730
1050
|
return {i.name: i.default for i in self.inputs if i.default is not None} | {
|
|
731
|
-
name: s.default for name, s in self.slots.items()
|
|
1051
|
+
name: [s.default] if s.multiple else s.default for name, s in self.slots.items()
|
|
732
1052
|
}
|
|
1053
|
+
|
|
1054
|
+
|
|
1055
|
+
def _cycle(edges: dict[str, list[str]]) -> list[str] | None:
|
|
1056
|
+
"""A loop in `edges` as [a, b, ..., a], or None."""
|
|
1057
|
+
state: dict[str, int] = {} # 1 visiting, 2 done
|
|
1058
|
+
path: list[str] = []
|
|
1059
|
+
|
|
1060
|
+
def visit(n: str) -> list[str] | None:
|
|
1061
|
+
state[n] = 1
|
|
1062
|
+
path.append(n)
|
|
1063
|
+
for m in edges.get(n, []):
|
|
1064
|
+
if state.get(m) == 1:
|
|
1065
|
+
return [*path[path.index(m):], m]
|
|
1066
|
+
if state.get(m) is None and (found := visit(m)):
|
|
1067
|
+
return found
|
|
1068
|
+
path.pop()
|
|
1069
|
+
state[n] = 2
|
|
1070
|
+
return None
|
|
1071
|
+
|
|
1072
|
+
for n in edges:
|
|
1073
|
+
if state.get(n) is None and (found := visit(n)):
|
|
1074
|
+
return found
|
|
1075
|
+
return None
|
|
1076
|
+
|
|
1077
|
+
|
|
1078
|
+
def _check_show_if(
|
|
1079
|
+
inputs: list[InputSpec],
|
|
1080
|
+
slots: dict[str, SlotSpec],
|
|
1081
|
+
where: str,
|
|
1082
|
+
earlier: list[InputSpec] = (),
|
|
1083
|
+
) -> None:
|
|
1084
|
+
"""Each condition names a slot or an input asked before this one, with values it can take."""
|
|
1085
|
+
seen: dict[str, InputSpec] = {i.name: i for i in earlier}
|
|
1086
|
+
for spec in inputs:
|
|
1087
|
+
for ref, want in (spec.show_if or {}).items():
|
|
1088
|
+
values = want if isinstance(want, list) else [want]
|
|
1089
|
+
if not values:
|
|
1090
|
+
raise PydanticCustomError(
|
|
1091
|
+
ErrorCode.INVALID_CONDITION.value,
|
|
1092
|
+
"{where}.{value}.show_if.{ref} lists no values.",
|
|
1093
|
+
{"where": where, "value": spec.name, "ref": ref, "remediation": "Give one value or a list."},
|
|
1094
|
+
)
|
|
1095
|
+
if ref in slots:
|
|
1096
|
+
allowed: list[Any] | None = list(slots[ref].adapters)
|
|
1097
|
+
elif ref in seen:
|
|
1098
|
+
other = seen[ref]
|
|
1099
|
+
if other.type is InputType.BOOLEAN:
|
|
1100
|
+
allowed = [True, False]
|
|
1101
|
+
elif other.options:
|
|
1102
|
+
allowed = list(other.options)
|
|
1103
|
+
elif other.actions:
|
|
1104
|
+
allowed = list(other.actions)
|
|
1105
|
+
else:
|
|
1106
|
+
allowed = None # free text: any value
|
|
1107
|
+
else:
|
|
1108
|
+
raise PydanticCustomError(
|
|
1109
|
+
ErrorCode.INVALID_CONDITION.value,
|
|
1110
|
+
"{where}.{value}.show_if names '{ref}', which is not a slot or an input asked before it.",
|
|
1111
|
+
{"where": where, "value": spec.name, "ref": ref,
|
|
1112
|
+
"remediation": "Name a slot, or an input declared above this one."},
|
|
1113
|
+
)
|
|
1114
|
+
bad = [v for v in values if allowed is not None and v not in allowed]
|
|
1115
|
+
# True == 1 in Python: a boolean condition must be written as a boolean.
|
|
1116
|
+
if seen.get(ref) is not None and seen[ref].type is InputType.BOOLEAN:
|
|
1117
|
+
bad += [v for v in values if not isinstance(v, bool)]
|
|
1118
|
+
if bad:
|
|
1119
|
+
raise PydanticCustomError(
|
|
1120
|
+
ErrorCode.INVALID_CONDITION.value,
|
|
1121
|
+
"{where}.{value}.show_if.{ref} = {bad!r} is not a value '{ref}' can take.",
|
|
1122
|
+
{"where": where, "value": spec.name, "ref": ref, "bad": bad[0],
|
|
1123
|
+
"remediation": f"Use one of: {', '.join(map(str, allowed or []))}."},
|
|
1124
|
+
)
|
|
1125
|
+
seen[spec.name] = spec
|