agentship-cli 0.0.1__py3-none-any.whl
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.
- agentship_cli/main.py +1023 -0
- agentship_cli/migrations.py +82 -0
- agentship_cli/py.typed +0 -0
- agentship_cli/scaffold.py +271 -0
- agentship_cli/verify.py +391 -0
- agentship_cli-0.0.1.dist-info/METADATA +25 -0
- agentship_cli-0.0.1.dist-info/RECORD +9 -0
- agentship_cli-0.0.1.dist-info/WHEEL +4 -0
- agentship_cli-0.0.1.dist-info/entry_points.txt +2 -0
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"""The single, in-code registry of AgentShip database migrations.
|
|
2
|
+
|
|
3
|
+
This module is the *one owning place* for all future DDL. The migration policy
|
|
4
|
+
(DESIGN.md §13.9 / §13.11, CLEAN-BUILD-PLAN.md "Migration policy") is:
|
|
5
|
+
|
|
6
|
+
- All schema creation is **idempotent, version-stamped, and gated** behind
|
|
7
|
+
``agentship db upgrade --allow-migrations``.
|
|
8
|
+
- **No phase ships its own ungated runner.** Every later phase that needs DDL
|
|
9
|
+
(P02's checkpointer ``setup()``, P07's vault, P08's memory tables, P09's
|
|
10
|
+
``agent_tasks``) appends a :class:`Migration` to :data:`REGISTERED_MIGRATIONS`
|
|
11
|
+
here instead of building a separate alembic/raw-SQL runner.
|
|
12
|
+
- A deployment on ``InMemorySaver`` + env API keys needs **zero DDL, zero approval**.
|
|
13
|
+
DDL only matters once ``AGENT_SESSION_STORE_URI`` points at Postgres.
|
|
14
|
+
|
|
15
|
+
To register a migration, append a :class:`Migration` with a unique, sortable
|
|
16
|
+
``version`` (e.g. ``"0002_agent_tasks"``) and an ``apply`` callable that runs
|
|
17
|
+
**idempotent** DDL against the given database URL. The runner applies pending
|
|
18
|
+
migrations in ``version`` order; because each is idempotent, re-running
|
|
19
|
+
``db upgrade`` is safe.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
from collections.abc import Callable
|
|
25
|
+
from dataclasses import dataclass
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass(frozen=True)
|
|
29
|
+
class Migration:
|
|
30
|
+
"""One idempotent, version-stamped schema migration.
|
|
31
|
+
|
|
32
|
+
Attributes:
|
|
33
|
+
version: A unique, lexically sortable identifier (e.g.
|
|
34
|
+
``"0002_agent_tasks"``). Migrations are applied in ascending
|
|
35
|
+
``version`` order, so prefix with a zero-padded number.
|
|
36
|
+
description: A short human-readable summary shown in the upgrade plan.
|
|
37
|
+
apply: A callable taking the resolved ``database_url`` and performing
|
|
38
|
+
**idempotent** DDL (e.g. ``CREATE TABLE IF NOT EXISTS ...``).
|
|
39
|
+
Re-running an already-applied migration must be a safe no-op.
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
version: str
|
|
43
|
+
description: str
|
|
44
|
+
apply: Callable[[str], None]
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
#: The single owning list of migrations. **Empty by design** for Week-1 (zero
|
|
48
|
+
#: DDL). Later phases append their :class:`Migration` here — this is the only
|
|
49
|
+
#: sanctioned migration runner; no phase may ship its own.
|
|
50
|
+
def _create_checkpoint_tables(database_url: str) -> None:
|
|
51
|
+
"""Create the LangGraph checkpointer's tables, idempotently.
|
|
52
|
+
|
|
53
|
+
A durable agent (``durability: checkpoint``) reads and writes these on every node. Until
|
|
54
|
+
they exist, the first turn against a Postgres-backed deployment fails with
|
|
55
|
+
``relation "checkpoints" does not exist`` — which is what happens the moment
|
|
56
|
+
``AGENT_SESSION_STORE_URI`` is set and this migration has not been applied.
|
|
57
|
+
|
|
58
|
+
The DDL itself is LangGraph's (``AsyncPostgresSaver.setup()``); registering it here is
|
|
59
|
+
what puts it behind the single gated ``agentship db upgrade --allow-migrations`` entry
|
|
60
|
+
point rather than having the engine create tables silently on boot.
|
|
61
|
+
"""
|
|
62
|
+
import asyncio
|
|
63
|
+
|
|
64
|
+
from agentship_langgraph.durability import open_checkpointer
|
|
65
|
+
|
|
66
|
+
async def create() -> None:
|
|
67
|
+
"""Open the Postgres saver with ``setup=True``, which runs its schema DDL."""
|
|
68
|
+
async with open_checkpointer(database_url, setup=True):
|
|
69
|
+
pass
|
|
70
|
+
|
|
71
|
+
asyncio.run(create())
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
#: Applied in ``version`` order by ``agentship db upgrade --allow-migrations``. Each entry is
|
|
75
|
+
#: idempotent, so re-running the command is always safe.
|
|
76
|
+
REGISTERED_MIGRATIONS: list[Migration] = [
|
|
77
|
+
Migration(
|
|
78
|
+
version="0001_langgraph_checkpoints",
|
|
79
|
+
description="LangGraph checkpointer tables (needed by durability: checkpoint)",
|
|
80
|
+
apply=_create_checkpoint_tables,
|
|
81
|
+
),
|
|
82
|
+
]
|
agentship_cli/py.typed
ADDED
|
File without changes
|
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
"""Scaffold templates for ``agentship init`` and ``agentship new-agent``.
|
|
2
|
+
|
|
3
|
+
These builders return the *text* of the files the scaffolding commands write —
|
|
4
|
+
kept out of ``main.py`` so the command code stays about I/O and error handling.
|
|
5
|
+
The scaffold is deliberately **single-tenant** (no auth/tenancy concepts): a fresh
|
|
6
|
+
project just runs, per the reusability plan. The starter agent uses the default
|
|
7
|
+
``langgraph`` engine over ``openai/gpt-4o-mini`` so ``agentship run`` works with a
|
|
8
|
+
single ``OPENAI_API_KEY``.
|
|
9
|
+
|
|
10
|
+
Every emitted spec is a valid :class:`~agentship.spec.AgentSpec` — the init tests
|
|
11
|
+
prove the generated ``assistant.yaml`` loads *and* builds, so these templates
|
|
12
|
+
cannot silently drift from the spec schema.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
#: The starter agent spec written by ``agentship init`` (default single-tenant).
|
|
18
|
+
ASSISTANT_YAML = """\
|
|
19
|
+
# A starter AgentShip agent. Run it with:
|
|
20
|
+
# agentship run agents/assistant.yaml --input "hello"
|
|
21
|
+
name: assistant
|
|
22
|
+
engine: langgraph
|
|
23
|
+
model: openai/gpt-4o-mini
|
|
24
|
+
prompt: You are a concise, helpful assistant. Answer in one short sentence.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
#: The example environment file. Ships a commented placeholder, never a real key.
|
|
28
|
+
ENV_EXAMPLE = """\
|
|
29
|
+
# Copy this file to `.env` and fill in the provider key you use.
|
|
30
|
+
# The starter assistant uses OpenAI, so only this one is required for the quickstart.
|
|
31
|
+
# OPENAI_API_KEY=your-key-here
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
#: The project README explaining the single-command run path.
|
|
35
|
+
README = """\
|
|
36
|
+
# My AgentShip project
|
|
37
|
+
|
|
38
|
+
A single-tenant AgentShip project scaffolded by `agentship init`.
|
|
39
|
+
|
|
40
|
+
## Layout
|
|
41
|
+
|
|
42
|
+
- `agents/` — your agent specs (YAML). One starter agent, `assistant.yaml`, is included.
|
|
43
|
+
- `.env.example` — copy to `.env` and add your provider key (e.g. `OPENAI_API_KEY`).
|
|
44
|
+
|
|
45
|
+
## Run the starter agent
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
cp .env.example .env # then edit .env and set OPENAI_API_KEY
|
|
49
|
+
agentship run agents/assistant.yaml --input "hello"
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Stream the response instead of waiting for the whole answer:
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
agentship run agents/assistant.yaml --input "hello" --stream
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Check your agents
|
|
59
|
+
|
|
60
|
+
Validate every agent spec against its engine's capabilities before running:
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
agentship doctor --agents-dir agents
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## Add another agent
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
agentship new-agent researcher
|
|
70
|
+
```
|
|
71
|
+
"""
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def new_agent_yaml(name: str, engine: str) -> str:
|
|
75
|
+
"""Return the text of a single starter agent spec named ``name`` on ``engine``.
|
|
76
|
+
|
|
77
|
+
Written by ``agentship new-agent``. The spec is a valid
|
|
78
|
+
:class:`~agentship.spec.AgentSpec`: a single agent with a prompt and, for the
|
|
79
|
+
default ``langgraph`` engine, a ``model`` line so it runs as-is. Other engines
|
|
80
|
+
omit the model line (they may not need one) and leave a comment pointing at it.
|
|
81
|
+
"""
|
|
82
|
+
header = (
|
|
83
|
+
f'# Agent {name!r}. Run it with:\n# agentship run agents/{name}.yaml --input "hello"\n'
|
|
84
|
+
)
|
|
85
|
+
if engine == "langgraph":
|
|
86
|
+
return (
|
|
87
|
+
f"{header}"
|
|
88
|
+
f"name: {name}\n"
|
|
89
|
+
f"engine: {engine}\n"
|
|
90
|
+
f"model: openai/gpt-4o-mini\n"
|
|
91
|
+
f"prompt: You are a helpful assistant.\n"
|
|
92
|
+
)
|
|
93
|
+
return (
|
|
94
|
+
f"{header}"
|
|
95
|
+
f"name: {name}\n"
|
|
96
|
+
f"engine: {engine}\n"
|
|
97
|
+
f"# model: <provider>/<model> # add the model this engine should use\n"
|
|
98
|
+
f"prompt: You are a helpful assistant.\n"
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def single_template_yaml(name: str) -> str:
|
|
103
|
+
"""Return a ``template: single`` spec named ``name`` (one model, zero author code).
|
|
104
|
+
|
|
105
|
+
The ``single`` template yields a runnable ReAct agent from the YAML alone —
|
|
106
|
+
``create_react_agent(model, tools, prompt)`` under the hood — so this spec needs
|
|
107
|
+
no companion ``agent.py``. It targets the ``langgraph`` engine (the only engine
|
|
108
|
+
that ships templates today) over ``openai/gpt-4o-mini``.
|
|
109
|
+
"""
|
|
110
|
+
return (
|
|
111
|
+
f"# Agent {name!r} — the `single` template (one model, zero author Python).\n"
|
|
112
|
+
f'# agentship run agents/{name}.yaml --input "hello"\n'
|
|
113
|
+
f"name: {name}\n"
|
|
114
|
+
f"engine: langgraph\n"
|
|
115
|
+
f"template: single\n"
|
|
116
|
+
f"model: openai/gpt-4o-mini\n"
|
|
117
|
+
f"prompt: You are a concise, helpful assistant.\n"
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def autonomous_template_yaml(name: str) -> str:
|
|
122
|
+
"""Return a ``template: autonomous`` spec named ``name`` (a single self-directing agent).
|
|
123
|
+
|
|
124
|
+
The ``autonomous`` template configures one agent that plans and calls its own
|
|
125
|
+
tools in a loop; it needs the optional ``agentship-langgraph[autonomous]`` extra
|
|
126
|
+
and a tool-calling model, so the scaffold is a coherent, loadable spec that
|
|
127
|
+
``agentship doctor`` version-guards before it runs.
|
|
128
|
+
"""
|
|
129
|
+
return (
|
|
130
|
+
f"# Agent {name!r} — the `autonomous` template (a single self-directing agent).\n"
|
|
131
|
+
f"# Needs the optional extra: pip install 'agentship-langgraph[autonomous]'\n"
|
|
132
|
+
f'# agentship run agents/{name}.yaml --input "hello"\n'
|
|
133
|
+
f"name: {name}\n"
|
|
134
|
+
f"engine: langgraph\n"
|
|
135
|
+
f"template: autonomous\n"
|
|
136
|
+
f"model: openai/gpt-4o-mini\n"
|
|
137
|
+
f"prompt: You are an autonomous assistant that plans and uses tools.\n"
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def graph_template_yaml(name: str, code_ref: str) -> str:
|
|
142
|
+
"""Return a ``graph``-template spec named ``name`` pointing ``code:`` at its ``agent.py``.
|
|
143
|
+
|
|
144
|
+
The ``graph`` template is a *custom-authoring* scaffold: the fillable supervisor
|
|
145
|
+
lives in a companion ``NAME/agent.py`` and the spec references it via ``code:``.
|
|
146
|
+
(``template:`` and ``code:`` are mutually exclusive — a graph scaffold *is* an
|
|
147
|
+
authored agent, so it uses ``code:``, not ``template: graph``.) ``code_ref`` is
|
|
148
|
+
the ``"file.py:build_agent"`` reference the caller computes (an absolute path so
|
|
149
|
+
the spec resolves regardless of the working directory it is loaded from — see
|
|
150
|
+
:func:`agentship.spec.resolve_code`).
|
|
151
|
+
"""
|
|
152
|
+
return (
|
|
153
|
+
f"# Agent {name!r} — the `graph` template (a fillable supervisor scaffold).\n"
|
|
154
|
+
f"# Open {name}/agent.py and follow the `# TODO(author)` markers.\n"
|
|
155
|
+
f'# agentship run agents/{name}.yaml --input "hello"\n'
|
|
156
|
+
f"name: {name}\n"
|
|
157
|
+
f"engine: langgraph\n"
|
|
158
|
+
f"code: {code_ref}\n"
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def graph_template_agent_py(name: str) -> str:
|
|
163
|
+
"""Return the companion ``agent.py`` for a ``graph``-template scaffold named ``name``.
|
|
164
|
+
|
|
165
|
+
Scaffolds a :class:`~agentship_langgraph.agent.LangGraphAgent` subclass whose
|
|
166
|
+
``build_graph(model, tools)`` is a compilable coordinator→worker supervisor with
|
|
167
|
+
``# TODO(author)`` markers where specialists, tools, and richer routing get
|
|
168
|
+
filled in. A ``build_agent()`` factory (referenced by the YAML's ``code:``)
|
|
169
|
+
returns the configured subclass instance, so the scaffold LOADS and BUILDS
|
|
170
|
+
as-is over a real (or, in tests, a fake) model.
|
|
171
|
+
"""
|
|
172
|
+
class_name = _class_name(name)
|
|
173
|
+
return f'''"""The ``{name}`` agent — a fillable LangGraph supervisor scaffold.
|
|
174
|
+
|
|
175
|
+
Scaffolded by ``agentship new-agent {name} --template graph``. Fill in the
|
|
176
|
+
``# TODO(author)`` markers: add specialist workers, bind tools, and widen the
|
|
177
|
+
routing. The harness wires ``model``/``tools`` and drives ``run``/``stream``; you
|
|
178
|
+
own only the graph shape below.
|
|
179
|
+
|
|
180
|
+
Note: this module intentionally omits ``from __future__ import annotations`` so the
|
|
181
|
+
``TypedDict`` state annotations are real objects at class-creation time. LangGraph
|
|
182
|
+
reads them via ``get_type_hints``, and a spec loaded from a file path (see
|
|
183
|
+
``agentship.spec.resolve_code``) is not registered in ``sys.modules``, so a stringized
|
|
184
|
+
``Annotated[...]`` annotation could not be resolved later.
|
|
185
|
+
"""
|
|
186
|
+
|
|
187
|
+
from typing import Annotated
|
|
188
|
+
|
|
189
|
+
from agentship.spec import AgentSpec
|
|
190
|
+
from agentship_langgraph import LangGraphAgent
|
|
191
|
+
from langgraph.graph import END, START, StateGraph
|
|
192
|
+
from langgraph.graph.message import add_messages
|
|
193
|
+
from typing_extensions import TypedDict
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
class {class_name}State(TypedDict):
|
|
197
|
+
"""The supervisor's state: the message history plus the chosen route.
|
|
198
|
+
|
|
199
|
+
``messages`` uses the ``add_messages`` reducer so each node appends rather than
|
|
200
|
+
replaces (the engine seeds ``[system, user]`` and reads the final message as the
|
|
201
|
+
answer). ``route`` is the coordinator's decision the conditional edge switches on.
|
|
202
|
+
"""
|
|
203
|
+
|
|
204
|
+
messages: Annotated[list, add_messages]
|
|
205
|
+
#: TODO(author): widen to your specialist worker names.
|
|
206
|
+
route: str
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
class {class_name}Agent(LangGraphAgent):
|
|
210
|
+
"""A fillable coordinator -> worker supervisor authored in native LangGraph."""
|
|
211
|
+
|
|
212
|
+
def build_graph(self, model, tools) -> StateGraph:
|
|
213
|
+
"""Build the coordinator -> worker supervisor over the wired model and tools."""
|
|
214
|
+
|
|
215
|
+
def coordinator(state: {class_name}State) -> dict:
|
|
216
|
+
"""Decide which worker handles this turn (the routing decision).
|
|
217
|
+
|
|
218
|
+
TODO(author): inspect the request and pick among several specialists,
|
|
219
|
+
setting ``route`` to the chosen worker's node name. The scaffold asks the
|
|
220
|
+
model once and always falls through to the single ``worker``.
|
|
221
|
+
"""
|
|
222
|
+
decision = model.invoke(state["messages"])
|
|
223
|
+
route = (decision.content or "worker").strip().lower()
|
|
224
|
+
if route != "done":
|
|
225
|
+
route = "worker"
|
|
226
|
+
return {{"route": route}}
|
|
227
|
+
|
|
228
|
+
def worker(state: {class_name}State) -> dict:
|
|
229
|
+
"""Answer the request, appending the reply to the message history.
|
|
230
|
+
|
|
231
|
+
TODO(author): add tools (``model.bind_tools(tools)``), specialist prompts,
|
|
232
|
+
or a sub-graph here. The scaffold just invokes the wired model.
|
|
233
|
+
"""
|
|
234
|
+
reply = model.invoke(state["messages"])
|
|
235
|
+
return {{"messages": [reply]}}
|
|
236
|
+
|
|
237
|
+
g = StateGraph({class_name}State)
|
|
238
|
+
g.add_node("coordinator", coordinator)
|
|
239
|
+
g.add_node("worker", worker)
|
|
240
|
+
g.add_edge(START, "coordinator")
|
|
241
|
+
# TODO(author): add more branches as you add specialists.
|
|
242
|
+
g.add_conditional_edges(
|
|
243
|
+
"coordinator",
|
|
244
|
+
lambda state: state["route"],
|
|
245
|
+
{{"worker": "worker", "done": END}},
|
|
246
|
+
)
|
|
247
|
+
g.add_edge("worker", END)
|
|
248
|
+
return g
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def build_agent() -> {class_name}Agent:
|
|
252
|
+
"""Return the configured ``{name}`` agent (referenced by ``{name}.yaml``'s ``code:``)."""
|
|
253
|
+
return {class_name}Agent(
|
|
254
|
+
AgentSpec(
|
|
255
|
+
name="{name}",
|
|
256
|
+
engine="langgraph",
|
|
257
|
+
model="openai/gpt-4o-mini",
|
|
258
|
+
prompt="Route the user request to the right specialist, then answer.",
|
|
259
|
+
)
|
|
260
|
+
)
|
|
261
|
+
'''
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def _class_name(name: str) -> str:
|
|
265
|
+
"""Turn an agent ``name`` (``a-z0-9_``) into a CamelCase Python class prefix.
|
|
266
|
+
|
|
267
|
+
``new-agent`` validates ``name`` to ``[a-z][a-z0-9_]*`` before this runs, so
|
|
268
|
+
splitting on ``_`` and title-casing each part yields a valid identifier prefix
|
|
269
|
+
(e.g. ``ticket_router`` -> ``TicketRouter``).
|
|
270
|
+
"""
|
|
271
|
+
return "".join(part.title() for part in name.split("_") if part)
|