local-coding-agent 1.1.0__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.
- local_coding_agent/__init__.py +314 -0
- local_coding_agent/__main__.py +5 -0
- local_coding_agent/acp_server/__init__.py +38 -0
- local_coding_agent/acp_server/_codec.py +95 -0
- local_coding_agent/acp_server/_protocol.py +13 -0
- local_coding_agent/acp_server/_server.py +663 -0
- local_coding_agent/acp_server/_session.py +24 -0
- local_coding_agent/acp_server/_tools.py +126 -0
- local_coding_agent/ast_compactor.py +130 -0
- local_coding_agent/atomizer.py +77 -0
- local_coding_agent/benchmark/__init__.py +57 -0
- local_coding_agent/benchmark/_cases.py +307 -0
- local_coding_agent/benchmark/_judge.py +175 -0
- local_coding_agent/benchmark/_oracles.py +157 -0
- local_coding_agent/benchmark/_runner.py +259 -0
- local_coding_agent/benchmark/_stats.py +22 -0
- local_coding_agent/benchmark/_summary.py +143 -0
- local_coding_agent/benchmark_oracle_worker.py +169 -0
- local_coding_agent/calibration.py +154 -0
- local_coding_agent/capability.py +450 -0
- local_coding_agent/chat_compaction.py +276 -0
- local_coding_agent/cli/__init__.py +12 -0
- local_coding_agent/cli/_dispatch.py +377 -0
- local_coding_agent/cli/_handlers.py +320 -0
- local_coding_agent/cli/_handlers2.py +725 -0
- local_coding_agent/cli/_input.py +61 -0
- local_coding_agent/cli/_main.py +159 -0
- local_coding_agent/cli/_parser.py +327 -0
- local_coding_agent/context_manager.py +155 -0
- local_coding_agent/controller/__init__.py +18 -0
- local_coding_agent/controller/_constants.py +108 -0
- local_coding_agent/controller/_controller.py +1008 -0
- local_coding_agent/controller/_post_apply.py +216 -0
- local_coding_agent/delegator.py +180 -0
- local_coding_agent/desktop/__init__.py +8 -0
- local_coding_agent/desktop/app.py +100 -0
- local_coding_agent/desktop/assets/lucide.min.js +12 -0
- local_coding_agent/desktop/assets/tailwind.css +1 -0
- local_coding_agent/desktop/client_js.py +1271 -0
- local_coding_agent/desktop/components.py +555 -0
- local_coding_agent/desktop/server/__init__.py +37 -0
- local_coding_agent/desktop/server/_handlers.py +2178 -0
- local_coding_agent/desktop/server/_models.py +204 -0
- local_coding_agent/desktop/server/_server.py +347 -0
- local_coding_agent/desktop/server/_telemetry.py +87 -0
- local_coding_agent/desktop/styles.py +76 -0
- local_coding_agent/desktop/ui.py +53 -0
- local_coding_agent/doctor.py +413 -0
- local_coding_agent/hooks/__init__.py +19 -0
- local_coding_agent/hooks/_adapters.py +458 -0
- local_coding_agent/hooks/_bridge.py +346 -0
- local_coding_agent/hooks/_decision.py +26 -0
- local_coding_agent/lsp/__init__.py +60 -0
- local_coding_agent/lsp/_client.py +322 -0
- local_coding_agent/lsp/_fallback.py +297 -0
- local_coding_agent/lsp/_manager.py +173 -0
- local_coding_agent/lsp/_mock.py +200 -0
- local_coding_agent/lsp/_types.py +398 -0
- local_coding_agent/mcp_config.py +398 -0
- local_coding_agent/mcp_server.py +377 -0
- local_coding_agent/memory.py +140 -0
- local_coding_agent/mode_router.py +200 -0
- local_coding_agent/model_scanner.py +525 -0
- local_coding_agent/monitor.py +635 -0
- local_coding_agent/observation_policy.py +184 -0
- local_coding_agent/ollama_adapter.py +736 -0
- local_coding_agent/plan_mode.py +653 -0
- local_coding_agent/prescriptions.py +278 -0
- local_coding_agent/profiles.py +239 -0
- local_coding_agent/repository_tools.py +782 -0
- local_coding_agent/ripgrep.py +326 -0
- local_coding_agent/semantic_linter.py +122 -0
- local_coding_agent/service.py +405 -0
- local_coding_agent/session_events.py +684 -0
- local_coding_agent/session_query.py +578 -0
- local_coding_agent/skill_config.py +426 -0
- local_coding_agent/smoke.py +268 -0
- local_coding_agent/speculative_racing.py +85 -0
- local_coding_agent/spill.py +258 -0
- local_coding_agent/stats.py +255 -0
- local_coding_agent/stdio.py +132 -0
- local_coding_agent/subagent.py +516 -0
- local_coding_agent/task.py +90 -0
- local_coding_agent/task_store.py +142 -0
- local_coding_agent/tasks.py +240 -0
- local_coding_agent/terminal/__init__.py +35 -0
- local_coding_agent/terminal/_manager.py +156 -0
- local_coding_agent/terminal/_process.py +148 -0
- local_coding_agent/terminal/_session.py +452 -0
- local_coding_agent/terminal/_tools.py +233 -0
- local_coding_agent/terminal/_types.py +60 -0
- local_coding_agent/tool_call_parser.py +178 -0
- local_coding_agent/validators.py +563 -0
- local_coding_agent/vram_fit.py +240 -0
- local_coding_agent/worker_pool.py +516 -0
- local_coding_agent-1.1.0.dist-info/METADATA +202 -0
- local_coding_agent-1.1.0.dist-info/RECORD +101 -0
- local_coding_agent-1.1.0.dist-info/WHEEL +5 -0
- local_coding_agent-1.1.0.dist-info/entry_points.txt +3 -0
- local_coding_agent-1.1.0.dist-info/licenses/LICENSE +21 -0
- local_coding_agent-1.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,426 @@
|
|
|
1
|
+
"""Skill configuration and installer for AI coding agents.
|
|
2
|
+
|
|
3
|
+
Integrates SKILL.md into Codex, Antigravity, Claude, and workspace skill directories.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import os
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
_EMBEDDED_SKILL_MD = """---
|
|
14
|
+
name: local-coding-agent
|
|
15
|
+
description: Offload and delegate atomic coding tasks, bug fixes, refactoring, and unit test implementations to local Ollama LLMs via MCP (delegate_code, apply_proposal) or direct CLI.
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
# Local Coding Agent — AI Agent Delegation Skill
|
|
19
|
+
|
|
20
|
+
## Overview
|
|
21
|
+
|
|
22
|
+
The `local-coding-agent` is a bounded, proposal-only controller designed to offload small, deterministic coding tasks to local LLMs (such as Qwen 2.5 Coder, Qwen3, Devstral, and Gemma) running via Ollama or OpenAI-compatible local endpoints.
|
|
23
|
+
|
|
24
|
+
### Core Security & Architecture Invariants
|
|
25
|
+
- **Proposal-Only by Default**: The local model never directly writes to disk or executes arbitrary shell commands. It only generates structured diffs (`patch`) or search/replace blocks (`edits`).
|
|
26
|
+
- **External Test Verification**: Test results are never self-reported by the model; they are executed and verified strictly by the external runner.
|
|
27
|
+
- **Bounded Scope**: Context and files are strictly allowlisted. Tasks exceeding budget limits must be decomposed into atomic child subtasks.
|
|
28
|
+
- **Mediated Apply with Auto-Rollback**: Applying patches (`apply_proposal` or `--apply`) verifies the diff, applies it, re-runs all targeted test checks, and automatically rolls back if any test fails.
|
|
29
|
+
- **100% CLI Parity**: Every feature available via MCP is equally accessible via CLI subcommands for non-MCP agents and terminal workflows.
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
## Delegation Decision Matrix
|
|
34
|
+
|
|
35
|
+
Use this matrix to determine whether to delegate a task to the local model:
|
|
36
|
+
|
|
37
|
+
| Task Type | Delegate? | Reason / Action |
|
|
38
|
+
|---|---|---|
|
|
39
|
+
| Single-function bug fix | **YES** | Perfect fit. Provide targeted test check and allowlist 1 file. |
|
|
40
|
+
| Writing unit tests | **YES** | Provide the target implementation file + test file in `files`. |
|
|
41
|
+
| Localized refactor | **YES** | Clear acceptance criteria and targeted test runner command. |
|
|
42
|
+
| Edge-case handling | **YES** | Explicit constraints and expected inputs/outputs in `context`. |
|
|
43
|
+
| Cross-repo architecture | **NO** | Solve yourself. Too broad for bounded local model context. |
|
|
44
|
+
| Unknown / exploratory debugging | **NO** | First inspect and isolate the issue, then delegate the isolated fix. |
|
|
45
|
+
| Task with no test / verification check | **NO** | Provide a check command or solve directly. |
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
## MCP Tools Reference
|
|
50
|
+
|
|
51
|
+
When the `local-coding-agent` MCP server is active in your environment, two primary tools are available:
|
|
52
|
+
|
|
53
|
+
### 1. `delegate_code`
|
|
54
|
+
Delegates one atomic, proposal-only coding task to a local Ollama model.
|
|
55
|
+
|
|
56
|
+
#### Parameters:
|
|
57
|
+
- `request_id` *(string, required)*: A unique caller-generated idempotency key (e.g. `task-fix-auth-01`).
|
|
58
|
+
- `workspace_ref` *(string, required)*: The registered workspace identifier (usually `"workspace"`).
|
|
59
|
+
- `model_profile` *(string, required)*: Named profile (e.g. `"qwen2.5-coder"`, `"qwen3-8b-q6k"`, `"devstral-small-2-24b"`).
|
|
60
|
+
- `task` *(object, required)*: The structured task envelope:
|
|
61
|
+
- `id` *(string)*: Short descriptive identifier (e.g. `"fix-null-dereference"`).
|
|
62
|
+
- `goal` *(string)*: 1 concise sentence describing the goal.
|
|
63
|
+
- `files` *(array of strings)*: List of allowlisted relative file paths (1 to 5 files, ideally 1-2).
|
|
64
|
+
- `context` *(string)*: Compact relevant context (types, function signatures, error traces). Do **not** dump entire irrelevant files.
|
|
65
|
+
- `constraints` *(array of strings)*: Guardrails (e.g. `["do not add external dependencies", "preserve public signature"]`).
|
|
66
|
+
- `checks` *(array of strings)*: Targeted test commands (e.g. `["pytest tests/test_auth.py -k test_null"]`). Non-empty patches require at least 1 check.
|
|
67
|
+
- `acceptance` *(array of strings)*: Bullet points describing acceptance criteria.
|
|
68
|
+
|
|
69
|
+
#### Example MCP Invocation:
|
|
70
|
+
```json
|
|
71
|
+
{
|
|
72
|
+
"request_id": "req-20260818-unique-01",
|
|
73
|
+
"workspace_ref": "workspace",
|
|
74
|
+
"model_profile": "qwen2.5-coder",
|
|
75
|
+
"task": {
|
|
76
|
+
"id": "unique-preserve-order",
|
|
77
|
+
"goal": "Remove sorting from unique function and preserve first-seen insertion order",
|
|
78
|
+
"files": ["src/unique.py"],
|
|
79
|
+
"context": "def unique(items: list[Any]) -> list[Any]:\\n # currently does sorted(set(items))",
|
|
80
|
+
"constraints": [
|
|
81
|
+
"preserve public signature of unique()",
|
|
82
|
+
"do not add external dependencies"
|
|
83
|
+
],
|
|
84
|
+
"checks": [
|
|
85
|
+
"pytest tests/test_unique.py"
|
|
86
|
+
],
|
|
87
|
+
"acceptance": [
|
|
88
|
+
"diff modifies only src/unique.py",
|
|
89
|
+
"sorting logic is replaced with order-preserving deduplication",
|
|
90
|
+
"targeted test passes"
|
|
91
|
+
]
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
### 2. `apply_proposal`
|
|
97
|
+
Safely applies a previously accepted proposal to the workspace after verifying confirmation.
|
|
98
|
+
|
|
99
|
+
#### Parameters:
|
|
100
|
+
- `request_id` *(string, required)*: The request ID of the accepted proposal.
|
|
101
|
+
- `workspace_ref` *(string, required)*: The target workspace.
|
|
102
|
+
- `confirmation` *(object)*: Standard MCP elicitation confirmation containing `confirm: true`, `proposal_id`, `workspace_ref`, and `proposal_digest`.
|
|
103
|
+
|
|
104
|
+
---
|
|
105
|
+
|
|
106
|
+
## Interpreting Result Envelopes
|
|
107
|
+
|
|
108
|
+
The return value is a controller-owned JSON object:
|
|
109
|
+
|
|
110
|
+
```json
|
|
111
|
+
{
|
|
112
|
+
"status": "accepted",
|
|
113
|
+
"summary": "Replaced sorted set with dict.fromkeys to preserve insertion order",
|
|
114
|
+
"patch": "--- a/src/unique.py\\n+++ b/src/unique.py\\n@@ -5,2 +5,2 @@\\n- return sorted(list(set(items)))\\n+ return list(dict.fromkeys(items))\\n",
|
|
115
|
+
"checks": [
|
|
116
|
+
{
|
|
117
|
+
"command": "pytest tests/test_unique.py",
|
|
118
|
+
"passed": true,
|
|
119
|
+
"evidence": "1 passed in 0.04s"
|
|
120
|
+
}
|
|
121
|
+
],
|
|
122
|
+
"risks": [],
|
|
123
|
+
"validation": {
|
|
124
|
+
"syntax_valid": true,
|
|
125
|
+
"allowlist_clean": true
|
|
126
|
+
},
|
|
127
|
+
"applied": false
|
|
128
|
+
}
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
### Result Statuses:
|
|
132
|
+
- **`accepted`**: Valid diff generated, all targeted checks executed and passed.
|
|
133
|
+
- **`rejected`**: The proposal failed syntax validation, touched non-allowlisted files, or failed targeted test checks. Check `error` and `risks`.
|
|
134
|
+
- **`needs_context`**: The local model requested clarification. Inspect the question, refine `context`, and retry.
|
|
135
|
+
- **`failed`**: Controller policy violation, timeout, or queue overload.
|
|
136
|
+
|
|
137
|
+
---
|
|
138
|
+
|
|
139
|
+
## Model Profile Selection & Multi-Runtime Support
|
|
140
|
+
|
|
141
|
+
The controller is runtime-agnostic and natively supports two local inference backends:
|
|
142
|
+
1. **Ollama Runtime** (`provider="ollama"`, default: `http://127.0.0.1:11434`): Standard native Ollama API with VRAM introspection and dynamic model loading/unloading.
|
|
143
|
+
2. **llama.cpp / llama-server & OpenAI-Compatible** (`provider="openai"`, default: `http://127.0.0.1:8080`): Standalone `llama-server`, vLLM, or LM Studio endpoints providing `/v1/chat/completions` with exact timing measurements (`prompt_ms`, `predicted_ms`) and tool-calling support.
|
|
144
|
+
|
|
145
|
+
### Model Tiers:
|
|
146
|
+
|
|
147
|
+
| Model Tier / Class | Memory / VRAM | Example Profiles / Runtimes | When to Choose |
|
|
148
|
+
|---|---|---|---|
|
|
149
|
+
| **Ultra-Lightweight (1B-3B)** | 2-4 GB VRAM / CPU | `qwen2.5-1.5b`, `gemma4-e2b-q4`, `ling-3.0-tiny-q6k` (llama-server) | Trivial fixes, single-line edits, docstrings, type annotations, ultra-fast response (<2s). |
|
|
150
|
+
| **Standard Workhorse (7B-14B)** | 6-12 GB VRAM | `qwen2.5-coder`, `qwen3-8b-q6k`, `qwen2.5-coder-7b-q4` | **Default choice.** Standard functions, bug fixes, localized refactoring, unit test generation. |
|
|
151
|
+
| **Advanced Reasoning (24B-32B)** | 16-24 GB VRAM | `devstral-small-2-24b`, `qwen3.8-27b-q4`, `qwen3-coder-30b` | Complex algorithms, subtle edge cases, multi-step logic, difficult refactors. |
|
|
152
|
+
| **llama-server / Custom OpenAI** | Any GGUF / GPU | `ling-3.0-tiny-q6k` (llama-server:8080), custom vLLM | Standalone `llama-server` instances or custom OpenAI-compatible proxies. |
|
|
153
|
+
|
|
154
|
+
### Dynamic Discovery Rule
|
|
155
|
+
Never guess or hardcode installed models. Determine host capabilities dynamically:
|
|
156
|
+
- **Discover active & installed profiles**: Run `local-agent profiles list --check-ollama --json` or `local-agent doctor --json`.
|
|
157
|
+
- **Targeting llama-server**: Specify `--endpoint http://127.0.0.1:8080` or choose an OpenAI-provider profile.
|
|
158
|
+
- **Default Profile**: If unspecified, use the default configured profile (`qwen2.5-coder` or the MCP server default).
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
---
|
|
164
|
+
|
|
165
|
+
## Error Handling & Task Decomposition
|
|
166
|
+
|
|
167
|
+
### 1. Preflight Budget Violations (`too_many_files`, `context_too_large`)
|
|
168
|
+
If a task touches more than 5 files or context exceeds 32KB, decompose the task into child subtasks using the decomposition tool:
|
|
169
|
+
```bash
|
|
170
|
+
local-agent decompose --task task.json --json
|
|
171
|
+
```
|
|
172
|
+
Or split the task into independent child envelopes per file.
|
|
173
|
+
|
|
174
|
+
### 2. Pinpointed Prescriptions
|
|
175
|
+
When a small local model produces a syntax or diff formatting error, the Pinpointed Prescriptions Engine returns a laser-focused deterministic hint (e.g. exact SEARCH/REPLACE alignment rules). Pass this hint directly in `constraints` on retry.
|
|
176
|
+
|
|
177
|
+
---
|
|
178
|
+
|
|
179
|
+
## CLI Console Control (Fool-Proof Fallback for Any Agent / Shell)
|
|
180
|
+
|
|
181
|
+
For AI agents operating in shell environments without MCP tools, or for direct script automation, every capability is directly available via `local-agent` / `python -m local_coding_agent`:
|
|
182
|
+
|
|
183
|
+
### Delegate a Task (Single or Speculative Multi-Draft Racing):
|
|
184
|
+
```bash
|
|
185
|
+
local-agent delegate --task '{"id":"fix-1","goal":"Fix bug","files":["src/foo.py"],"checks":["pytest tests/test_foo.py"]}' --json
|
|
186
|
+
# Or run speculative racing between 2 drafts:
|
|
187
|
+
local-agent delegate --task task.json --speculative-drafts 2 --json
|
|
188
|
+
# Or target ANY installed model tag without registering a profile (--model override):
|
|
189
|
+
local-agent delegate --task task.json --model qwen2.5-coder:7b --json
|
|
190
|
+
```
|
|
191
|
+
Accepted/candidate patches are also persisted to `.local-run/proposals/<task_id>.patch`
|
|
192
|
+
and referenced as `patch_file` in the JSON output. Environment defaults: `LCA_PROFILE`
|
|
193
|
+
(default `qwen2.5-coder`), `LCA_WORKSPACE`.
|
|
194
|
+
|
|
195
|
+
### Apply Patch Directly:
|
|
196
|
+
```bash
|
|
197
|
+
local-agent delegate --task task.json --apply --json
|
|
198
|
+
# Or apply a raw patch with verification (`--file` declares the scope and is
|
|
199
|
+
# repeatable; the patch must touch only those files, and at least one `--check`
|
|
200
|
+
# is mandatory — apply without a check is refused):
|
|
201
|
+
local-agent apply --patch-file changes.diff --workspace . --file src/foo.py --check "pytest tests/" --json
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
### Fast Semantic Linter Pre-Gate:
|
|
205
|
+
```bash
|
|
206
|
+
local-agent lint-patch --patch-file changes.diff --workspace . --json
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
### AST-Guided Skeletonization:
|
|
210
|
+
```bash
|
|
211
|
+
local-agent skeletonize src/large_module.py --symbol target_function --json
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
### Decompose a Broad Task:
|
|
215
|
+
```bash
|
|
216
|
+
local-agent decompose --task-file wide_task.json --strategy by_files --json
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
### Manage Profiles:
|
|
220
|
+
```bash
|
|
221
|
+
local-agent profiles list --json
|
|
222
|
+
local-agent profiles get qwen2.5-coder --json
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
### Manage Ollama Memory & VRAM:
|
|
226
|
+
```bash
|
|
227
|
+
local-agent memory status --json
|
|
228
|
+
local-agent memory unload qwen2.5-coder:latest --json
|
|
229
|
+
local-agent memory enforce --limit 12884901888 --keep qwen3-8b-q6k:latest --json
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
### Check System & Remediate Environment:
|
|
233
|
+
```bash
|
|
234
|
+
local-agent doctor --json
|
|
235
|
+
# Or auto-remediate missing MCP configs, Agent Skills, and setup:
|
|
236
|
+
local-agent doctor --fix
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
### Launch Desktop AI Coding Harness (R23 Cockpit):
|
|
240
|
+
```bash
|
|
241
|
+
local-agent desktop --port 8767
|
|
242
|
+
# Or force open in system browser:
|
|
243
|
+
local-agent desktop --browser
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
### Tool Output Spill Store & Fast Ripgrep (R24):
|
|
247
|
+
```bash
|
|
248
|
+
# Read or paginate oversized spill artifact:
|
|
249
|
+
local-agent spill-read locator:spill:session_123/456_output.txt --offset 0 --limit 100 --json
|
|
250
|
+
|
|
251
|
+
# Fast ripgrep search across workspace:
|
|
252
|
+
local-agent grep "def my_func" src/*.py --json
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
### LSP Code Intelligence & Navigation (R25):
|
|
256
|
+
```bash
|
|
257
|
+
# Extract document symbols (classes, functions, methods):
|
|
258
|
+
local-agent lsp --operation symbols --file src/service.py --json
|
|
259
|
+
|
|
260
|
+
# Jump to symbol definition:
|
|
261
|
+
local-agent lsp --operation definition --file src/app.py --line 42 --char 15 --json
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
### Interactive Chat with Mode Routing (R31):
|
|
265
|
+
```bash
|
|
266
|
+
# Single-shot classified chat. Mode defaults to hybrid (auto-router to chat/build/plan):
|
|
267
|
+
local-agent chat "fix off-by-one in sliding window" --json
|
|
268
|
+
# Force a specific mode:
|
|
269
|
+
local-agent chat "refactor calculate_total to integer cents" --mode build --json
|
|
270
|
+
local-agent chat "explain what main.py does" --mode chat --json
|
|
271
|
+
local-agent chat "plan the migration to the new API" --mode plan --json
|
|
272
|
+
```
|
|
273
|
+
The hybrid mode routes each prompt to `chat` (plain completion), `build` (Controller
|
|
274
|
+
tool-loop → patch + external test evidence), or `plan` (read-only exploration →
|
|
275
|
+
`PlanArtifact` with goal/steps/risks/files_to_modify). A small local model
|
|
276
|
+
(`qwen2.5-1.5b` or `ling-3.0-tiny-q6k`) is consulted in an isolated context every
|
|
277
|
+
N requests to auto-select the mode; on failure it falls back to deterministic
|
|
278
|
+
heuristics. The desktop harness (`local-agent desktop`) exposes the same four
|
|
279
|
+
modes via the Chat/Build/Plan/Auto selector.
|
|
280
|
+
|
|
281
|
+
### Multi-Turn Chat REPL & Persistent Sessions:
|
|
282
|
+
```bash
|
|
283
|
+
# Interactive multi-turn REPL with persistent session history:
|
|
284
|
+
local-agent chat --repl
|
|
285
|
+
# Create/resume a named session:
|
|
286
|
+
local-agent chat --repl --session-id my-session
|
|
287
|
+
# List persisted sessions or show a full event transcript:
|
|
288
|
+
local-agent sessions list
|
|
289
|
+
local-agent sessions show my-session
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
### Install / Export Agent Skill:
|
|
293
|
+
```bash
|
|
294
|
+
local-agent init-skill --write
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
### Wire Cloud Agents to Local Models (OpenAI-compatible /v1):
|
|
298
|
+
The Desktop Harness exposes an OpenAI-compatible router at `/v1` that proxies
|
|
299
|
+
`/v1/models` and `/v1/chat/completions` to whichever local backend serves the
|
|
300
|
+
requested model (resident llama-server first, then Ollama). Point any
|
|
301
|
+
OpenAI-compatible cloud agent at your local models:
|
|
302
|
+
```bash
|
|
303
|
+
# Preview the Codex provider config (writes ~/.codex/config.toml with --write):
|
|
304
|
+
local-agent init-agent --agent codex --model qwen2.5-coder:latest
|
|
305
|
+
# Generic OpenAI-compatible clients: prints OPENAI_BASE_URL / OPENAI_API_KEY env:
|
|
306
|
+
local-agent init-agent --agent generic --json
|
|
307
|
+
# Launch Codex against local models afterwards:
|
|
308
|
+
codex -c model_provider=local_agent -m "qwen2.5-coder:latest"
|
|
309
|
+
```
|
|
310
|
+
Prerequisite: `local-agent desktop` must be running and a model loaded in its
|
|
311
|
+
Local Inference Servers panel. Requests for models no backend serves receive a
|
|
312
|
+
prescriptive 404 listing available model ids.
|
|
313
|
+
"""
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
def get_skill_content(workspace: str | Path = ".") -> str:
|
|
318
|
+
"""Retrieve skill markdown content from file or embedded fallback."""
|
|
319
|
+
candidate_paths = [
|
|
320
|
+
Path(workspace) / "skills" / "local-coding-agent" / "SKILL.md",
|
|
321
|
+
Path(__file__).resolve().parent.parent / "skills" / "local-coding-agent" / "SKILL.md",
|
|
322
|
+
]
|
|
323
|
+
for path in candidate_paths:
|
|
324
|
+
if path.is_file():
|
|
325
|
+
try:
|
|
326
|
+
return path.read_text(encoding="utf-8")
|
|
327
|
+
except OSError:
|
|
328
|
+
pass
|
|
329
|
+
return _EMBEDDED_SKILL_MD.strip() + "\n"
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
def _get_skill_target_path(client: str, workspace: str | Path = ".") -> Path:
|
|
333
|
+
home = Path.home()
|
|
334
|
+
c = client.lower().strip()
|
|
335
|
+
if c in ("codex", "chatgpt"):
|
|
336
|
+
return home / ".codex" / "skills" / "local-coding-agent" / "SKILL.md"
|
|
337
|
+
if c in ("antigravity", "agy", "gemini"):
|
|
338
|
+
return home / ".gemini" / "antigravity" / "skills" / "local-coding-agent" / "SKILL.md"
|
|
339
|
+
if c == "claude":
|
|
340
|
+
return home / ".claude" / "skills" / "local-coding-agent" / "SKILL.md"
|
|
341
|
+
if c == "workspace":
|
|
342
|
+
return Path(workspace).resolve() / "skills" / "local-coding-agent" / "SKILL.md"
|
|
343
|
+
return home / ".codex" / "skills" / "local-coding-agent" / "SKILL.md"
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
def _detect_installed_agent_dirs(workspace: str | Path = ".") -> list[tuple[str, Path]]:
|
|
347
|
+
home = Path.home()
|
|
348
|
+
detected = []
|
|
349
|
+
|
|
350
|
+
# Codex
|
|
351
|
+
codex_dir = home / ".codex"
|
|
352
|
+
if codex_dir.is_dir() or (home / ".codex" / "skills").is_dir():
|
|
353
|
+
detected.append(("codex", codex_dir / "skills" / "local-coding-agent" / "SKILL.md"))
|
|
354
|
+
|
|
355
|
+
# Antigravity
|
|
356
|
+
agy_dir = home / ".gemini" / "antigravity"
|
|
357
|
+
if agy_dir.is_dir() or (agy_dir / "skills").is_dir():
|
|
358
|
+
detected.append(("antigravity", agy_dir / "skills" / "local-coding-agent" / "SKILL.md"))
|
|
359
|
+
|
|
360
|
+
# Claude
|
|
361
|
+
claude_dir = home / ".claude"
|
|
362
|
+
if claude_dir.is_dir():
|
|
363
|
+
detected.append(("claude", claude_dir / "skills" / "local-coding-agent" / "SKILL.md"))
|
|
364
|
+
|
|
365
|
+
# Always include workspace target
|
|
366
|
+
ws_target = Path(workspace).resolve() / "skills" / "local-coding-agent" / "SKILL.md"
|
|
367
|
+
detected.append(("workspace", ws_target))
|
|
368
|
+
|
|
369
|
+
return detected
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
def integrate_skill_config(
|
|
373
|
+
client: str = "auto",
|
|
374
|
+
workspace: str | Path = ".",
|
|
375
|
+
target_path: str | Path | None = None,
|
|
376
|
+
dry_run: bool = False,
|
|
377
|
+
print_content: bool = False,
|
|
378
|
+
) -> dict[str, Any]:
|
|
379
|
+
"""Export or install the Agent Skill to agent skill directories."""
|
|
380
|
+
content = get_skill_content(workspace)
|
|
381
|
+
|
|
382
|
+
if print_content:
|
|
383
|
+
return {"action": "print", "content": content}
|
|
384
|
+
|
|
385
|
+
client_norm = client.lower().strip()
|
|
386
|
+
|
|
387
|
+
if target_path:
|
|
388
|
+
dest_path = Path(target_path).resolve()
|
|
389
|
+
if dest_path.is_dir():
|
|
390
|
+
dest_path = dest_path / "SKILL.md"
|
|
391
|
+
targets = [(client_norm or "custom", dest_path)]
|
|
392
|
+
elif client_norm in ("auto", "all"):
|
|
393
|
+
targets = _detect_installed_agent_dirs(workspace)
|
|
394
|
+
else:
|
|
395
|
+
targets = [(client_norm, _get_skill_target_path(client_norm, workspace))]
|
|
396
|
+
|
|
397
|
+
results = []
|
|
398
|
+
for client_name, path in targets:
|
|
399
|
+
written = False
|
|
400
|
+
error_message = None
|
|
401
|
+
if not dry_run:
|
|
402
|
+
try:
|
|
403
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
404
|
+
path.write_text(content, encoding="utf-8")
|
|
405
|
+
written = True
|
|
406
|
+
except Exception as error:
|
|
407
|
+
error_message = str(error)
|
|
408
|
+
result = {
|
|
409
|
+
"client": client_name,
|
|
410
|
+
"path": str(path),
|
|
411
|
+
"written": written,
|
|
412
|
+
"status": "installed" if written else ("dry_run_preview" if dry_run else "failed"),
|
|
413
|
+
}
|
|
414
|
+
if error_message is not None:
|
|
415
|
+
result["error"] = error_message
|
|
416
|
+
results.append(result)
|
|
417
|
+
|
|
418
|
+
if target_path or (len(results) == 1 and client_norm not in ("auto", "all")):
|
|
419
|
+
return results[0]
|
|
420
|
+
|
|
421
|
+
return {
|
|
422
|
+
"action": "install_skill",
|
|
423
|
+
"results": results,
|
|
424
|
+
"dry_run": dry_run,
|
|
425
|
+
}
|
|
426
|
+
|
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
"""Interactive smoke test and end-to-end task runner."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
import sys
|
|
8
|
+
import tempfile
|
|
9
|
+
import time
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
from .controller import Controller
|
|
14
|
+
from .ollama_adapter import build_client
|
|
15
|
+
from .profiles import ModelProfile, get_profile
|
|
16
|
+
from .task import TaskEnvelope
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class MockSmokeOllamaClient:
|
|
20
|
+
"""Mock client providing a scripted tool-loop response for isolated smoke tests."""
|
|
21
|
+
|
|
22
|
+
def __init__(self, profile: ModelProfile, test_command: str = "") -> None:
|
|
23
|
+
self.profile = profile
|
|
24
|
+
self.test_command = test_command
|
|
25
|
+
self.turn = 0
|
|
26
|
+
self.endpoint = profile.endpoint
|
|
27
|
+
|
|
28
|
+
def available_models(self) -> dict[str, Any]:
|
|
29
|
+
return {"models": [{"name": self.profile.model}]}
|
|
30
|
+
|
|
31
|
+
def chat(self, messages: list[dict[str, Any]], tools: list[dict[str, Any]] | None = None, **kwargs: Any) -> dict[str, Any]:
|
|
32
|
+
self.turn += 1
|
|
33
|
+
if self.turn == 1:
|
|
34
|
+
return {
|
|
35
|
+
"message": {
|
|
36
|
+
"role": "assistant",
|
|
37
|
+
"content": "",
|
|
38
|
+
"tool_calls": [
|
|
39
|
+
{
|
|
40
|
+
"id": "call-read-1",
|
|
41
|
+
"function": {
|
|
42
|
+
"name": "read_file",
|
|
43
|
+
"arguments": {"path": "calculator.py"},
|
|
44
|
+
},
|
|
45
|
+
}
|
|
46
|
+
],
|
|
47
|
+
},
|
|
48
|
+
"eval_count": 45,
|
|
49
|
+
"eval_duration": 450_000_000,
|
|
50
|
+
"prompt_eval_count": 120,
|
|
51
|
+
"prompt_eval_duration": 100_000_000,
|
|
52
|
+
}
|
|
53
|
+
elif self.turn == 2:
|
|
54
|
+
return {
|
|
55
|
+
"message": {
|
|
56
|
+
"role": "assistant",
|
|
57
|
+
"content": "",
|
|
58
|
+
"tool_calls": [
|
|
59
|
+
{
|
|
60
|
+
"id": "call-patch-1",
|
|
61
|
+
"function": {
|
|
62
|
+
"name": "propose_patch",
|
|
63
|
+
"arguments": {
|
|
64
|
+
"edits": [
|
|
65
|
+
{
|
|
66
|
+
"file": "calculator.py",
|
|
67
|
+
"search": " return a - b",
|
|
68
|
+
"replace": " return a + b",
|
|
69
|
+
}
|
|
70
|
+
]
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
}
|
|
74
|
+
],
|
|
75
|
+
},
|
|
76
|
+
"eval_count": 60,
|
|
77
|
+
"eval_duration": 600_000_000,
|
|
78
|
+
"prompt_eval_count": 200,
|
|
79
|
+
"prompt_eval_duration": 150_000_000,
|
|
80
|
+
}
|
|
81
|
+
else:
|
|
82
|
+
return {
|
|
83
|
+
"message": {
|
|
84
|
+
"role": "assistant",
|
|
85
|
+
"content": json.dumps({
|
|
86
|
+
"status": "candidate",
|
|
87
|
+
"summary": "Fixed subtraction to addition in calculator.py",
|
|
88
|
+
"edits": [
|
|
89
|
+
{
|
|
90
|
+
"file": "calculator.py",
|
|
91
|
+
"search": " return a - b",
|
|
92
|
+
"replace": " return a + b",
|
|
93
|
+
}
|
|
94
|
+
],
|
|
95
|
+
"checks": [],
|
|
96
|
+
"risks": [],
|
|
97
|
+
}),
|
|
98
|
+
},
|
|
99
|
+
"eval_count": 40,
|
|
100
|
+
"eval_duration": 400_000_000,
|
|
101
|
+
"prompt_eval_count": 260,
|
|
102
|
+
"prompt_eval_duration": 180_000_000,
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def run_smoke_test(
|
|
108
|
+
profile_name: str = "qwen2.5-coder",
|
|
109
|
+
workspace_dir: str | Path | None = None,
|
|
110
|
+
use_mock: bool = False,
|
|
111
|
+
fallback_to_mock: bool = False,
|
|
112
|
+
verbose: bool = True,
|
|
113
|
+
) -> dict[str, Any]:
|
|
114
|
+
steps = []
|
|
115
|
+
start_total = time.perf_counter()
|
|
116
|
+
|
|
117
|
+
if verbose:
|
|
118
|
+
print("=" * 60)
|
|
119
|
+
print(" Local Coding Agent — End-to-End Smoke Test")
|
|
120
|
+
print("=" * 60)
|
|
121
|
+
|
|
122
|
+
# 1. Setup isolated workspace
|
|
123
|
+
is_temp = workspace_dir is None
|
|
124
|
+
temp_dir_obj = tempfile.TemporaryDirectory() if is_temp else None
|
|
125
|
+
ws_path = Path(temp_dir_obj.name if temp_dir_obj else workspace_dir).resolve()
|
|
126
|
+
|
|
127
|
+
try:
|
|
128
|
+
# Create calculator.py
|
|
129
|
+
target_file = ws_path / "calculator.py"
|
|
130
|
+
target_file.write_text("def add(a: int, b: int) -> int:\n return a - b\n", encoding="utf-8")
|
|
131
|
+
|
|
132
|
+
steps.append({"step": "workspace_prep", "status": "ok", "message": f"Workspace created at {ws_path}"})
|
|
133
|
+
if verbose:
|
|
134
|
+
print("[OK] Step 1: Isolated test workspace prepared.")
|
|
135
|
+
|
|
136
|
+
# 2. Build task envelope
|
|
137
|
+
task = TaskEnvelope(
|
|
138
|
+
id="smoke-fix-add",
|
|
139
|
+
goal="Fix addition bug in calculator.py so that add(a, b) returns a + b",
|
|
140
|
+
files=["calculator.py"],
|
|
141
|
+
context="The function add currently subtracts instead of adding.",
|
|
142
|
+
constraints=["Do not change the function signature"],
|
|
143
|
+
checks=[],
|
|
144
|
+
acceptance=["calculator.py fixed"],
|
|
145
|
+
)
|
|
146
|
+
steps.append({"step": "task_envelope", "status": "ok", "message": "TaskEnvelope ready"})
|
|
147
|
+
if verbose:
|
|
148
|
+
print("[OK] Step 2: Atomic TaskEnvelope formulated.")
|
|
149
|
+
|
|
150
|
+
# 3. Client selection
|
|
151
|
+
profile = get_profile(profile_name)
|
|
152
|
+
mock_fallback = False
|
|
153
|
+
backend_verified = False
|
|
154
|
+
|
|
155
|
+
if use_mock:
|
|
156
|
+
client: Any = MockSmokeOllamaClient(profile)
|
|
157
|
+
if verbose:
|
|
158
|
+
print("[INFO] Step 3: Using scripted mock model runner.")
|
|
159
|
+
else:
|
|
160
|
+
try:
|
|
161
|
+
real_client = build_client(profile)
|
|
162
|
+
real_client.available_models()
|
|
163
|
+
client = real_client
|
|
164
|
+
backend_verified = True
|
|
165
|
+
if verbose:
|
|
166
|
+
print(f"[OK] Step 3: Connected to live Ollama ({profile.model}).")
|
|
167
|
+
except Exception as exc:
|
|
168
|
+
if fallback_to_mock:
|
|
169
|
+
mock_fallback = True
|
|
170
|
+
client = MockSmokeOllamaClient(profile)
|
|
171
|
+
if verbose:
|
|
172
|
+
print(f"[WARN] Step 3: Live Ollama unavailable ({exc}). Using mock fallback.")
|
|
173
|
+
else:
|
|
174
|
+
steps.append({
|
|
175
|
+
"step": "backend_readiness",
|
|
176
|
+
"status": "fail",
|
|
177
|
+
"message": f"Live backend unavailable: {exc}",
|
|
178
|
+
})
|
|
179
|
+
return {
|
|
180
|
+
"success": False,
|
|
181
|
+
"status": "backend_unavailable",
|
|
182
|
+
"backend_verified": False,
|
|
183
|
+
"synthetic": False,
|
|
184
|
+
"tps": None,
|
|
185
|
+
"duration_seconds": round(time.perf_counter() - start_total, 2),
|
|
186
|
+
"mock_fallback": False,
|
|
187
|
+
"error": str(exc),
|
|
188
|
+
"steps": steps,
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
# 4. Controller execution
|
|
193
|
+
start_exec = time.perf_counter()
|
|
194
|
+
controller = Controller(client, ws_path, max_turns=5)
|
|
195
|
+
run_res = controller.run(task, apply=False)
|
|
196
|
+
exec_duration = round(time.perf_counter() - start_exec, 2)
|
|
197
|
+
|
|
198
|
+
status = run_res.get("status")
|
|
199
|
+
success = status == "accepted"
|
|
200
|
+
steps.append({
|
|
201
|
+
"step": "execution",
|
|
202
|
+
"status": "ok" if success else "fail",
|
|
203
|
+
"message": f"Controller status: {status} in {exec_duration}s",
|
|
204
|
+
})
|
|
205
|
+
|
|
206
|
+
if verbose:
|
|
207
|
+
tag = "[OK]" if success else "[FAIL]"
|
|
208
|
+
print(f"{tag} Step 4: Delegation completed. Status: {status} ({exec_duration}s)")
|
|
209
|
+
|
|
210
|
+
# 5. TPS Calculation & validation summary
|
|
211
|
+
total_eval_tokens = 0
|
|
212
|
+
total_eval_duration_ns = 0
|
|
213
|
+
for ev in run_res.get("audit", []):
|
|
214
|
+
if ev.get("event") == "model_response":
|
|
215
|
+
dur = ev.get("eval_duration_ns") or 0
|
|
216
|
+
cnt = ev.get("eval_tokens") or 0
|
|
217
|
+
total_eval_tokens += cnt
|
|
218
|
+
total_eval_duration_ns += dur
|
|
219
|
+
|
|
220
|
+
tps: float | None = (
|
|
221
|
+
round(total_eval_tokens / (total_eval_duration_ns / 1e9), 1)
|
|
222
|
+
if total_eval_duration_ns > 0
|
|
223
|
+
else None
|
|
224
|
+
)
|
|
225
|
+
tps_label = f" ({tps} tokens/sec)" if tps is not None else ""
|
|
226
|
+
|
|
227
|
+
if success:
|
|
228
|
+
steps.append({
|
|
229
|
+
"step": "validation",
|
|
230
|
+
"status": "ok",
|
|
231
|
+
"message": f"Patch candidate validated{tps_label}",
|
|
232
|
+
})
|
|
233
|
+
if verbose:
|
|
234
|
+
print(f"[OK] Step 5: Patch candidate validated. Generation speed:{tps_label}")
|
|
235
|
+
else:
|
|
236
|
+
steps.append({
|
|
237
|
+
"step": "validation",
|
|
238
|
+
"status": "fail",
|
|
239
|
+
"message": f"Patch validation skipped or failed (Status: {status})",
|
|
240
|
+
})
|
|
241
|
+
if verbose:
|
|
242
|
+
print(f"[FAIL] Step 5: Patch validation skipped or failed (Status: {status}).")
|
|
243
|
+
print("")
|
|
244
|
+
print("-" * 60)
|
|
245
|
+
print(" Smoke Test Summary")
|
|
246
|
+
print("-" * 60)
|
|
247
|
+
print(f" Result Status : {status.upper() if status else 'UNKNOWN'}")
|
|
248
|
+
print(f" Total Time : {round(time.perf_counter() - start_total, 2)}s")
|
|
249
|
+
if tps is not None:
|
|
250
|
+
print(f" Eval TPS : {tps} tok/s")
|
|
251
|
+
print(f" Monitor UI : http://127.0.0.1:8765/dashboard")
|
|
252
|
+
print("=" * 60)
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
return {
|
|
256
|
+
"success": success,
|
|
257
|
+
"status": status,
|
|
258
|
+
"backend_verified": backend_verified,
|
|
259
|
+
"synthetic": use_mock or mock_fallback,
|
|
260
|
+
"tps": tps,
|
|
261
|
+
"duration_seconds": round(time.perf_counter() - start_total, 2),
|
|
262
|
+
"mock_fallback": mock_fallback,
|
|
263
|
+
"result": run_res,
|
|
264
|
+
"steps": steps,
|
|
265
|
+
}
|
|
266
|
+
finally:
|
|
267
|
+
if temp_dir_obj:
|
|
268
|
+
temp_dir_obj.cleanup()
|