lackpy 0.1.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.
Files changed (81) hide show
  1. lackpy-0.1.0/.github/workflows/ci.yml +57 -0
  2. lackpy-0.1.0/.gitignore +9 -0
  3. lackpy-0.1.0/.readthedocs.yaml +16 -0
  4. lackpy-0.1.0/PKG-INFO +34 -0
  5. lackpy-0.1.0/docs/concepts/architecture.md +122 -0
  6. lackpy-0.1.0/docs/concepts/inference.md +170 -0
  7. lackpy-0.1.0/docs/concepts/kits.md +142 -0
  8. lackpy-0.1.0/docs/concepts/language-spec.md +170 -0
  9. lackpy-0.1.0/docs/css/extra.css +4 -0
  10. lackpy-0.1.0/docs/extending/custom-rules.md +211 -0
  11. lackpy-0.1.0/docs/extending/inference-providers.md +175 -0
  12. lackpy-0.1.0/docs/extending/tool-providers.md +152 -0
  13. lackpy-0.1.0/docs/getting-started.md +183 -0
  14. lackpy-0.1.0/docs/index.md +77 -0
  15. lackpy-0.1.0/docs/reference/api.md +127 -0
  16. lackpy-0.1.0/docs/reference/cli.md +330 -0
  17. lackpy-0.1.0/docs/superpowers/plans/2026-03-30-docs-and-publishing.md +548 -0
  18. lackpy-0.1.0/docs/superpowers/plans/2026-03-30-lackpy-v1.md +3535 -0
  19. lackpy-0.1.0/docs/superpowers/specs/2026-03-30-lackpy-design.md +870 -0
  20. lackpy-0.1.0/docs/tutorial.md +487 -0
  21. lackpy-0.1.0/mkdocs.yml +79 -0
  22. lackpy-0.1.0/pyproject.toml +42 -0
  23. lackpy-0.1.0/src/lackpy/__init__.py +24 -0
  24. lackpy-0.1.0/src/lackpy/cli.py +255 -0
  25. lackpy-0.1.0/src/lackpy/config.py +56 -0
  26. lackpy-0.1.0/src/lackpy/infer/__init__.py +1 -0
  27. lackpy-0.1.0/src/lackpy/infer/dispatch.py +60 -0
  28. lackpy-0.1.0/src/lackpy/infer/prompt.py +51 -0
  29. lackpy-0.1.0/src/lackpy/infer/providers/__init__.py +1 -0
  30. lackpy-0.1.0/src/lackpy/infer/providers/anthropic.py +46 -0
  31. lackpy-0.1.0/src/lackpy/infer/providers/base.py +15 -0
  32. lackpy-0.1.0/src/lackpy/infer/providers/ollama.py +50 -0
  33. lackpy-0.1.0/src/lackpy/infer/providers/rules.py +46 -0
  34. lackpy-0.1.0/src/lackpy/infer/providers/templates.py +97 -0
  35. lackpy-0.1.0/src/lackpy/infer/sanitize.py +22 -0
  36. lackpy-0.1.0/src/lackpy/kit/__init__.py +1 -0
  37. lackpy-0.1.0/src/lackpy/kit/providers/__init__.py +1 -0
  38. lackpy-0.1.0/src/lackpy/kit/providers/base.py +14 -0
  39. lackpy-0.1.0/src/lackpy/kit/providers/builtin.py +51 -0
  40. lackpy-0.1.0/src/lackpy/kit/providers/python.py +31 -0
  41. lackpy-0.1.0/src/lackpy/kit/registry.py +95 -0
  42. lackpy-0.1.0/src/lackpy/kit/toolbox.py +59 -0
  43. lackpy-0.1.0/src/lackpy/lang/__init__.py +1 -0
  44. lackpy-0.1.0/src/lackpy/lang/grader.py +29 -0
  45. lackpy-0.1.0/src/lackpy/lang/grammar.py +60 -0
  46. lackpy-0.1.0/src/lackpy/lang/rules.py +65 -0
  47. lackpy-0.1.0/src/lackpy/lang/spec.py +23 -0
  48. lackpy-0.1.0/src/lackpy/lang/validator.py +103 -0
  49. lackpy-0.1.0/src/lackpy/mcp/__init__.py +1 -0
  50. lackpy-0.1.0/src/lackpy/mcp/server.py +85 -0
  51. lackpy-0.1.0/src/lackpy/py.typed +0 -0
  52. lackpy-0.1.0/src/lackpy/run/__init__.py +1 -0
  53. lackpy-0.1.0/src/lackpy/run/base.py +24 -0
  54. lackpy-0.1.0/src/lackpy/run/runner.py +68 -0
  55. lackpy-0.1.0/src/lackpy/run/trace.py +67 -0
  56. lackpy-0.1.0/src/lackpy/service.py +190 -0
  57. lackpy-0.1.0/tests/__init__.py +0 -0
  58. lackpy-0.1.0/tests/conftest.py +1 -0
  59. lackpy-0.1.0/tests/infer/__init__.py +0 -0
  60. lackpy-0.1.0/tests/infer/test_anthropic_provider.py +25 -0
  61. lackpy-0.1.0/tests/infer/test_dispatch.py +58 -0
  62. lackpy-0.1.0/tests/infer/test_ollama.py +31 -0
  63. lackpy-0.1.0/tests/infer/test_prompt.py +31 -0
  64. lackpy-0.1.0/tests/infer/test_rules.py +41 -0
  65. lackpy-0.1.0/tests/infer/test_sanitize.py +36 -0
  66. lackpy-0.1.0/tests/infer/test_templates.py +59 -0
  67. lackpy-0.1.0/tests/kit/__init__.py +0 -0
  68. lackpy-0.1.0/tests/kit/test_registry.py +68 -0
  69. lackpy-0.1.0/tests/kit/test_toolbox.py +57 -0
  70. lackpy-0.1.0/tests/lang/__init__.py +0 -0
  71. lackpy-0.1.0/tests/lang/test_grader.py +31 -0
  72. lackpy-0.1.0/tests/lang/test_grammar.py +46 -0
  73. lackpy-0.1.0/tests/lang/test_rules.py +54 -0
  74. lackpy-0.1.0/tests/lang/test_validator.py +180 -0
  75. lackpy-0.1.0/tests/run/__init__.py +0 -0
  76. lackpy-0.1.0/tests/run/test_runner.py +75 -0
  77. lackpy-0.1.0/tests/run/test_trace.py +67 -0
  78. lackpy-0.1.0/tests/test_cli.py +44 -0
  79. lackpy-0.1.0/tests/test_config.py +49 -0
  80. lackpy-0.1.0/tests/test_integration.py +97 -0
  81. lackpy-0.1.0/tests/test_service.py +78 -0
