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/compiler.py
ADDED
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
"""Source-preserving skill generation with deterministic and host-assisted paths."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import ast
|
|
6
|
+
import os
|
|
7
|
+
import re
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
import pathspec
|
|
11
|
+
import yaml
|
|
12
|
+
|
|
13
|
+
from .errors import BudgetExceeded, ConflictError, NotFoundError, SourceChanged, ValidationError
|
|
14
|
+
from .jsonio import atomic_write, digest, dumps, read_json, slug, within
|
|
15
|
+
from .models import Skill
|
|
16
|
+
from .schema import validate
|
|
17
|
+
|
|
18
|
+
EXCLUDED = {
|
|
19
|
+
".git",
|
|
20
|
+
".venv",
|
|
21
|
+
"venv",
|
|
22
|
+
"node_modules",
|
|
23
|
+
"__pycache__",
|
|
24
|
+
"dist",
|
|
25
|
+
"build",
|
|
26
|
+
".skillstate",
|
|
27
|
+
".pytest_cache",
|
|
28
|
+
".ruff_cache",
|
|
29
|
+
"research",
|
|
30
|
+
"generate-skill-state",
|
|
31
|
+
}
|
|
32
|
+
SUFFIXES = {".md", ".py", ".toml", ".yaml", ".yml", ".json"}
|
|
33
|
+
SECRET_NAMES = {"credentials.json", "auth.json", "secrets.json", "package-lock.json", "uv.lock"}
|
|
34
|
+
IR_SCHEMA = {
|
|
35
|
+
"type": "object",
|
|
36
|
+
"additionalProperties": False,
|
|
37
|
+
"required": ["name", "instructions", "state_schema", "initial_state", "steps"],
|
|
38
|
+
"properties": {
|
|
39
|
+
"name": {"type": "string", "maxLength": 64},
|
|
40
|
+
"instructions": {"type": "string", "minLength": 1, "maxLength": 32000},
|
|
41
|
+
"state_schema": {"type": "object"},
|
|
42
|
+
"initial_state": {"type": "object"},
|
|
43
|
+
"steps": {
|
|
44
|
+
"type": "array",
|
|
45
|
+
"minItems": 1,
|
|
46
|
+
"maxItems": 64,
|
|
47
|
+
"items": {
|
|
48
|
+
"type": "object",
|
|
49
|
+
"additionalProperties": False,
|
|
50
|
+
"required": ["id", "description", "sources"],
|
|
51
|
+
"properties": {
|
|
52
|
+
"id": {"type": "string", "pattern": "^[a-z0-9-]+$", "maxLength": 64},
|
|
53
|
+
"description": {"type": "string", "minLength": 1, "maxLength": 2000},
|
|
54
|
+
"sources": {
|
|
55
|
+
"type": "array",
|
|
56
|
+
"minItems": 1,
|
|
57
|
+
"maxItems": 32,
|
|
58
|
+
"items": {"type": "string", "maxLength": 500},
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _redact(text: str) -> str:
|
|
68
|
+
return re.sub(
|
|
69
|
+
r"""(?im)(\b(?:api[_-]?key|access[_-]?token|password|secret)\s*[:=]\s*)["'][^"'\n]+["']""",
|
|
70
|
+
r'\1"[REDACTED]"',
|
|
71
|
+
text,
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _source_allowed(path: Path) -> bool:
|
|
76
|
+
return (
|
|
77
|
+
path.suffix.lower() in SUFFIXES
|
|
78
|
+
and path.name.lower() not in SECRET_NAMES
|
|
79
|
+
and not path.name.lower().startswith(".env")
|
|
80
|
+
and not any(part in EXCLUDED for part in path.parts)
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def scan(project: Path, source: str | Path = ".") -> dict:
|
|
85
|
+
project = project.resolve()
|
|
86
|
+
source_path = within(project, source)
|
|
87
|
+
if not source_path.exists():
|
|
88
|
+
raise NotFoundError("Source does not exist")
|
|
89
|
+
patterns = []
|
|
90
|
+
for name in (".gitignore", ".skillstateignore"):
|
|
91
|
+
ignore_path = within(project, name)
|
|
92
|
+
if ignore_path.is_file():
|
|
93
|
+
if ignore_path.stat().st_size > 128_000:
|
|
94
|
+
raise BudgetExceeded("Ignore file is too large")
|
|
95
|
+
patterns.extend(ignore_path.read_text(encoding="utf-8").splitlines())
|
|
96
|
+
ignore = pathspec.PathSpec.from_lines("gitwildmatch", patterns)
|
|
97
|
+
candidates = []
|
|
98
|
+
if source_path.is_file():
|
|
99
|
+
candidates = [source_path]
|
|
100
|
+
else:
|
|
101
|
+
for base, dirs, names in os.walk(source_path, followlinks=False):
|
|
102
|
+
dirs[:] = sorted(
|
|
103
|
+
d
|
|
104
|
+
for d in dirs
|
|
105
|
+
if d not in EXCLUDED
|
|
106
|
+
and not (Path(base) / d).is_symlink()
|
|
107
|
+
and not ignore.match_file((Path(base) / d).relative_to(project).as_posix() + "/")
|
|
108
|
+
)
|
|
109
|
+
for name in sorted(names):
|
|
110
|
+
path = Path(base) / name
|
|
111
|
+
if _source_allowed(path.relative_to(project)) and not ignore.match_file(
|
|
112
|
+
path.relative_to(project).as_posix()
|
|
113
|
+
):
|
|
114
|
+
candidates.append(path)
|
|
115
|
+
if len(candidates) > 128:
|
|
116
|
+
raise BudgetExceeded(
|
|
117
|
+
"More than 128 source files; narrow the source or add .skillstateignore"
|
|
118
|
+
)
|
|
119
|
+
entries = []
|
|
120
|
+
total = 0
|
|
121
|
+
for candidate in candidates:
|
|
122
|
+
relative = candidate.relative_to(project).as_posix()
|
|
123
|
+
if not _source_allowed(Path(relative)) or ignore.match_file(relative):
|
|
124
|
+
if source_path.is_file():
|
|
125
|
+
raise ValidationError("Source is excluded or has an unsupported extension")
|
|
126
|
+
continue
|
|
127
|
+
path = within(project, candidate)
|
|
128
|
+
size = path.stat().st_size
|
|
129
|
+
if size > 96_000:
|
|
130
|
+
raise BudgetExceeded(f"Source file exceeds 96000 bytes: {relative}")
|
|
131
|
+
total += size
|
|
132
|
+
if total > 512_000:
|
|
133
|
+
raise BudgetExceeded("Source budget exceeds 512000 bytes; narrow the source")
|
|
134
|
+
try:
|
|
135
|
+
raw = path.read_bytes()
|
|
136
|
+
text = raw.decode("utf-8-sig")
|
|
137
|
+
except UnicodeError as exc:
|
|
138
|
+
raise ValidationError(f"Source must be UTF-8: {relative}") from exc
|
|
139
|
+
if "skillstate_generated: true" in text[:2000]:
|
|
140
|
+
continue
|
|
141
|
+
symbols = []
|
|
142
|
+
if path.suffix == ".py":
|
|
143
|
+
try:
|
|
144
|
+
tree = ast.parse(text)
|
|
145
|
+
symbols = [
|
|
146
|
+
{"name": node.name, "line": node.lineno, "kind": type(node).__name__}
|
|
147
|
+
for node in ast.walk(tree)
|
|
148
|
+
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef))
|
|
149
|
+
]
|
|
150
|
+
except SyntaxError:
|
|
151
|
+
symbols = [{"parse_error": "Python syntax could not be parsed"}]
|
|
152
|
+
entries.append(
|
|
153
|
+
{"path": relative, "sha256": digest(raw), "text": _redact(text), "symbols": symbols}
|
|
154
|
+
)
|
|
155
|
+
if not entries:
|
|
156
|
+
raise ValidationError("No supported source files found")
|
|
157
|
+
manifest = [{"path": entry["path"], "sha256": entry["sha256"]} for entry in entries]
|
|
158
|
+
return {
|
|
159
|
+
"source": source_path.relative_to(project).as_posix(),
|
|
160
|
+
"files": entries,
|
|
161
|
+
"manifest": manifest,
|
|
162
|
+
"source_hash": digest(dumps(manifest)),
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def _frontmatter(text: str) -> tuple[dict, str]:
|
|
167
|
+
if not text.startswith("---\n") and not text.startswith("---\r\n"):
|
|
168
|
+
return {}, text
|
|
169
|
+
parts = re.split(r"^---\s*$", text, maxsplit=2, flags=re.MULTILINE)
|
|
170
|
+
if len(parts) < 3:
|
|
171
|
+
raise ValidationError("Unclosed skill frontmatter")
|
|
172
|
+
if "&" in parts[1] or "*" in parts[1]:
|
|
173
|
+
raise ValidationError("YAML aliases are not supported in skill frontmatter")
|
|
174
|
+
try:
|
|
175
|
+
metadata = yaml.safe_load(parts[1]) or {}
|
|
176
|
+
except yaml.YAMLError as exc:
|
|
177
|
+
raise ValidationError("Invalid skill frontmatter") from exc
|
|
178
|
+
if type(metadata) is not dict:
|
|
179
|
+
raise ValidationError("Skill frontmatter must be an object")
|
|
180
|
+
return metadata, parts[2].strip()
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def tracking_ir(inventory: dict, name: str | None = None, profile: str = "auto") -> dict:
|
|
184
|
+
files = inventory["files"]
|
|
185
|
+
skill_files = [entry for entry in files if Path(entry["path"]).name == "SKILL.md"]
|
|
186
|
+
if len(skill_files) > 1:
|
|
187
|
+
raise ValidationError("Multiple skills found; generate one skill at a time")
|
|
188
|
+
primary = skill_files[0] if skill_files else files[0]
|
|
189
|
+
metadata, body = _frontmatter(primary["text"])
|
|
190
|
+
if profile not in ("auto", "tracking", "python-tests"):
|
|
191
|
+
raise ValidationError("Unknown generation profile")
|
|
192
|
+
is_python = any(entry["path"].endswith(".py") for entry in files)
|
|
193
|
+
if profile == "python-tests" and not is_python:
|
|
194
|
+
raise ValidationError("python-tests profile requires Python source")
|
|
195
|
+
base_name = str(metadata.get("name") or Path(primary["path"]).parent.name or "project")
|
|
196
|
+
base_name = re.sub("[^a-z0-9]+", "-", base_name.lower()).strip("-")[:48] or "project"
|
|
197
|
+
name = slug(name or base_name + "-state")
|
|
198
|
+
# This adapter preserves the original procedure. It does not claim to infer
|
|
199
|
+
# a domain-specific state machine from arbitrary prose.
|
|
200
|
+
descriptions = re.findall(r"^\s*\d+[.)]\s+(.+)$", body, flags=re.MULTILINE)[:32]
|
|
201
|
+
if not descriptions:
|
|
202
|
+
descriptions = [
|
|
203
|
+
"Inspect task inputs",
|
|
204
|
+
"Execute the source procedure",
|
|
205
|
+
"Verify and report the result",
|
|
206
|
+
]
|
|
207
|
+
steps = [
|
|
208
|
+
{"id": f"step-{i + 1}", "description": desc[:2000], "sources": [primary["path"]]}
|
|
209
|
+
for i, desc in enumerate(descriptions)
|
|
210
|
+
]
|
|
211
|
+
if profile == "python-tests":
|
|
212
|
+
body = (
|
|
213
|
+
"Inspect the Python project and its test configuration. Run the existing tests using "
|
|
214
|
+
"the project's documented runner. Record observed failures, make task-authorized "
|
|
215
|
+
"changes, rerun relevant tests, and provide evidence. Do not claim unexecuted tests passed."
|
|
216
|
+
)
|
|
217
|
+
instructions = (
|
|
218
|
+
f"Source procedure: {primary['path']}. Resolve its resource paths relative to "
|
|
219
|
+
f"{Path(primary['path']).parent.as_posix()}. Follow the user's current task and authorizations.\n\n"
|
|
220
|
+
+ body
|
|
221
|
+
+ "\n\nTrack active_step, completed_steps, facts, blockers and artifacts. "
|
|
222
|
+
"Only mark steps complete after checking their results. Retain facts needed by later steps. "
|
|
223
|
+
"Store large outputs as artifacts. The original source remains authoritative for procedure details."
|
|
224
|
+
)
|
|
225
|
+
state_schema = {
|
|
226
|
+
"type": "object",
|
|
227
|
+
"additionalProperties": False,
|
|
228
|
+
"required": ["goal", "active_step", "completed_steps", "facts", "blockers", "artifacts"],
|
|
229
|
+
"properties": {
|
|
230
|
+
"goal": {"type": "string", "maxLength": 2000},
|
|
231
|
+
"active_step": {"enum": [s["id"] for s in steps] + ["done"]},
|
|
232
|
+
"completed_steps": {
|
|
233
|
+
"type": "array",
|
|
234
|
+
"maxItems": len(steps),
|
|
235
|
+
"uniqueItems": True,
|
|
236
|
+
"items": {"enum": [s["id"] for s in steps]},
|
|
237
|
+
},
|
|
238
|
+
"facts": {
|
|
239
|
+
"type": "object",
|
|
240
|
+
"maxProperties": 32,
|
|
241
|
+
"additionalProperties": {"type": "string", "maxLength": 1000},
|
|
242
|
+
},
|
|
243
|
+
"blockers": {
|
|
244
|
+
"type": "array",
|
|
245
|
+
"maxItems": 16,
|
|
246
|
+
"items": {"type": "string", "maxLength": 500},
|
|
247
|
+
},
|
|
248
|
+
"artifacts": {
|
|
249
|
+
"type": "array",
|
|
250
|
+
"maxItems": 32,
|
|
251
|
+
"items": {"type": "string", "maxLength": 200},
|
|
252
|
+
},
|
|
253
|
+
},
|
|
254
|
+
}
|
|
255
|
+
return {
|
|
256
|
+
"name": name,
|
|
257
|
+
"instructions": instructions,
|
|
258
|
+
"state_schema": state_schema,
|
|
259
|
+
"initial_state": {
|
|
260
|
+
"goal": "",
|
|
261
|
+
"active_step": steps[0]["id"],
|
|
262
|
+
"completed_steps": [],
|
|
263
|
+
"facts": {},
|
|
264
|
+
"blockers": [],
|
|
265
|
+
"artifacts": [],
|
|
266
|
+
},
|
|
267
|
+
"steps": steps,
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
def prepare(project: Path, source: str | Path = ".", name: str | None = None) -> dict:
|
|
272
|
+
inventory = scan(project, source)
|
|
273
|
+
return {
|
|
274
|
+
"protocol_version": 1,
|
|
275
|
+
"inventory": inventory,
|
|
276
|
+
"proposal_schema": IR_SCHEMA,
|
|
277
|
+
"suggested_name": name,
|
|
278
|
+
"instructions": (
|
|
279
|
+
"Propose a SkillIR JSON object matching proposal_schema. Preserve the user's procedure. "
|
|
280
|
+
"Reference existing inventory paths in each step's sources. Use inline draft 2020-12 "
|
|
281
|
+
"state_schema and a valid initial_state. Bound collections and strings. Do not invent "
|
|
282
|
+
"executed results, credentials, or tools. The proposal will be checked before writing."
|
|
283
|
+
),
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
def generate(
|
|
288
|
+
project: Path,
|
|
289
|
+
source: str | Path = ".",
|
|
290
|
+
*,
|
|
291
|
+
name: str | None = None,
|
|
292
|
+
profile: str = "auto",
|
|
293
|
+
proposal: dict | None = None,
|
|
294
|
+
expected_source_hash: str | None = None,
|
|
295
|
+
) -> dict:
|
|
296
|
+
project = project.resolve()
|
|
297
|
+
inventory = scan(project, source)
|
|
298
|
+
if expected_source_hash is not None and inventory["source_hash"] != expected_source_hash:
|
|
299
|
+
raise SourceChanged("Sources changed after generation_prepare; prepare again")
|
|
300
|
+
ir = proposal if proposal is not None else tracking_ir(inventory, name, profile)
|
|
301
|
+
validate(IR_SCHEMA, ir)
|
|
302
|
+
if name is not None and ir["name"] != name:
|
|
303
|
+
raise ValidationError("Proposal name differs from requested name")
|
|
304
|
+
known = {entry["path"] for entry in inventory["files"]}
|
|
305
|
+
ids = [step["id"] for step in ir["steps"]]
|
|
306
|
+
if len(ids) != len(set(ids)):
|
|
307
|
+
raise ValidationError("Step IDs must be unique")
|
|
308
|
+
for step in ir["steps"]:
|
|
309
|
+
slug(step["id"])
|
|
310
|
+
if any(path not in known for path in step["sources"]):
|
|
311
|
+
raise ValidationError("Step references a source outside the scanned inventory")
|
|
312
|
+
skill = Skill(ir["name"], ir["instructions"], ir["state_schema"], ir["initial_state"])
|
|
313
|
+
bundle = {
|
|
314
|
+
"format_version": 1,
|
|
315
|
+
"skill": skill.to_dict(),
|
|
316
|
+
"steps": ir["steps"],
|
|
317
|
+
"source": inventory["source"],
|
|
318
|
+
"manifest": inventory["manifest"],
|
|
319
|
+
"source_hash": inventory["source_hash"],
|
|
320
|
+
"generation": {
|
|
321
|
+
"mode": "semantic" if proposal is not None else "tracking",
|
|
322
|
+
"validation": "statically_checked",
|
|
323
|
+
"behaviorally_checked": False,
|
|
324
|
+
},
|
|
325
|
+
}
|
|
326
|
+
# Content-addressed revisions keep already-running skills immutable.
|
|
327
|
+
bundle_hash = digest(dumps(bundle))
|
|
328
|
+
directory = within(project, f".skillstate/definitions/{skill.name}")
|
|
329
|
+
path = within(project, directory / f"{bundle_hash}.json")
|
|
330
|
+
if not path.exists():
|
|
331
|
+
atomic_write(path, dumps(bundle) + "\n")
|
|
332
|
+
elif read_json(path) != bundle:
|
|
333
|
+
raise ConflictError("Existing content-addressed bundle was modified")
|
|
334
|
+
atomic_write(within(project, directory / "latest.json"), dumps({"hash": bundle_hash}) + "\n")
|
|
335
|
+
return {
|
|
336
|
+
"name": skill.name,
|
|
337
|
+
"bundle_hash": bundle_hash,
|
|
338
|
+
"path": path.relative_to(project).as_posix(),
|
|
339
|
+
**bundle["generation"],
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
def load_bundle(project: Path, name: str, *, check_sources: bool = True) -> dict:
|
|
344
|
+
directory = within(project, f".skillstate/definitions/{slug(name)}")
|
|
345
|
+
pointer = within(project, directory / "latest.json")
|
|
346
|
+
if not pointer.exists():
|
|
347
|
+
raise NotFoundError(f"No generated skill: {name}")
|
|
348
|
+
latest = read_json(pointer)
|
|
349
|
+
fingerprint = latest.get("hash") if type(latest) is dict else None
|
|
350
|
+
if not isinstance(fingerprint, str) or not re.fullmatch(r"[a-f0-9]{64}", fingerprint):
|
|
351
|
+
raise ValidationError("Invalid bundle pointer")
|
|
352
|
+
bundle = read_json(within(project, directory / f"{fingerprint}.json"))
|
|
353
|
+
if digest(dumps(bundle)) != fingerprint:
|
|
354
|
+
raise ValidationError("Bundle integrity check failed")
|
|
355
|
+
if bundle.get("format_version") != 1:
|
|
356
|
+
raise ValidationError("Unsupported bundle format")
|
|
357
|
+
Skill.from_dict(bundle["skill"])
|
|
358
|
+
if check_sources:
|
|
359
|
+
for source in bundle["manifest"]:
|
|
360
|
+
path = within(project, source["path"])
|
|
361
|
+
if (
|
|
362
|
+
not path.is_file()
|
|
363
|
+
or path.stat().st_size > 96_000
|
|
364
|
+
or digest(path.read_bytes()) != source["sha256"]
|
|
365
|
+
):
|
|
366
|
+
raise SourceChanged(f"Source changed: {source['path']}; regenerate the skill")
|
|
367
|
+
return bundle
|
skillstate/demo.py
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"""Offline end-to-end runtime example; the model is explicitly scripted."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
|
|
7
|
+
from .jsonio import dumps
|
|
8
|
+
from .models import Skill, Tool, ToolResult
|
|
9
|
+
from .runtime import SkillRuntime
|
|
10
|
+
from .store import SQLiteStore
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
async def run_demo() -> dict:
|
|
14
|
+
sizes = []
|
|
15
|
+
effects = []
|
|
16
|
+
|
|
17
|
+
def model(payload):
|
|
18
|
+
sizes.append(len(dumps(payload).encode("utf-8")))
|
|
19
|
+
count = payload["state"]["count"]
|
|
20
|
+
if count == 5:
|
|
21
|
+
return {"patch": [], "action": None, "done": True}
|
|
22
|
+
return {
|
|
23
|
+
"patch": [{"op": "set", "path": "/count", "value": count + 1}],
|
|
24
|
+
"action": {"name": "record", "arguments": {"value": count + 1}},
|
|
25
|
+
"done": False,
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
def record(arguments, operation_id):
|
|
29
|
+
effects.append(operation_id)
|
|
30
|
+
return ToolResult(True, {"recorded": arguments["value"]})
|
|
31
|
+
|
|
32
|
+
skill = Skill(
|
|
33
|
+
"counter-demo",
|
|
34
|
+
"Record five numbered items, then finish.",
|
|
35
|
+
{
|
|
36
|
+
"type": "object",
|
|
37
|
+
"properties": {"count": {"type": "integer", "minimum": 0, "maximum": 5}},
|
|
38
|
+
"required": ["count"],
|
|
39
|
+
"additionalProperties": False,
|
|
40
|
+
},
|
|
41
|
+
{"count": 0},
|
|
42
|
+
)
|
|
43
|
+
tool = Tool(
|
|
44
|
+
"record",
|
|
45
|
+
"Record one item",
|
|
46
|
+
{
|
|
47
|
+
"type": "object",
|
|
48
|
+
"properties": {"value": {"type": "integer", "minimum": 1, "maximum": 5}},
|
|
49
|
+
"required": ["value"],
|
|
50
|
+
"additionalProperties": False,
|
|
51
|
+
},
|
|
52
|
+
record,
|
|
53
|
+
)
|
|
54
|
+
with SQLiteStore() as store:
|
|
55
|
+
store.create("demo", skill, "demo", {"event": "start"})
|
|
56
|
+
runtime = SkillRuntime(store, model, [tool], completion_check=lambda s: s["count"] == 5)
|
|
57
|
+
result = await runtime.run("demo", "demo")
|
|
58
|
+
return {
|
|
59
|
+
"status": result["status"],
|
|
60
|
+
"state": result["state"],
|
|
61
|
+
"model": "scripted (offline)",
|
|
62
|
+
"model_calls": len(sizes),
|
|
63
|
+
"prompt_bytes": sizes,
|
|
64
|
+
"unique_operations": len(set(effects)),
|
|
65
|
+
"events": len(store.events("demo")),
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def main():
|
|
70
|
+
print(dumps(asyncio.run(run_demo())))
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
if __name__ == "__main__":
|
|
74
|
+
main()
|
skillstate/errors.py
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"""Stable, machine-readable failure categories."""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class SkillStateError(Exception):
|
|
5
|
+
code = "skillstate_error"
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class ValidationError(SkillStateError):
|
|
9
|
+
code = "validation_error"
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class BudgetExceeded(SkillStateError):
|
|
13
|
+
code = "budget_exceeded"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class ConflictError(SkillStateError):
|
|
17
|
+
code = "conflict"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class NotFoundError(SkillStateError):
|
|
21
|
+
code = "not_found"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class OperationUncertain(SkillStateError):
|
|
25
|
+
code = "operation_uncertain"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class StepLimitExceeded(SkillStateError):
|
|
29
|
+
code = "step_limit"
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class SourceChanged(SkillStateError):
|
|
33
|
+
code = "source_changed"
|