skillstate-kit 0.1.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.
- skillstate/__init__.py +35 -0
- skillstate/__main__.py +3 -0
- skillstate/artifacts.py +55 -0
- skillstate/cli.py +256 -0
- skillstate/compiler.py +367 -0
- skillstate/demo.py +74 -0
- skillstate/errors.py +33 -0
- skillstate/hosts.py +322 -0
- skillstate/jsonio.py +143 -0
- skillstate/mcp_server.py +142 -0
- skillstate/models.py +88 -0
- skillstate/providers.py +89 -0
- skillstate/py.typed +0 -0
- skillstate/runtime.py +179 -0
- skillstate/schema.py +91 -0
- skillstate/service.py +59 -0
- skillstate/store.py +332 -0
- skillstate_kit-0.1.1.dist-info/METADATA +157 -0
- skillstate_kit-0.1.1.dist-info/RECORD +22 -0
- skillstate_kit-0.1.1.dist-info/WHEEL +4 -0
- skillstate_kit-0.1.1.dist-info/entry_points.txt +2 -0
- skillstate_kit-0.1.1.dist-info/licenses/LICENSE +21 -0
skillstate/store.py
ADDED
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
"""Transactional checkpoints and a durable operation journal.
|
|
2
|
+
|
|
3
|
+
SQLite transactions never span model or tool calls. A reserved operation prevents
|
|
4
|
+
other writers from advancing its run until its outcome is recorded or reconciled.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import sqlite3
|
|
10
|
+
import threading
|
|
11
|
+
import time
|
|
12
|
+
import uuid
|
|
13
|
+
from contextlib import contextmanager
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import Any
|
|
16
|
+
|
|
17
|
+
from .errors import ConflictError, NotFoundError, ValidationError
|
|
18
|
+
from .jsonio import dumps, loads
|
|
19
|
+
from .models import Skill
|
|
20
|
+
from .schema import apply_patch, validate
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _identifier(value: str) -> str:
|
|
24
|
+
if type(value) is not str or not value or len(value) > 128:
|
|
25
|
+
raise ValidationError("Identifiers must be nonempty strings up to 128 characters")
|
|
26
|
+
return value
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class SQLiteStore:
|
|
30
|
+
"""One local database, multiple processes, optimistic revision checks.
|
|
31
|
+
|
|
32
|
+
Owner identifiers coordinate cooperating clients; they are not authentication.
|
|
33
|
+
Keep this file private to the local project/user. Do not place it on network FS.
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
def __init__(self, path: str | Path = ":memory:"):
|
|
37
|
+
self.path = str(path)
|
|
38
|
+
if self.path != ":memory:":
|
|
39
|
+
Path(path).parent.mkdir(parents=True, exist_ok=True)
|
|
40
|
+
self._lock = threading.RLock()
|
|
41
|
+
self._db = sqlite3.connect(
|
|
42
|
+
self.path, timeout=10, isolation_level=None, check_same_thread=False
|
|
43
|
+
)
|
|
44
|
+
self._db.row_factory = sqlite3.Row
|
|
45
|
+
self._db.execute("PRAGMA foreign_keys=ON")
|
|
46
|
+
self._db.execute("PRAGMA busy_timeout=10000")
|
|
47
|
+
self._db.execute("PRAGMA synchronous=FULL")
|
|
48
|
+
version = self._db.execute("PRAGMA user_version").fetchone()[0]
|
|
49
|
+
if version not in (0, 1):
|
|
50
|
+
self._db.close()
|
|
51
|
+
raise ValidationError(f"Unsupported database version: {version}")
|
|
52
|
+
self._db.execute("PRAGMA journal_mode=WAL")
|
|
53
|
+
self._db.executescript("""
|
|
54
|
+
CREATE TABLE IF NOT EXISTS runs (
|
|
55
|
+
id TEXT PRIMARY KEY, skill TEXT NOT NULL, fingerprint TEXT NOT NULL,
|
|
56
|
+
state TEXT NOT NULL, observation TEXT NOT NULL, revision INTEGER NOT NULL,
|
|
57
|
+
status TEXT NOT NULL, owner TEXT NOT NULL, pending TEXT,
|
|
58
|
+
created REAL NOT NULL, updated REAL NOT NULL
|
|
59
|
+
);
|
|
60
|
+
CREATE TABLE IF NOT EXISTS operations (
|
|
61
|
+
id TEXT PRIMARY KEY, run_id TEXT NOT NULL REFERENCES runs(id),
|
|
62
|
+
action TEXT NOT NULL, candidate TEXT NOT NULL, status TEXT NOT NULL,
|
|
63
|
+
result TEXT, created REAL NOT NULL
|
|
64
|
+
);
|
|
65
|
+
CREATE TABLE IF NOT EXISTS events (
|
|
66
|
+
sequence INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
67
|
+
run_id TEXT NOT NULL REFERENCES runs(id), kind TEXT NOT NULL,
|
|
68
|
+
payload TEXT NOT NULL, created REAL NOT NULL
|
|
69
|
+
);
|
|
70
|
+
PRAGMA user_version=1;
|
|
71
|
+
""")
|
|
72
|
+
|
|
73
|
+
def close(self) -> None:
|
|
74
|
+
with self._lock:
|
|
75
|
+
self._db.close()
|
|
76
|
+
|
|
77
|
+
def __enter__(self) -> SQLiteStore:
|
|
78
|
+
return self
|
|
79
|
+
|
|
80
|
+
def __exit__(self, *args) -> None:
|
|
81
|
+
self.close()
|
|
82
|
+
|
|
83
|
+
@contextmanager
|
|
84
|
+
def _transaction(self):
|
|
85
|
+
with self._lock:
|
|
86
|
+
self._db.execute("BEGIN IMMEDIATE")
|
|
87
|
+
try:
|
|
88
|
+
yield
|
|
89
|
+
self._db.execute("COMMIT")
|
|
90
|
+
except BaseException:
|
|
91
|
+
self._db.execute("ROLLBACK")
|
|
92
|
+
raise
|
|
93
|
+
|
|
94
|
+
def _event(self, run_id: str, kind: str, payload: Any) -> None:
|
|
95
|
+
self._db.execute(
|
|
96
|
+
"INSERT INTO events(run_id,kind,payload,created) VALUES(?,?,?,?)",
|
|
97
|
+
(run_id, kind, dumps(payload), time.time()),
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
def _row(self, run_id: str) -> sqlite3.Row:
|
|
101
|
+
row = self._db.execute("SELECT * FROM runs WHERE id=?", (run_id,)).fetchone()
|
|
102
|
+
if row is None:
|
|
103
|
+
raise NotFoundError(f"Run not found: {run_id}")
|
|
104
|
+
return row
|
|
105
|
+
|
|
106
|
+
def _guard(
|
|
107
|
+
self, row: sqlite3.Row, owner: str, revision: int | None = None, pending: bool = False
|
|
108
|
+
) -> None:
|
|
109
|
+
if row["owner"] != owner:
|
|
110
|
+
raise ConflictError("Run belongs to a different owner; use handoff")
|
|
111
|
+
if revision is not None and (type(revision) is not int or row["revision"] != revision):
|
|
112
|
+
raise ConflictError("Stale revision; reload context and reconsider the decision")
|
|
113
|
+
if not pending and row["pending"] is not None:
|
|
114
|
+
raise ConflictError("An operation is pending or unknown; reconcile it first")
|
|
115
|
+
if row["status"] in ("completed", "cancelled"):
|
|
116
|
+
raise ConflictError("Run is already terminal")
|
|
117
|
+
|
|
118
|
+
@staticmethod
|
|
119
|
+
def _snapshot(row: sqlite3.Row) -> dict:
|
|
120
|
+
return {
|
|
121
|
+
"run_id": row["id"],
|
|
122
|
+
"skill": loads(row["skill"]),
|
|
123
|
+
"state": loads(row["state"]),
|
|
124
|
+
"observation": loads(row["observation"]),
|
|
125
|
+
"revision": row["revision"],
|
|
126
|
+
"status": row["status"],
|
|
127
|
+
"owner": row["owner"],
|
|
128
|
+
"pending_operation": row["pending"],
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
def get(self, run_id: str) -> dict:
|
|
132
|
+
with self._lock:
|
|
133
|
+
return self._snapshot(self._row(run_id))
|
|
134
|
+
|
|
135
|
+
def list_runs(self, limit: int = 100) -> list[dict]:
|
|
136
|
+
if type(limit) is not int or not 1 <= limit <= 1000:
|
|
137
|
+
raise ValidationError("Run limit must be between 1 and 1000")
|
|
138
|
+
with self._lock:
|
|
139
|
+
rows = self._db.execute(
|
|
140
|
+
"SELECT id,revision,status,owner,pending FROM runs ORDER BY updated DESC LIMIT ?",
|
|
141
|
+
(limit,),
|
|
142
|
+
).fetchall()
|
|
143
|
+
return [dict(row) for row in rows]
|
|
144
|
+
|
|
145
|
+
def create(self, run_id: str, skill: Skill, owner: str, observation: Any = None) -> dict:
|
|
146
|
+
_identifier(run_id)
|
|
147
|
+
_identifier(owner)
|
|
148
|
+
skill = Skill.from_dict(skill.to_dict())
|
|
149
|
+
obs = dumps(observation, skill.limits.observation_bytes)
|
|
150
|
+
with self._transaction():
|
|
151
|
+
if self._db.execute("SELECT 1 FROM runs WHERE id=?", (run_id,)).fetchone():
|
|
152
|
+
raise ConflictError("Run already exists; resume it without resetting its state")
|
|
153
|
+
now = time.time()
|
|
154
|
+
self._db.execute(
|
|
155
|
+
"INSERT INTO runs VALUES(?,?,?,?,?,0,'ready',?,NULL,?,?)",
|
|
156
|
+
(
|
|
157
|
+
run_id,
|
|
158
|
+
dumps(skill.to_dict()),
|
|
159
|
+
skill.fingerprint,
|
|
160
|
+
dumps(skill.initial_state),
|
|
161
|
+
obs,
|
|
162
|
+
owner,
|
|
163
|
+
now,
|
|
164
|
+
now,
|
|
165
|
+
),
|
|
166
|
+
)
|
|
167
|
+
self._event(run_id, "created", {"skill": skill.name, "owner": owner})
|
|
168
|
+
return self.get(run_id)
|
|
169
|
+
|
|
170
|
+
def _candidate(self, row, patch: list[dict]) -> dict:
|
|
171
|
+
skill = Skill.from_dict(loads(row["skill"]))
|
|
172
|
+
candidate = apply_patch(loads(row["state"]), patch)
|
|
173
|
+
validate(skill.state_schema, candidate)
|
|
174
|
+
dumps(candidate, skill.limits.state_bytes)
|
|
175
|
+
return candidate
|
|
176
|
+
|
|
177
|
+
def update(
|
|
178
|
+
self,
|
|
179
|
+
run_id: str,
|
|
180
|
+
owner: str,
|
|
181
|
+
revision: int,
|
|
182
|
+
patch: list[dict],
|
|
183
|
+
observation: Any = None,
|
|
184
|
+
*,
|
|
185
|
+
done: bool = False,
|
|
186
|
+
) -> dict:
|
|
187
|
+
"""Record agent-reported facts. Does not certify external business truth."""
|
|
188
|
+
if type(done) is not bool:
|
|
189
|
+
raise ValidationError("done must be boolean")
|
|
190
|
+
with self._transaction():
|
|
191
|
+
row = self._row(run_id)
|
|
192
|
+
self._guard(row, owner, revision)
|
|
193
|
+
skill = Skill.from_dict(loads(row["skill"]))
|
|
194
|
+
candidate = self._candidate(row, patch)
|
|
195
|
+
obs = dumps(observation, skill.limits.observation_bytes)
|
|
196
|
+
self._db.execute(
|
|
197
|
+
"UPDATE runs SET state=?,observation=?,revision=revision+1,status=?,updated=? WHERE id=?",
|
|
198
|
+
(dumps(candidate), obs, "completed" if done else "ready", time.time(), run_id),
|
|
199
|
+
)
|
|
200
|
+
self._event(
|
|
201
|
+
run_id,
|
|
202
|
+
"completed" if done else "updated",
|
|
203
|
+
{"patch": patch, "origin": "agent_reported"},
|
|
204
|
+
)
|
|
205
|
+
return self.get(run_id)
|
|
206
|
+
|
|
207
|
+
def reserve(
|
|
208
|
+
self, run_id: str, owner: str, revision: int, action: dict, patch: list[dict]
|
|
209
|
+
) -> dict:
|
|
210
|
+
dumps(action, 32_000)
|
|
211
|
+
if (
|
|
212
|
+
type(action) is not dict
|
|
213
|
+
or set(action) != {"name", "arguments"}
|
|
214
|
+
or type(action["name"]) is not str
|
|
215
|
+
or not action["name"]
|
|
216
|
+
or type(action["arguments"]) is not dict
|
|
217
|
+
):
|
|
218
|
+
raise ValidationError("Invalid action contract")
|
|
219
|
+
with self._transaction():
|
|
220
|
+
row = self._row(run_id)
|
|
221
|
+
self._guard(row, owner, revision)
|
|
222
|
+
candidate = self._candidate(row, patch)
|
|
223
|
+
operation_id = uuid.uuid4().hex
|
|
224
|
+
self._db.execute(
|
|
225
|
+
"INSERT INTO operations VALUES(?,?,?,?,'pending',NULL,?)",
|
|
226
|
+
(operation_id, run_id, dumps(action), dumps(candidate), time.time()),
|
|
227
|
+
)
|
|
228
|
+
self._db.execute(
|
|
229
|
+
"UPDATE runs SET pending=?,status='running',revision=revision+1,updated=? WHERE id=?",
|
|
230
|
+
(operation_id, time.time(), run_id),
|
|
231
|
+
)
|
|
232
|
+
self._event(run_id, "reserved", {"operation_id": operation_id, "action": action})
|
|
233
|
+
return {"operation_id": operation_id, "run": self.get(run_id)}
|
|
234
|
+
|
|
235
|
+
def operation(self, operation_id: str) -> dict:
|
|
236
|
+
with self._lock:
|
|
237
|
+
row = self._db.execute(
|
|
238
|
+
"SELECT * FROM operations WHERE id=?", (operation_id,)
|
|
239
|
+
).fetchone()
|
|
240
|
+
if row is None:
|
|
241
|
+
raise NotFoundError("Operation not found")
|
|
242
|
+
return {
|
|
243
|
+
"operation_id": row["id"],
|
|
244
|
+
"run_id": row["run_id"],
|
|
245
|
+
"action": loads(row["action"]),
|
|
246
|
+
"status": row["status"],
|
|
247
|
+
"result": loads(row["result"]) if row["result"] else None,
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
def mark_unknown(self, operation_id: str, owner: str, reason: str) -> dict:
|
|
251
|
+
reason = str(reason)[:1000]
|
|
252
|
+
with self._transaction():
|
|
253
|
+
op = self.operation(operation_id)
|
|
254
|
+
row = self._row(op["run_id"])
|
|
255
|
+
self._guard(row, owner, pending=True)
|
|
256
|
+
if row["pending"] != operation_id or op["status"] not in ("pending", "unknown"):
|
|
257
|
+
raise ConflictError("Operation is not outstanding")
|
|
258
|
+
self._db.execute("UPDATE operations SET status='unknown' WHERE id=?", (operation_id,))
|
|
259
|
+
self._db.execute(
|
|
260
|
+
"UPDATE runs SET status='unknown',revision=revision+1,updated=? WHERE id=?",
|
|
261
|
+
(time.time(), op["run_id"]),
|
|
262
|
+
)
|
|
263
|
+
self._event(op["run_id"], "unknown", {"operation_id": operation_id, "reason": reason})
|
|
264
|
+
return self.get(op["run_id"])
|
|
265
|
+
|
|
266
|
+
def record_result(
|
|
267
|
+
self,
|
|
268
|
+
operation_id: str,
|
|
269
|
+
owner: str,
|
|
270
|
+
success: bool,
|
|
271
|
+
observation: Any,
|
|
272
|
+
*,
|
|
273
|
+
reconcile: bool = False,
|
|
274
|
+
origin: str = "agent_reported",
|
|
275
|
+
) -> dict:
|
|
276
|
+
if type(success) is not bool or type(reconcile) is not bool:
|
|
277
|
+
raise ValidationError("success and reconcile must be booleans")
|
|
278
|
+
if origin not in ("agent_reported", "runtime_observed"):
|
|
279
|
+
raise ValidationError("Invalid result origin")
|
|
280
|
+
with self._transaction():
|
|
281
|
+
op = self.operation(operation_id)
|
|
282
|
+
row = self._row(op["run_id"])
|
|
283
|
+
if row["owner"] != owner:
|
|
284
|
+
raise ConflictError("Run owner mismatch")
|
|
285
|
+
skill = Skill.from_dict(loads(row["skill"]))
|
|
286
|
+
obs = dumps(observation, skill.limits.observation_bytes)
|
|
287
|
+
result = {"success": success, "observation": loads(obs), "origin": origin}
|
|
288
|
+
if op["status"] in ("succeeded", "failed"):
|
|
289
|
+
if op["result"] != result:
|
|
290
|
+
raise ConflictError("Operation already has a different result")
|
|
291
|
+
return self._snapshot(row)
|
|
292
|
+
if row["pending"] != operation_id:
|
|
293
|
+
raise ConflictError("Operation does not match run's pending operation")
|
|
294
|
+
if op["status"] == "unknown" and not reconcile:
|
|
295
|
+
raise ConflictError("Unknown result requires explicit reconciliation")
|
|
296
|
+
candidate_row = self._db.execute(
|
|
297
|
+
"SELECT candidate FROM operations WHERE id=?", (operation_id,)
|
|
298
|
+
).fetchone()
|
|
299
|
+
next_state = candidate_row[0] if success else row["state"]
|
|
300
|
+
self._db.execute(
|
|
301
|
+
"UPDATE operations SET status=?,result=? WHERE id=?",
|
|
302
|
+
("succeeded" if success else "failed", dumps(result), operation_id),
|
|
303
|
+
)
|
|
304
|
+
self._db.execute(
|
|
305
|
+
"UPDATE runs SET state=?,observation=?,pending=NULL,status='ready',revision=revision+1,updated=? WHERE id=?",
|
|
306
|
+
(next_state, obs, time.time(), op["run_id"]),
|
|
307
|
+
)
|
|
308
|
+
self._event(op["run_id"], "result", {"operation_id": operation_id, **result})
|
|
309
|
+
return self.get(op["run_id"])
|
|
310
|
+
|
|
311
|
+
def handoff(self, run_id: str, owner: str, revision: int, target: str) -> dict:
|
|
312
|
+
_identifier(target)
|
|
313
|
+
with self._transaction():
|
|
314
|
+
row = self._row(run_id)
|
|
315
|
+
self._guard(row, owner, revision)
|
|
316
|
+
self._db.execute(
|
|
317
|
+
"UPDATE runs SET owner=?,revision=revision+1,updated=? WHERE id=?",
|
|
318
|
+
(target, time.time(), run_id),
|
|
319
|
+
)
|
|
320
|
+
self._event(run_id, "handoff", {"from": owner, "to": target})
|
|
321
|
+
return self.get(run_id)
|
|
322
|
+
|
|
323
|
+
def events(self, run_id: str, after: int = 0, limit: int = 100) -> list[dict]:
|
|
324
|
+
if type(after) is not int or after < 0 or type(limit) is not int or not 1 <= limit <= 1000:
|
|
325
|
+
raise ValidationError("Invalid event pagination")
|
|
326
|
+
with self._lock:
|
|
327
|
+
self._row(run_id)
|
|
328
|
+
rows = self._db.execute(
|
|
329
|
+
"SELECT sequence,kind,payload FROM events WHERE run_id=? AND sequence>? ORDER BY sequence LIMIT ?",
|
|
330
|
+
(run_id, after, limit),
|
|
331
|
+
).fetchall()
|
|
332
|
+
return [{"sequence": r[0], "kind": r[1], "payload": loads(r[2])} for r in rows]
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: skillstate-kit
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: Compile existing agent skills into portable, validated execution state.
|
|
5
|
+
Project-URL: Homepage, https://github.com/Atakan-Emre/skillstate-kit
|
|
6
|
+
Project-URL: Repository, https://github.com/Atakan-Emre/skillstate-kit
|
|
7
|
+
Project-URL: Documentation, https://github.com/Atakan-Emre/skillstate-kit/tree/main/docs
|
|
8
|
+
Project-URL: Issues, https://github.com/Atakan-Emre/skillstate-kit/issues
|
|
9
|
+
Author: Atakan Emre
|
|
10
|
+
License-Expression: MIT
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Keywords: agents,claude,codex,mcp,skills,state
|
|
13
|
+
Classifier: Development Status :: 3 - Alpha
|
|
14
|
+
Classifier: Operating System :: OS Independent
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Classifier: Typing :: Typed
|
|
20
|
+
Requires-Python: >=3.11
|
|
21
|
+
Requires-Dist: jsonschema<5,>=4.23
|
|
22
|
+
Requires-Dist: pathspec<1,>=0.12
|
|
23
|
+
Requires-Dist: pyyaml<7,>=6.0.2
|
|
24
|
+
Requires-Dist: tomlkit<1,>=0.13
|
|
25
|
+
Provides-Extra: dev
|
|
26
|
+
Requires-Dist: build<2,>=1.2; extra == 'dev'
|
|
27
|
+
Requires-Dist: httpx<1,>=0.28; extra == 'dev'
|
|
28
|
+
Requires-Dist: hypothesis<7,>=6.120; extra == 'dev'
|
|
29
|
+
Requires-Dist: mcp<2,>=1.20; extra == 'dev'
|
|
30
|
+
Requires-Dist: pytest-cov<7,>=6; extra == 'dev'
|
|
31
|
+
Requires-Dist: pytest<9,>=8; extra == 'dev'
|
|
32
|
+
Requires-Dist: ruff<1,>=0.11; extra == 'dev'
|
|
33
|
+
Requires-Dist: twine<7,>=6; extra == 'dev'
|
|
34
|
+
Provides-Extra: http
|
|
35
|
+
Requires-Dist: httpx<1,>=0.28; extra == 'http'
|
|
36
|
+
Provides-Extra: mcp
|
|
37
|
+
Requires-Dist: mcp<2,>=1.20; extra == 'mcp'
|
|
38
|
+
Description-Content-Type: text/markdown
|
|
39
|
+
|
|
40
|
+
# skillstate-kit
|
|
41
|
+
|
|
42
|
+
**Portable, validated execution state for agent skills.**
|
|
43
|
+
|
|
44
|
+
Convert an existing `SKILL.md` into a state-backed skill, retain durable checkpoints, and integrate the same execution engine into a Python application.
|
|
45
|
+
|
|
46
|
+
Python 3.11+ · MIT license · Initial alpha
|
|
47
|
+
|
|
48
|
+
## Install
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
python -m pip install "skillstate-kit[mcp,http]"
|
|
52
|
+
skillstate demo
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
The core package requires no model account. The demo uses an explicitly scripted model and local tools; it is not a benchmark of real-model performance.
|
|
56
|
+
|
|
57
|
+
Optional extras:
|
|
58
|
+
|
|
59
|
+
- `mcp`: a project-scoped STDIO MCP server and connection diagnostics.
|
|
60
|
+
- `http`: a stateless adapter for a configured JSON-compatible chat-completions endpoint.
|
|
61
|
+
|
|
62
|
+
## Use an existing skill
|
|
63
|
+
|
|
64
|
+
Run these commands in your target project. Replace `skills/qa/SKILL.md` with your existing skill file:
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
skillstate init --host codex --host claude-code --host antigravity --mcp
|
|
68
|
+
skillstate generate skills/qa/SKILL.md --name qa-state --install
|
|
69
|
+
skillstate validate qa-state
|
|
70
|
+
skillstate doctor --mcp
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
After your host discovers the installed `generate-skill-state` skill, ask it to generate state from an existing skill and install the result. Invoke the generated `qa-state` skill to run the procedure with checkpoints. Generating a definition does not execute the task.
|
|
74
|
+
|
|
75
|
+
`generate` preserves the source procedure and adds bounded progress fields. Domain-specific generation uses the active agent's proposal:
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
skillstate generate skills/qa/SKILL.md --prepare
|
|
79
|
+
skillstate generate skills/qa/SKILL.md --proposal proposal.json --source-hash HASH_FROM_PREPARE --install
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
The preparation response contains source text, its fingerprint and the proposal schema. The agent writes `proposal.json`; the library validates it and refuses stale source fingerprints. Structural validation is not proof that every business rule is correct.
|
|
83
|
+
|
|
84
|
+
You can also supply an explicitly configured endpoint with `--base-url` and `--model`. Terminal commands do not borrow your IDE's model credentials.
|
|
85
|
+
|
|
86
|
+
## Python integration
|
|
87
|
+
|
|
88
|
+
This complete example uses a scripted model. Replace `model` and `record` with your own model and service functions:
|
|
89
|
+
|
|
90
|
+
```python
|
|
91
|
+
import asyncio
|
|
92
|
+
from skillstate import Skill, SkillRuntime, SQLiteStore, Tool, ToolResult
|
|
93
|
+
|
|
94
|
+
skill = Skill(
|
|
95
|
+
name="record-job",
|
|
96
|
+
instructions="Record the job, then finish after its result is confirmed.",
|
|
97
|
+
state_schema={
|
|
98
|
+
"type": "object",
|
|
99
|
+
"properties": {"recorded": {"type": "boolean"}},
|
|
100
|
+
"required": ["recorded"],
|
|
101
|
+
"additionalProperties": False,
|
|
102
|
+
},
|
|
103
|
+
initial_state={"recorded": False},
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
def model(context):
|
|
107
|
+
if context["state"]["recorded"]:
|
|
108
|
+
return {"patch": [], "action": None, "done": True}
|
|
109
|
+
return {
|
|
110
|
+
"patch": [{"op": "set", "path": "/recorded", "value": True}],
|
|
111
|
+
"action": {"name": "record", "arguments": {}},
|
|
112
|
+
"done": False,
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
def record(arguments, operation_id):
|
|
116
|
+
# For a real API, forward operation_id to its idempotency facility when
|
|
117
|
+
# supported and inspect the actual response before reporting success.
|
|
118
|
+
return ToolResult(True, {"recorded": True})
|
|
119
|
+
|
|
120
|
+
async def main():
|
|
121
|
+
with SQLiteStore() as store: # Pass a local file path for durable storage.
|
|
122
|
+
store.create("job-001", skill, "worker")
|
|
123
|
+
tool = Tool("record", "Record a job",
|
|
124
|
+
{"type": "object", "additionalProperties": False}, record)
|
|
125
|
+
runtime = SkillRuntime(store, model, [tool],
|
|
126
|
+
completion_check=lambda state: state["recorded"])
|
|
127
|
+
result = await runtime.run("job-001", "worker", max_steps=10)
|
|
128
|
+
print(result["status"], result["state"])
|
|
129
|
+
|
|
130
|
+
asyncio.run(main())
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
For persistent storage, pass a database path to `SQLiteStore`. Reopen the same database and run ID to resume; creating a duplicate run is rejected.
|
|
134
|
+
|
|
135
|
+
## Execution contract
|
|
136
|
+
|
|
137
|
+
- Explicit `set`/`delete` patches with JSON Pointer paths, inline JSON Schema and UTF-8 size limits.
|
|
138
|
+
- State and tool arguments validated before a managed tool executes.
|
|
139
|
+
- Persistent operation intent before tool execution; atomic state, result and event commit afterward.
|
|
140
|
+
- Failed tools retain the prior state. Exceptions, timeouts and ambiguous results block replay until reconciliation.
|
|
141
|
+
- Revision checks prevent competing clients from silently overwriting each other.
|
|
142
|
+
- Immutable skill definitions, source drift detection and sequential owner handoff.
|
|
143
|
+
- Large text artifacts kept outside the active context.
|
|
144
|
+
|
|
145
|
+
Native host skills provide state and checkpoints. They do **not** replace host conversation history or intercept every native tool. Use the managed Python runtime with a stateless model for history-free model inputs.
|
|
146
|
+
|
|
147
|
+
SQLite does not make external APIs transactional. Owner names coordinate local clients; they are not authentication. The library validates reported results but cannot independently prove external business truth. Keep local state private and test recovery behavior for your application.
|
|
148
|
+
|
|
149
|
+
## Compatibility and documentation
|
|
150
|
+
|
|
151
|
+
Adapters generate project skill files and optional MCP configuration for Codex, Claude Code and Antigravity. They target documented integration surfaces. MCP protocol tests and configuration discovery do not certify every IDE version or live task.
|
|
152
|
+
|
|
153
|
+
The source distribution includes `docs/cli.md`, `docs/architecture.md`, `docs/compatibility.md`, `docs/validation.md`, `docs/README_TR.md` and runnable examples. Download the source archive from this package's files to read them offline. The development repository currently requires collaborator access.
|
|
154
|
+
|
|
155
|
+
## Research
|
|
156
|
+
|
|
157
|
+
An independent implementation inspired by [SKILL.state: Scalable Long-Horizon Agent Skills](https://arxiv.org/abs/2608.26263). Not affiliated with the paper's authors. The compiler, host adapters and operation journal are engineering extensions; no reproduction of the paper's benchmark scores or token savings is claimed.
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
skillstate/__init__.py,sha256=HafjazXaBSNs7d092K4h4JenqfjKkWBiVAAxmGnxVr0,757
|
|
2
|
+
skillstate/__main__.py,sha256=k1ocEWawweo1qCJWNFAAvyxz3tcY13dzvCenHszij30,48
|
|
3
|
+
skillstate/artifacts.py,sha256=nJXt1Wmv1-Drja2RcDdVnNdbB7Jbz1OkPrnjSpqeY2A,2083
|
|
4
|
+
skillstate/cli.py,sha256=33XyLwAFTsTdzEtQMKDCXmhiaz2BSHTW4vvic3gADmk,10719
|
|
5
|
+
skillstate/compiler.py,sha256=IYzkzffbRdcQQXCmdpQshpxhTnxI35w8DDQCVvtPdqY,14978
|
|
6
|
+
skillstate/demo.py,sha256=GMbpROEginms1mdaUAFnVqx5lXHR3m2v7_Y9dPKqVQA,2193
|
|
7
|
+
skillstate/errors.py,sha256=5BfKLGTARdABa22bw70KVkmm7d8ZKivP4Bmqe5HPPsw,598
|
|
8
|
+
skillstate/hosts.py,sha256=v8kqZbYQ7SUOlYVpKEwplh5JJsKKvGZDjuoA2MuDy3Q,13784
|
|
9
|
+
skillstate/jsonio.py,sha256=cfvRVds7UK9Nzedl5S6qcPjX_JBpX6KZr3smaBXXjXA,4927
|
|
10
|
+
skillstate/mcp_server.py,sha256=1HC8hJ3U8_Ab33yK95y3U0pqvtH4fqzhF8wlw7pWWk8,5592
|
|
11
|
+
skillstate/models.py,sha256=INE9zYIru0ae4pbWatcpCFRzyM9uCee5R_1vfDLg-bc,2618
|
|
12
|
+
skillstate/providers.py,sha256=dN7RuP-5w0-PNewginE1jyt2IM4PbUk8QEVtfrJFb_o,3650
|
|
13
|
+
skillstate/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
14
|
+
skillstate/runtime.py,sha256=0shW9knRnVu9bxFlPWncq_e0CZ4HTJhl7dXTMzmjdA0,7768
|
|
15
|
+
skillstate/schema.py,sha256=zfbm_euLdCVK8ihM2fO6_67gcXLxL1mdt-M43rPGP4w,3967
|
|
16
|
+
skillstate/service.py,sha256=bRNYgaRSXGIQ0WX6xBpgMm2Ao_BBfR8MybsjfGjLLk0,2233
|
|
17
|
+
skillstate/store.py,sha256=1SQ-lAzWP8cB4YgYwQVA__3CUfu6WNM0I-mLtG_mrEw,14060
|
|
18
|
+
skillstate_kit-0.1.1.dist-info/METADATA,sha256=VRdG7gmSA305HCJLnNhvTpLgJSjHXIe0TQkkaNNm834,7481
|
|
19
|
+
skillstate_kit-0.1.1.dist-info/WHEEL,sha256=zOwg4jB6zX2kU910N-cMawjivD6tO8NEWvE12je1bVk,87
|
|
20
|
+
skillstate_kit-0.1.1.dist-info/entry_points.txt,sha256=96xGFcS705trwVFkVCR42YJCwYDZjmD8ajaKCOKGx9g,51
|
|
21
|
+
skillstate_kit-0.1.1.dist-info/licenses/LICENSE,sha256=7GRdWqwPbYWWkOBJqMbsMtHkZR_IvUbHoyixEnwXQPI,1068
|
|
22
|
+
skillstate_kit-0.1.1.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Atakan Emre
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|