@@ -0,0 +1,57 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ tags: ["v*"]
7
+ pull_request:
8
+
9
+ jobs:
10
+ test:
11
+ runs-on: ubuntu-latest
12
+ strategy:
13
+ matrix:
14
+ python-version: ["3.10", "3.11", "3.12"]
15
+ steps:
16
+ - uses: actions/checkout@v4
17
+ - uses: actions/setup-python@v5
18
+ with:
19
+ python-version: ${{ matrix.python-version }}
20
+ - name: Install
21
+ run: pip install -e ".[dev]"
22
+ - name: Run tests
23
+ run: pytest tests/ -v
24
+
25
+ build:
26
+ if: startsWith(github.ref, 'refs/tags/v')
27
+ needs: test
28
+ runs-on: ubuntu-latest
29
+ steps:
30
+ - uses: actions/checkout@v4
31
+ - uses: actions/setup-python@v5
32
+ with:
33
+ python-version: "3.12"
34
+ - name: Install build tools
35
+ run: pip install hatch
36
+ - name: Build package
37
+ run: hatch build
38
+ - name: Upload artifacts
39
+ uses: actions/upload-artifact@v4
40
+ with:
41
+ name: dist
42
+ path: dist/
43
+
44
+ publish:
45
+ needs: build
46
+ runs-on: ubuntu-latest
47
+ environment: pypi
48
+ permissions:
49
+ id-token: write
50
+ steps:
51
+ - name: Download artifacts
52
+ uses: actions/download-artifact@v4
53
+ with:
54
+ name: dist
55
+ path: dist/
56
+ - name: Publish to PyPI
57
+ uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,9 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ *.egg-info/
4
+ dist/
5
+ build/
6
+ .eggs/
7
+ *.egg
8
+ .pytest_cache/
9
+ .mypy_cache/
@@ -0,0 +1,16 @@
1
+ version: 2
2
+
3
+ build:
4
+ os: ubuntu-22.04
5
+ tools:
6
+ python: "3.12"
7
+
8
+ mkdocs:
9
+ configuration: mkdocs.yml
10
+
11
+ python:
12
+ install:
13
+ - method: pip
14
+ path: .
15
+ extra_requirements:
16
+ - docs
lackpy-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,34 @@
1
+ Metadata-Version: 2.4
2
+ Name: lackpy
3
+ Version: 0.1.0
4
+ Summary: Python that lacks most of Python. Restricted program generation and execution for tool composition.
5
+ Project-URL: Documentation, https://lackpy.readthedocs.io
6
+ Project-URL: Repository, https://github.com/teague/lackpy
7
+ Requires-Python: >=3.10
8
+ Provides-Extra: anthropic
9
+ Requires-Dist: anthropic>=0.40; extra == 'anthropic'
10
+ Provides-Extra: blq
11
+ Requires-Dist: blq-cli; extra == 'blq'
12
+ Provides-Extra: dev
13
+ Requires-Dist: pytest-asyncio>=0.24; extra == 'dev'
14
+ Requires-Dist: pytest>=8; extra == 'dev'
15
+ Provides-Extra: docs
16
+ Requires-Dist: mkdocs-gen-files>=0.5; extra == 'docs'
17
+ Requires-Dist: mkdocs-literate-nav>=0.6; extra == 'docs'
18
+ Requires-Dist: mkdocs-material>=9; extra == 'docs'
19
+ Requires-Dist: mkdocstrings[python]>=0.24; extra == 'docs'
20
+ Provides-Extra: fledgling
21
+ Requires-Dist: fledgling; extra == 'fledgling'
22
+ Provides-Extra: full
23
+ Requires-Dist: anthropic>=0.40; extra == 'full'
24
+ Requires-Dist: blq-cli; extra == 'full'
25
+ Requires-Dist: fledgling; extra == 'full'
26
+ Requires-Dist: mcp[cli]; extra == 'full'
27
+ Requires-Dist: nsjail-python; extra == 'full'
28
+ Requires-Dist: ollama>=0.4; extra == 'full'
29
+ Provides-Extra: mcp
30
+ Requires-Dist: mcp[cli]; extra == 'mcp'
31
+ Provides-Extra: ollama
32
+ Requires-Dist: ollama>=0.4; extra == 'ollama'
33
+ Provides-Extra: sandbox
34
+ Requires-Dist: nsjail-python; extra == 'sandbox'
@@ -0,0 +1,122 @@
1
+ # Architecture
2
+
3
+ ## Pipeline
4
+
5
+ A `delegate()` call traverses four stages in sequence:
6
+
7
+ ```
8
+ ┌────────────────────────────────────────────────────────────────┐
9
+ │ LackpyService │
10
+ │ │
11
+ │ 1. Kit resolution │
12
+ │ kit name/list/dict ──► ResolvedKit (tools + callables) │
13
+ │ │
14
+ │ 2. Inference │
15
+ │ intent + namespace_desc │
16
+ │ ──► InferenceDispatcher │
17
+ │ tier 0: TemplatesProvider (regex pattern match) │
18
+ │ tier 1: RulesProvider (keyword rules) │
19
+ │ tier 2: OllamaProvider (local LLM, optional) │
20
+ │ tier 3: AnthropicProvider (cloud LLM, optional) │
21
+ │ ◄── GenerationResult (program + provider_name + time_ms) │
22
+ │ │
23
+ │ 3. Validation │
24
+ │ program + allowed_names + extra_rules │
25
+ │ ──► validate() (AST walk) │
26
+ │ ◄── ValidationResult (valid, errors, calls, variables) │
27
+ │ │
28
+ │ 4. Execution │
29
+ │ program + resolved.callables + param_values │
30
+ │ ──► RestrictedRunner.run() │
31
+ │ ◄── ExecutionResult (success, output, trace, variables) │
32
+ └────────────────────────────────────────────────────────────────┘
33
+ ```
34
+
35
+ Validation is also performed inside `InferenceDispatcher` after each provider attempt. If a provider returns an invalid program, the dispatcher feeds the errors back to the provider for a retry before moving to the next tier.
36
+
37
+ ---
38
+
39
+ ## Modules
40
+
41
+ | Module | Responsibility | Key dependencies |
42
+ |--------|---------------|-----------------|
43
+ | `lackpy.service` | Unified service layer; wires all components together | All modules |
44
+ | `lackpy.config` | Load `config.toml` from `.lackpy/` | stdlib `tomllib` / `tomli` |
45
+ | `lackpy.lang.grammar` | `ALLOWED_NODES`, `FORBIDDEN_NODES`, `FORBIDDEN_NAMES`, `ALLOWED_BUILTINS` | `ast` |
46
+ | `lackpy.lang.validator` | AST walk + rule application → `ValidationResult` | `lang.grammar` |
47
+ | `lackpy.lang.grader` | `Grade(w, d)` computation from tool specs | none |
48
+ | `lackpy.lang.rules` | Built-in custom rule callables | `ast` |
49
+ | `lackpy.lang.spec` | Machine-readable grammar spec (used by `lackpy spec`) | `lang.grammar` |
50
+ | `lackpy.kit.toolbox` | `Toolbox` — provider registry + tool resolution | none |
51
+ | `lackpy.kit.registry` | `resolve_kit()` — name/list/dict → `ResolvedKit` | `kit.toolbox`, `lang.grader` |
52
+ | `lackpy.kit.providers.builtin` | Built-in tools: `read`, `glob`, `write`, `edit` | `pathlib` |
53
+ | `lackpy.kit.providers.python` | Wrap any importable function as a tool | `importlib` |
54
+ | `lackpy.run.trace` | `Trace`, `TraceEntry`, `make_traced()` | `inspect`, `time` |
55
+ | `lackpy.run.base` | `ExecutionResult`, `Executor` protocol | `run.trace` |
56
+ | `lackpy.run.runner` | `RestrictedRunner` — compile + exec with traced namespace | `lang.grammar`, `run.trace` |
57
+ | `lackpy.infer.dispatch` | `InferenceDispatcher` — priority-ordered provider loop | `lang.validator`, `infer.sanitize` |
58
+ | `lackpy.infer.prompt` | `build_system_prompt()`, `format_params_description()` | `lang.grammar` |
59
+ | `lackpy.infer.sanitize` | Strip model artifacts (markdown fences, preambles) | none |
60
+ | `lackpy.infer.providers.*` | `TemplatesProvider`, `RulesProvider`, `OllamaProvider`, `AnthropicProvider` | `infer.prompt` |
61
+ | `lackpy.cli` | `argparse`-based CLI; calls `LackpyService` | `service` |
62
+ | `lackpy.mcp` | MCP server exposing the service as tools | `service` |
63
+
64
+ ---
65
+
66
+ ## Service layer role
67
+
68
+ `LackpyService` is the single entry point. Both the CLI (`lackpy.cli`) and the MCP server (`lackpy.mcp`) call the service methods rather than accessing lower-level modules directly. This means:
69
+
70
+ - The MCP server and CLI always have identical behaviour.
71
+ - Third-party code using the Python API benefits from the same validation, tracing, and grade computation as the built-in interfaces.
72
+ - Configuration is loaded once at `LackpyService.__init__` and propagated automatically.
73
+
74
+ ---
75
+
76
+ ## Security model
77
+
78
+ lackpy uses three layers of defence in depth:
79
+
80
+ ### Layer 1 — AST validation (primary)
81
+
82
+ Every program is parsed and walked before it is compiled or executed. `ALLOWED_NODES` is a whitelist: if a node type is not in the set, the program is rejected. This means:
83
+
84
+ - `import` / `from ... import` → structurally impossible
85
+ - `def` / `class` / `lambda` → structurally impossible
86
+ - `while` / `try` / `except` → structurally impossible
87
+
88
+ The validator also checks:
89
+
90
+ - All function calls are to names in the kit or `ALLOWED_BUILTINS`
91
+ - No name in `FORBIDDEN_NAMES` appears anywhere
92
+ - No string constant contains `__` (prevents dunder access via `getattr`)
93
+ - `for` loops must iterate over a function call or a variable (not a literal)
94
+
95
+ ### Layer 2 — Restricted execution namespace (secondary)
96
+
97
+ `RestrictedRunner` executes programs with `__builtins__` set to `{}` (the empty dict). The only names available at runtime are:
98
+
99
+ - Kit tools (wrapped in tracing callables)
100
+ - `ALLOWED_BUILTINS` (direct references from the `builtins` module)
101
+ - Parameter values
102
+
103
+ Even if a program somehow bypassed the AST check, it could not call `eval`, `exec`, `compile`, or any other dangerous builtin — they are simply not in scope.
104
+
105
+ ### Layer 3 — nsjail (v2, planned)
106
+
107
+ A future `sandbox` tier will use nsjail for process-level isolation with configurable memory and time limits. The `sandbox_enabled` config flag and `sandbox` parameter on `delegate`/`run_program` are already wired; the nsjail integration is slated for v2.
108
+
109
+ ---
110
+
111
+ ## Grade system
112
+
113
+ Every kit has a `Grade(w, d)` computed from its tools:
114
+
115
+ | Field | Meaning | Scale |
116
+ |-------|---------|-------|
117
+ | `w` | World coupling | 0 = pure, 1 = pinhole read, 2 = scoped exec, 3 = scoped write |
118
+ | `d` | Effects ceiling | 0–3, higher = more side effects |
119
+
120
+ `compute_grade()` takes the element-wise maximum across all tools in the kit. The grade is reported in every `delegate()` result and `kit_info()` response so callers can decide whether a given kit is acceptable for their context.
121
+
122
+ Tool authors set `grade_w` and `effects_ceiling` on their `ToolSpec`. The built-in tools default to `grade_w=3, effects_ceiling=3` (conservative).
@@ -0,0 +1,170 @@
1
+ # Inference Pipeline
2
+
3
+ ## Tiers
4
+
5
+ | Tier | Provider | Plugin | Latency | Requires |
6
+ |------|----------|--------|---------|----------|
7
+ | 0 | `TemplatesProvider` | built-in | ~0 ms | `.lackpy/templates/*.tmpl` files |
8
+ | 1 | `RulesProvider` | built-in | ~0 ms | nothing |
9
+ | 2 | `OllamaProvider` | `ollama` | 200–2000 ms | `pip install lackpy[ollama]`, running Ollama |
10
+ | 3 | `AnthropicProvider` | `anthropic` | 500–3000 ms | `pip install lackpy[full]`, `ANTHROPIC_API_KEY` |
11
+
12
+ The dispatcher tries each available provider in priority order. A provider is skipped if `available()` returns `False` (e.g. the `ollama` package is not installed). If a provider returns a syntactically valid program that fails AST validation, the dispatcher feeds the errors back for one retry before moving on.
13
+
14
+ ---
15
+
16
+ ## Tier 0 — Templates
17
+
18
+ Templates are `.tmpl` files in `.lackpy/templates/`. Each file contains a frontmatter block and a program body:
19
+
20
+ ```
21
+ ---
22
+ name: read-file
23
+ pattern: "read the file {path}"
24
+ success_count: 12
25
+ fail_count: 0
26
+ ---
27
+ content = read('{path}')
28
+ content
29
+ ```
30
+
31
+ The `pattern` field is a mini-template: `{name}` placeholders are converted to named regex groups. The intent is matched case-insensitively. On match, placeholders in the program body are substituted with the captured values.
32
+
33
+ Templates are checked in sorted filename order. The first match wins.
34
+
35
+ ---
36
+
37
+ ## Tier 1 — Rules
38
+
39
+ The rules tier uses direct regex matching for common intents. It handles:
40
+
41
+ - `read (the )? file <path>` → `content = read('<path>')\ncontent`
42
+ - `find (the )? definition(s)? (of|for) <name>` → `results = find_definitions('<name>')\nresults`
43
+ - `find (all)? callers|usages|references (of|for) <name>` → `results = find_callers('<name>')\nresults`
44
+ - `(find|list) all <ext> files` → `files = glob('**/*.<ext>')\nfiles`
45
+ - `glob <pattern>` → `files = glob('<pattern>')\nfiles`
46
+
47
+ Rules are only applied if the corresponding tool name appears in the namespace description. The rules tier always returns `available() = True`.
48
+
49
+ ---
50
+
51
+ ## Tier 2 — Ollama
52
+
53
+ The Ollama provider sends a structured system prompt + user intent to a local model. The system prompt describes:
54
+
55
+ - The available tools and their signatures
56
+ - The `ALLOWED_BUILTINS`
57
+ - Any pre-set parameter variables
58
+ - The constraints (no `import`, `def`, `class`, etc.)
59
+
60
+ If the first generation fails validation, the errors are appended to the user message and the model is called again once.
61
+
62
+ ---
63
+
64
+ ## Tier 3 — Anthropic
65
+
66
+ The Anthropic provider works identically to the Ollama provider but calls the Anthropic Messages API. It is intended as a high-quality fallback for intents that a small local model cannot handle.
67
+
68
+ ---
69
+
70
+ ## Dispatch flow
71
+
72
+ ```
73
+ for provider in providers:
74
+ if not provider.available():
75
+ continue
76
+
77
+ raw = await provider.generate(intent, namespace_desc)
78
+ program = sanitize_output(raw)
79
+ result = validate(program, allowed_names, extra_rules)
80
+
81
+ if result.valid:
82
+ return GenerationResult(program, provider.name, elapsed_ms)
83
+
84
+ # One retry with error feedback
85
+ raw = await provider.generate(intent, namespace_desc, error_feedback=result.errors)
86
+ program = sanitize_output(raw)
87
+ result = validate(program, allowed_names, extra_rules)
88
+
89
+ if result.valid:
90
+ return GenerationResult(program, provider.name, elapsed_ms)
91
+
92
+ raise RuntimeError("All providers failed")
93
+ ```
94
+
95
+ ---
96
+
97
+ ## Config example
98
+
99
+ ```toml
100
+ [inference]
101
+ order = ["templates", "rules", "ollama-local", "anthropic-fallback"]
102
+
103
+ [inference.providers.ollama-local]
104
+ plugin = "ollama"
105
+ host = "http://localhost:11434"
106
+ model = "qwen2.5-coder:1.5b"
107
+ temperature = 0.2
108
+ keep_alive = "30m"
109
+
110
+ [inference.providers.anthropic-fallback]
111
+ plugin = "anthropic"
112
+ model = "claude-haiku-4-5-20251001"
113
+ ```
114
+
115
+ The `order` list controls priority. Built-in providers (`templates`, `rules`) are always prepended regardless of their position in `order`.
116
+
117
+ ---
118
+
119
+ ## The ratchet
120
+
121
+ The ratchet pattern is a workflow built on top of the template tier:
122
+
123
+ 1. Issue `delegate` — the intent is handled by rules or an LLM on the first call.
124
+ 2. Verify the result is correct.
125
+ 3. Issue `create` to save the validated program as a template with an intent pattern.
126
+ 4. Subsequent `delegate` calls with matching intents hit tier 0 — zero latency, guaranteed valid.
127
+
128
+ Over time, the template library grows and LLM calls become less frequent. The template tier acts as a ratchet: once an intent is captured, it stays captured.
129
+
130
+ ```bash
131
+ # Step 1: first run (rules tier)
132
+ lackpy delegate "read the file pyproject.toml" --kit read
133
+
134
+ # Step 2: save as template
135
+ cat > read_pyproject.py << 'EOF'
136
+ content = read('pyproject.toml')
137
+ content
138
+ EOF
139
+ lackpy create read_pyproject.py --name read-pyproject --kit read
140
+
141
+ # Step 3: future runs hit tier 0
142
+ lackpy delegate "read the file pyproject.toml" --kit read
143
+ # generation_tier: "templates"
144
+ ```
145
+
146
+ ---
147
+
148
+ ## Custom providers
149
+
150
+ Inference providers implement a simple protocol. See [Extending: Inference Providers](../extending/inference-providers.md) for the full guide.
151
+
152
+ The minimum interface is:
153
+
154
+ ```python
155
+ class MyProvider:
156
+ @property
157
+ def name(self) -> str: ...
158
+
159
+ def available(self) -> bool: ...
160
+
161
+ async def generate(
162
+ self,
163
+ intent: str,
164
+ namespace_desc: str,
165
+ config: dict | None = None,
166
+ error_feedback: list[str] | None = None,
167
+ ) -> str | None: ...
168
+ ```
169
+
170
+ Register the provider on the service's dispatcher by appending it to `svc._inference_providers` before calling `delegate` or `generate`.
@@ -0,0 +1,142 @@
1
+ # Kits & Toolbox
2
+
3
+ ## Toolbox vs Kits
4
+
5
+ | Concept | What it is | Scope |
6
+ |---------|------------|-------|
7
+ | **Toolbox** | The global registry of all available tools and their providers | Service-wide |
8
+ | **Kit** | A named subset of toolbox tools for a specific task | Per-request |
9
+
10
+ The `Toolbox` holds every tool that has been registered across all providers. A `Kit` is the subset of those tools that a particular program may call — it defines the allowed namespace for validation and the callable namespace for execution.
11
+
12
+ ---
13
+
14
+ ## ToolSpec fields
15
+
16
+ `ToolSpec` is the metadata record for a single tool:
17
+
18
+ | Field | Type | Description |
19
+ |-------|------|-------------|
20
+ | `name` | `str` | The function name used in lackpy programs |
21
+ | `provider` | `str` | Provider name that resolves this tool (e.g. `"builtin"`, `"python"`) |
22
+ | `provider_config` | `dict` | Provider-specific config (e.g. `module`, `function` for the `python` provider) |
23
+ | `description` | `str` | Human-readable description, shown to LLMs in the system prompt |
24
+ | `args` | `list[ArgSpec]` | Argument names, types, and descriptions |
25
+ | `returns` | `str` | Return type annotation string |
26
+ | `grade_w` | `int` | World coupling (0–3) |
27
+ | `effects_ceiling` | `int` | Effects ceiling (0–3) |
28
+
29
+ `ArgSpec` fields: `name`, `type` (string), `description`.
30
+
31
+ ---
32
+
33
+ ## Registering tools
34
+
35
+ Tools are registered by adding a `ToolSpec` to the `Toolbox` and ensuring a matching provider is also registered:
36
+
37
+ ```python
38
+ from lackpy import LackpyService
39
+ from lackpy.kit.toolbox import ToolSpec, ArgSpec
40
+
41
+ svc = LackpyService()
42
+
43
+ # Register a custom tool backed by a Python function
44
+ svc.toolbox.register_tool(ToolSpec(
45
+ name="count_lines",
46
+ provider="python",
47
+ provider_config={
48
+ "module": "my_tools",
49
+ "function": "count_lines",
50
+ },
51
+ description="Count the number of lines in a file",
52
+ args=[ArgSpec(name="path", type="str", description="File path")],
53
+ returns="int",
54
+ grade_w=1,
55
+ effects_ceiling=0,
56
+ ))
57
+ ```
58
+
59
+ The `python` provider is always registered. It resolves tools by importing the named module and looking up the function.
60
+
61
+ ---
62
+
63
+ ## Provider table
64
+
65
+ | Provider | Name | How it resolves tools |
66
+ |----------|------|----------------------|
67
+ | `BuiltinProvider` | `"builtin"` | Hardcoded implementations for `read`, `glob`, `write`, `edit` |
68
+ | `PythonProvider` | `"python"` | `importlib.import_module(module)` then `getattr(module, function)` |
69
+ | Custom | any string | Implement the provider protocol (see [Tool Providers](../extending/tool-providers.md)) |
70
+
71
+ ---
72
+
73
+ ## Kit parameter forms
74
+
75
+ `resolve_kit()` accepts three kit forms:
76
+
77
+ | Form | Type | Example | Behaviour |
78
+ |------|------|---------|-----------|
79
+ | Named kit | `str` | `"filesystem"` | Loads `.lackpy/kits/filesystem.kit` |
80
+ | Tool list | `list[str]` | `["read", "glob"]` | Uses tool names directly as aliases |
81
+ | Tool mapping | `dict` | `{"find": "glob"}` | Alias → actual tool name |
82
+ | Nested dict | `dict` | `{"r": {"tool": "read"}}` | Dict entry with `"tool"` key |
83
+
84
+ With the tool mapping form, the program sees `find(...)` but the toolbox resolves it to the `glob` implementation.
85
+
86
+ ---
87
+
88
+ ## Kit file format
89
+
90
+ Named kits are stored as `.kit` files in `.lackpy/kits/`:
91
+
92
+ ```
93
+ ---
94
+ name: filesystem
95
+ description: Read, write, and search files
96
+ ---
97
+ read
98
+ glob
99
+ write
100
+ edit
101
+ ```
102
+
103
+ - The YAML-like frontmatter between `---` lines is metadata.
104
+ - Lines after the closing `---` are tool names, one per line.
105
+ - Lines starting with `#` are treated as comments.
106
+ - The frontmatter is not validated against a schema; only `name` and `description` are used.
107
+
108
+ ---
109
+
110
+ ## CLI management
111
+
112
+ ```bash
113
+ # List all kits in .lackpy/kits/
114
+ lackpy kit list
115
+
116
+ # Show tools and grade for a kit
117
+ lackpy kit info filesystem
118
+
119
+ # Show tools and grade for an ad-hoc list
120
+ lackpy kit info read,glob,write
121
+
122
+ # Create a new kit
123
+ lackpy kit create mykit --tools read glob --description "Read-only tools"
124
+ ```
125
+
126
+ ---
127
+
128
+ ## Grade computation
129
+
130
+ `compute_grade(tools)` takes a dict of `{name: {"grade_w": int, "effects_ceiling": int}}` and returns the element-wise maximum across all tools:
131
+
132
+ ```python
133
+ from lackpy import compute_grade
134
+
135
+ grade = compute_grade({
136
+ "read": {"grade_w": 1, "effects_ceiling": 0},
137
+ "write": {"grade_w": 3, "effects_ceiling": 3},
138
+ })
139
+ # Grade(w=3, d=3)
140
+ ```
141
+
142
+ This grade is attached to every `ResolvedKit` and reported in `delegate()` results. The grade is informational — lackpy does not block execution based on grade values, but callers can use it to gate access in security-sensitive contexts.