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.
- lackpy-0.1.0/.github/workflows/ci.yml +57 -0
- lackpy-0.1.0/.gitignore +9 -0
- lackpy-0.1.0/.readthedocs.yaml +16 -0
- lackpy-0.1.0/PKG-INFO +34 -0
- lackpy-0.1.0/docs/concepts/architecture.md +122 -0
- lackpy-0.1.0/docs/concepts/inference.md +170 -0
- lackpy-0.1.0/docs/concepts/kits.md +142 -0
- lackpy-0.1.0/docs/concepts/language-spec.md +170 -0
- lackpy-0.1.0/docs/css/extra.css +4 -0
- lackpy-0.1.0/docs/extending/custom-rules.md +211 -0
- lackpy-0.1.0/docs/extending/inference-providers.md +175 -0
- lackpy-0.1.0/docs/extending/tool-providers.md +152 -0
- lackpy-0.1.0/docs/getting-started.md +183 -0
- lackpy-0.1.0/docs/index.md +77 -0
- lackpy-0.1.0/docs/reference/api.md +127 -0
- lackpy-0.1.0/docs/reference/cli.md +330 -0
- lackpy-0.1.0/docs/superpowers/plans/2026-03-30-docs-and-publishing.md +548 -0
- lackpy-0.1.0/docs/superpowers/plans/2026-03-30-lackpy-v1.md +3535 -0
- lackpy-0.1.0/docs/superpowers/specs/2026-03-30-lackpy-design.md +870 -0
- lackpy-0.1.0/docs/tutorial.md +487 -0
- lackpy-0.1.0/mkdocs.yml +79 -0
- lackpy-0.1.0/pyproject.toml +42 -0
- lackpy-0.1.0/src/lackpy/__init__.py +24 -0
- lackpy-0.1.0/src/lackpy/cli.py +255 -0
- lackpy-0.1.0/src/lackpy/config.py +56 -0
- lackpy-0.1.0/src/lackpy/infer/__init__.py +1 -0
- lackpy-0.1.0/src/lackpy/infer/dispatch.py +60 -0
- lackpy-0.1.0/src/lackpy/infer/prompt.py +51 -0
- lackpy-0.1.0/src/lackpy/infer/providers/__init__.py +1 -0
- lackpy-0.1.0/src/lackpy/infer/providers/anthropic.py +46 -0
- lackpy-0.1.0/src/lackpy/infer/providers/base.py +15 -0
- lackpy-0.1.0/src/lackpy/infer/providers/ollama.py +50 -0
- lackpy-0.1.0/src/lackpy/infer/providers/rules.py +46 -0
- lackpy-0.1.0/src/lackpy/infer/providers/templates.py +97 -0
- lackpy-0.1.0/src/lackpy/infer/sanitize.py +22 -0
- lackpy-0.1.0/src/lackpy/kit/__init__.py +1 -0
- lackpy-0.1.0/src/lackpy/kit/providers/__init__.py +1 -0
- lackpy-0.1.0/src/lackpy/kit/providers/base.py +14 -0
- lackpy-0.1.0/src/lackpy/kit/providers/builtin.py +51 -0
- lackpy-0.1.0/src/lackpy/kit/providers/python.py +31 -0
- lackpy-0.1.0/src/lackpy/kit/registry.py +95 -0
- lackpy-0.1.0/src/lackpy/kit/toolbox.py +59 -0
- lackpy-0.1.0/src/lackpy/lang/__init__.py +1 -0
- lackpy-0.1.0/src/lackpy/lang/grader.py +29 -0
- lackpy-0.1.0/src/lackpy/lang/grammar.py +60 -0
- lackpy-0.1.0/src/lackpy/lang/rules.py +65 -0
- lackpy-0.1.0/src/lackpy/lang/spec.py +23 -0
- lackpy-0.1.0/src/lackpy/lang/validator.py +103 -0
- lackpy-0.1.0/src/lackpy/mcp/__init__.py +1 -0
- lackpy-0.1.0/src/lackpy/mcp/server.py +85 -0
- lackpy-0.1.0/src/lackpy/py.typed +0 -0
- lackpy-0.1.0/src/lackpy/run/__init__.py +1 -0
- lackpy-0.1.0/src/lackpy/run/base.py +24 -0
- lackpy-0.1.0/src/lackpy/run/runner.py +68 -0
- lackpy-0.1.0/src/lackpy/run/trace.py +67 -0
- lackpy-0.1.0/src/lackpy/service.py +190 -0
- lackpy-0.1.0/tests/__init__.py +0 -0
- lackpy-0.1.0/tests/conftest.py +1 -0
- lackpy-0.1.0/tests/infer/__init__.py +0 -0
- lackpy-0.1.0/tests/infer/test_anthropic_provider.py +25 -0
- lackpy-0.1.0/tests/infer/test_dispatch.py +58 -0
- lackpy-0.1.0/tests/infer/test_ollama.py +31 -0
- lackpy-0.1.0/tests/infer/test_prompt.py +31 -0
- lackpy-0.1.0/tests/infer/test_rules.py +41 -0
- lackpy-0.1.0/tests/infer/test_sanitize.py +36 -0
- lackpy-0.1.0/tests/infer/test_templates.py +59 -0
- lackpy-0.1.0/tests/kit/__init__.py +0 -0
- lackpy-0.1.0/tests/kit/test_registry.py +68 -0
- lackpy-0.1.0/tests/kit/test_toolbox.py +57 -0
- lackpy-0.1.0/tests/lang/__init__.py +0 -0
- lackpy-0.1.0/tests/lang/test_grader.py +31 -0
- lackpy-0.1.0/tests/lang/test_grammar.py +46 -0
- lackpy-0.1.0/tests/lang/test_rules.py +54 -0
- lackpy-0.1.0/tests/lang/test_validator.py +180 -0
- lackpy-0.1.0/tests/run/__init__.py +0 -0
- lackpy-0.1.0/tests/run/test_runner.py +75 -0
- lackpy-0.1.0/tests/run/test_trace.py +67 -0
- lackpy-0.1.0/tests/test_cli.py +44 -0
- lackpy-0.1.0/tests/test_config.py +49 -0
- lackpy-0.1.0/tests/test_integration.py +97 -0
- 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
|
lackpy-0.1.0/.gitignore
ADDED
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.
|