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.
@@ -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)