localcode 0.4.0__py3-none-macosx_13_0_arm64.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.
- localcode/__init__.py +11 -0
- localcode/__main__.py +5 -0
- localcode/_subproc_env.py +54 -0
- localcode/agent/__init__.py +177 -0
- localcode/agent/app_tasks.py +157 -0
- localcode/agent/constants.py +275 -0
- localcode/agent/context.py +1152 -0
- localcode/agent/goal.py +244 -0
- localcode/agent/helpers.py +1036 -0
- localcode/agent/hollow_module.py +230 -0
- localcode/agent/hooks.py +414 -0
- localcode/agent/loop.py +2855 -0
- localcode/agent/plateau.py +297 -0
- localcode/agent/project_check_gate.py +91 -0
- localcode/agent/prompt_context.py +264 -0
- localcode/agent/prompts.py +368 -0
- localcode/agent/reasoning_loop.py +79 -0
- localcode/agent/recovery.py +606 -0
- localcode/agent/recovery_controller.py +25 -0
- localcode/agent/round_policy.py +23 -0
- localcode/agent/sections.py +241 -0
- localcode/agent/state_machine.py +100 -0
- localcode/agent/streaming.py +324 -0
- localcode/agent/tool_execution.py +368 -0
- localcode/agent/tool_orchestration.py +54 -0
- localcode/agent/turn_finalization.py +135 -0
- localcode/app.py +1595 -0
- localcode/approvals.py +17 -0
- localcode/auto_compact.py +176 -0
- localcode/autonomy.py +103 -0
- localcode/bin/__init__.py +0 -0
- localcode/bin/llama-server +0 -0
- localcode/bin/localcode-ui +4 -0
- localcode/bootstrap.py +1191 -0
- localcode/cache.py +194 -0
- localcode/checkpoint.py +375 -0
- localcode/compact.py +73 -0
- localcode/compaction.py +341 -0
- localcode/composer.py +72 -0
- localcode/config.py +521 -0
- localcode/context.py +161 -0
- localcode/display.py +58 -0
- localcode/embeddings.py +480 -0
- localcode/entrypoint.py +536 -0
- localcode/errors.py +350 -0
- localcode/events.py +409 -0
- localcode/evidence.py +102 -0
- localcode/execution_policy.py +48 -0
- localcode/features.py +308 -0
- localcode/formatting.py +91 -0
- localcode/headless_json.py +265 -0
- localcode/health.py +288 -0
- localcode/hf_quants.py +203 -0
- localcode/history.py +594 -0
- localcode/indexer.py +144 -0
- localcode/injection_defense.py +204 -0
- localcode/launcher.py +373 -0
- localcode/logging_utils.py +28 -0
- localcode/lsp.py +153 -0
- localcode/mcp/__init__.py +88 -0
- localcode/mcp/_bridge.py +61 -0
- localcode/mcp/_config.py +33 -0
- localcode/mcp/_transports.py +128 -0
- localcode/mcp/client.py +395 -0
- localcode/memory_guard.py +291 -0
- localcode/model_config.py +288 -0
- localcode/model_delete.py +444 -0
- localcode/model_families.py +279 -0
- localcode/models.py +236 -0
- localcode/models_catalog.py +901 -0
- localcode/notebook.py +123 -0
- localcode/output.py +403 -0
- localcode/patching.py +65 -0
- localcode/paths.py +303 -0
- localcode/performance.py +623 -0
- localcode/permissions.py +79 -0
- localcode/permissions_v2.py +310 -0
- localcode/plans.py +198 -0
- localcode/process_registry.py +271 -0
- localcode/protocol/__init__.py +56 -0
- localcode/protocol/events.py +116 -0
- localcode/protocol/jsonl.py +153 -0
- localcode/protocol/outcomes.py +231 -0
- localcode/reasoning_capabilities.py +59 -0
- localcode/recommendations.py +87 -0
- localcode/recovery.py +134 -0
- localcode/redaction.py +164 -0
- localcode/runtime.py +2331 -0
- localcode/server_manager.py +1069 -0
- localcode/session.py +296 -0
- localcode/shell.py +83 -0
- localcode/skills/debug.md +21 -0
- localcode/skills/edit-verified.md +30 -0
- localcode/skills/explain.md +19 -0
- localcode/skills/finish-verified.md +11 -0
- localcode/skills/git-commit-safely.md +30 -0
- localcode/skills/locate.md +21 -0
- localcode/skills/plan-task.md +51 -0
- localcode/skills/review.md +22 -0
- localcode/skills/run-tests.md +24 -0
- localcode/skills.py +728 -0
- localcode/snapshots.py +195 -0
- localcode/sounds.py +50 -0
- localcode/telemetry.py +273 -0
- localcode/theme.py +137 -0
- localcode/thermal.py +160 -0
- localcode/thinking.py +114 -0
- localcode/tool_router.py +246 -0
- localcode/toolkit.py +1241 -0
- localcode/tools/__init__.py +400 -0
- localcode/tools/agent.py +259 -0
- localcode/tools/append_file.py +55 -0
- localcode/tools/background_process.py +115 -0
- localcode/tools/base.py +168 -0
- localcode/tools/bash.py +1160 -0
- localcode/tools/code_navigation.py +97 -0
- localcode/tools/edit_diff.py +80 -0
- localcode/tools/edit_file.py +477 -0
- localcode/tools/facts.py +149 -0
- localcode/tools/glob_tool.py +60 -0
- localcode/tools/grep.py +65 -0
- localcode/tools/inspect_symbol.py +266 -0
- localcode/tools/launch_app.py +93 -0
- localcode/tools/list_files.py +42 -0
- localcode/tools/multi_edit.py +167 -0
- localcode/tools/plan_mode.py +86 -0
- localcode/tools/project_check.py +685 -0
- localcode/tools/read_file.py +261 -0
- localcode/tools/read_state.py +223 -0
- localcode/tools/skill_tool.py +41 -0
- localcode/tools/syntax_check.py +216 -0
- localcode/tools/todo_write.py +200 -0
- localcode/tools/tool_call_repair.py +157 -0
- localcode/tools/web_fetch.py +182 -0
- localcode/tools/web_search.py +45 -0
- localcode/tools/write_file.py +281 -0
- localcode/tui/__init__.py +1 -0
- localcode/tui/app.py +485 -0
- localcode/tui/bridge.py +78 -0
- localcode/tui/clipboard_image.py +153 -0
- localcode/tui/paste_collapse.py +95 -0
- localcode/tui/screens/__init__.py +0 -0
- localcode/tui/screens/chat.py +5052 -0
- localcode/tui/screens/confirm.py +123 -0
- localcode/tui/screens/mode_picker.py +79 -0
- localcode/tui/screens/model_picker.py +962 -0
- localcode/tui/screens/setup.py +891 -0
- localcode/tui/styles/__init__.py +0 -0
- localcode/tui/styles/app.tcss +197 -0
- localcode/tui/widgets/__init__.py +0 -0
- localcode/tui/widgets/approval.py +2 -0
- localcode/tui/widgets/chat_log.py +2063 -0
- localcode/tui/widgets/messages/__init__.py +21 -0
- localcode/tui/widgets/messages/diff.py +287 -0
- localcode/tui/widgets/voice_visualizer.py +122 -0
- localcode/turn_diff.py +180 -0
- localcode/ui/FORK_COMMIT +1 -0
- localcode/ui/__init__.py +58 -0
- localcode/ui/launch.py +204 -0
- localcode/ui/picker_cli.py +165 -0
- localcode/ui/plugin/localcode.ts +626 -0
- localcode/ui/ports.py +42 -0
- localcode/ui/server_cmd.py +89 -0
- localcode/ui/supervisor.py +736 -0
- localcode/undo.py +121 -0
- localcode/verification.py +87 -0
- localcode/voice.py +948 -0
- localcode-0.4.0.dist-info/METADATA +160 -0
- localcode-0.4.0.dist-info/RECORD +173 -0
- localcode-0.4.0.dist-info/WHEEL +5 -0
- localcode-0.4.0.dist-info/entry_points.txt +3 -0
- localcode-0.4.0.dist-info/licenses/LICENSE +201 -0
- localcode-0.4.0.dist-info/top_level.txt +1 -0
localcode/agent/goal.py
ADDED
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
"""Goal-state inference for a single user turn.
|
|
2
|
+
|
|
3
|
+
LocalCode's earlier loop inferred "done" from surface form:
|
|
4
|
+
no tool calls + some assistant text. That is too weak for agentic work.
|
|
5
|
+
This module gives each turn a first-class goal object so prompting,
|
|
6
|
+
telemetry, history, and completion gates all speak the same language.
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from dataclasses import asdict, dataclass
|
|
11
|
+
import re
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
__all__ = [
|
|
15
|
+
"GoalState",
|
|
16
|
+
"infer_goal_state",
|
|
17
|
+
"extract_feature_criteria",
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
_FEATURE_EXTRACTION_PROMPT = """\
|
|
22
|
+
You are a requirements analyst. Read the user's request and output the discrete, \
|
|
23
|
+
verifiable features the user wants. ONE per line, prefixed with "- ". \
|
|
24
|
+
Output ONLY the checklist — no preamble, no commentary, no numbering, no headings.
|
|
25
|
+
|
|
26
|
+
Each item must:
|
|
27
|
+
- be a single concrete outcome the user wants (not a step or action),
|
|
28
|
+
- be specific enough that someone else could verify it later (with a probe, a read, or a test),
|
|
29
|
+
- restate ONLY what the user asked for — do NOT invent features the user did not request.
|
|
30
|
+
|
|
31
|
+
If the request is trivial (e.g. a question, a one-line edit, a quick command), output \
|
|
32
|
+
the single line "- (trivial — no feature checklist needed)" and nothing else.
|
|
33
|
+
|
|
34
|
+
User request:
|
|
35
|
+
\"\"\"
|
|
36
|
+
{user_text}
|
|
37
|
+
\"\"\"
|
|
38
|
+
|
|
39
|
+
Checklist:
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def extract_feature_criteria(runtime, user_text: str, *, max_features: int = 12) -> list[str]:
|
|
44
|
+
"""Best-effort extraction of discrete user-requested features.
|
|
45
|
+
|
|
46
|
+
Calls the underlying model once at task intake to decompose the user's
|
|
47
|
+
request into a verifiable checklist. The model sees ONLY the user's
|
|
48
|
+
text — no app-specific scaffolding — so the result is general across
|
|
49
|
+
any kind of request (build_app, edit, dashboard, fix, etc.).
|
|
50
|
+
|
|
51
|
+
Returns [] on any failure or if the model decides the request is
|
|
52
|
+
trivial. Caller should fall back to the pre-existing generic
|
|
53
|
+
success_criteria in that case.
|
|
54
|
+
|
|
55
|
+
Why a model call rather than a heuristic: phrasings like
|
|
56
|
+
'with audio AND text AND quiz' vs 'should support uploading and exporting CSV'
|
|
57
|
+
vs 'a dashboard showing revenue / churn / MRR' don't share a common
|
|
58
|
+
parse tree. Heuristic splitters miss most of them. The model's
|
|
59
|
+
natural-language understanding gets it right cheaply (~3 s of TTFT,
|
|
60
|
+
~50–150 decoded tokens at task start).
|
|
61
|
+
"""
|
|
62
|
+
text = (user_text or "").strip()
|
|
63
|
+
if not text or len(text) < 20:
|
|
64
|
+
return []
|
|
65
|
+
try:
|
|
66
|
+
prompt = _FEATURE_EXTRACTION_PROMPT.format(user_text=text)
|
|
67
|
+
raw = runtime.generate_once(
|
|
68
|
+
[{"role": "user", "content": prompt}],
|
|
69
|
+
max_tokens=512,
|
|
70
|
+
)
|
|
71
|
+
except Exception:
|
|
72
|
+
return []
|
|
73
|
+
if not raw:
|
|
74
|
+
return []
|
|
75
|
+
out: list[str] = []
|
|
76
|
+
for line in raw.splitlines():
|
|
77
|
+
s = line.strip()
|
|
78
|
+
if not s:
|
|
79
|
+
continue
|
|
80
|
+
if s.startswith(("- ", "* ", "• ")):
|
|
81
|
+
s = s[2:].strip()
|
|
82
|
+
elif s[:1].isdigit():
|
|
83
|
+
# Strip leading "1. " / "1) " numbering if the model added it
|
|
84
|
+
# despite our instruction.
|
|
85
|
+
m = re.match(r"^\d+[.)]\s*(.*)", s)
|
|
86
|
+
if m:
|
|
87
|
+
s = m.group(1).strip()
|
|
88
|
+
if not s:
|
|
89
|
+
continue
|
|
90
|
+
if "trivial" in s.lower() and "no feature" in s.lower():
|
|
91
|
+
return []
|
|
92
|
+
# Drop obvious meta-commentary the model sometimes emits despite
|
|
93
|
+
# the instruction.
|
|
94
|
+
if s.lower().startswith(("here is", "here's", "checklist", "the user")):
|
|
95
|
+
continue
|
|
96
|
+
if len(s) > 200:
|
|
97
|
+
s = s[:197] + "…"
|
|
98
|
+
if s not in out:
|
|
99
|
+
out.append(s)
|
|
100
|
+
if len(out) >= max_features:
|
|
101
|
+
break
|
|
102
|
+
return out
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
_SLUG_STOPWORDS = {
|
|
106
|
+
"a",
|
|
107
|
+
"an",
|
|
108
|
+
"and",
|
|
109
|
+
"app",
|
|
110
|
+
"application",
|
|
111
|
+
"ability",
|
|
112
|
+
"able",
|
|
113
|
+
"all",
|
|
114
|
+
"build",
|
|
115
|
+
"can",
|
|
116
|
+
"create",
|
|
117
|
+
"for",
|
|
118
|
+
"from",
|
|
119
|
+
"have",
|
|
120
|
+
"help",
|
|
121
|
+
"i",
|
|
122
|
+
"in",
|
|
123
|
+
"it",
|
|
124
|
+
"make",
|
|
125
|
+
"me",
|
|
126
|
+
"my",
|
|
127
|
+
"of",
|
|
128
|
+
"please",
|
|
129
|
+
"should",
|
|
130
|
+
"that",
|
|
131
|
+
"the",
|
|
132
|
+
"this",
|
|
133
|
+
"to",
|
|
134
|
+
"using",
|
|
135
|
+
"via",
|
|
136
|
+
"with",
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
@dataclass(frozen=True)
|
|
141
|
+
class GoalState:
|
|
142
|
+
raw_user_text: str
|
|
143
|
+
goal_type: str
|
|
144
|
+
task_kind: str
|
|
145
|
+
task_slug: str
|
|
146
|
+
goal_summary: str
|
|
147
|
+
success_criteria: tuple[str, ...]
|
|
148
|
+
allows_blocking_question: bool = True
|
|
149
|
+
|
|
150
|
+
def as_dict(self) -> dict:
|
|
151
|
+
data = asdict(self)
|
|
152
|
+
data["success_criteria"] = list(self.success_criteria)
|
|
153
|
+
return data
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
# NOTE ON THIS CLASSIFIER: goal_type is a WEAK hint, not the authority on
|
|
157
|
+
# whether a task is finished. Completion is governed by the model's own todo
|
|
158
|
+
# list (see the open-todo gate in agent/loop.py) — never by these regexes or by
|
|
159
|
+
# a detected server port. History: "Build me an Anki clone … so I can launch it
|
|
160
|
+
# like a native app" used to match the run_or_launch pattern on the phrase
|
|
161
|
+
# "launch it", which then let a freshly-opened dev-server port count as "app
|
|
162
|
+
# built" at ~10% of the work. The lasting fix is that a port no longer ends a
|
|
163
|
+
# turn while todos are open; the widening below just keeps genuine builds out of
|
|
164
|
+
# run_or_launch in the first place. Misclassifying an edit as build_app is
|
|
165
|
+
# cheap (its criteria are just "code exists + verification passes"); the
|
|
166
|
+
# dangerous direction is a build leaking into run_or_launch, so bias toward
|
|
167
|
+
# build_app and require run_or_launch to be an *unambiguous* run request.
|
|
168
|
+
_BUILD_VERB = r"(?:build|create|make|scaffold|implement|develop|generate|write|design|code)"
|
|
169
|
+
_APP_NOUN = (
|
|
170
|
+
r"(?:app|application|web ?app|website|web ?page|site|dashboard|api|service|"
|
|
171
|
+
r"project|clone|game|tool|extension|widget|spa|pwa|frontend|back ?end|"
|
|
172
|
+
r"platform|bot|cli|ui|component|library|package|script|program|"
|
|
173
|
+
r"tracker|editor|viewer|generator)"
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
_TRIVIAL_TURN_RE = re.compile(
|
|
178
|
+
r"^(hi|hey|hello|yo|sup|thanks|thank you|thx|ta|ok|okay|k|cool|nice|great|"
|
|
179
|
+
r"good (morning|afternoon|evening|night)|how are you|what'?s up|"
|
|
180
|
+
r"morning|evening)[\s!.?,]*$",
|
|
181
|
+
re.IGNORECASE,
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def is_trivial_turn(user_text: str) -> bool:
|
|
186
|
+
"""True for a greeting or acknowledgement - conversation, not a task.
|
|
187
|
+
|
|
188
|
+
The system prompt tells the model to plan multi-step work with todo_write,
|
|
189
|
+
and its carve-out is written in units of task size ("skip the plan for
|
|
190
|
+
straightforward one/two-step tasks"). Nothing there covers a turn that is
|
|
191
|
+
not a task at all, so a small quant reading a header that shouts FINISH THE
|
|
192
|
+
WHOLE TASK will happily open a todo list for "hi". Callers use this to stop
|
|
193
|
+
offering the tool at all on such turns - a model cannot call what it was
|
|
194
|
+
never given.
|
|
195
|
+
"""
|
|
196
|
+
return bool(_TRIVIAL_TURN_RE.match((user_text or "").strip()))
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def infer_goal_state(user_text: str) -> GoalState:
|
|
200
|
+
text = (user_text or "").strip()
|
|
201
|
+
lower = text.lower()
|
|
202
|
+
# build verb + app-ish noun anywhere in the request → it's a build. No tight
|
|
203
|
+
# proximity window (the old `.{0,40}` cap is exactly why "Build me a … web
|
|
204
|
+
# app" fell through — the noun sat 48 chars past the verb).
|
|
205
|
+
if re.search(rf"\b{_BUILD_VERB}\b", lower) and re.search(rf"\b{_APP_NOUN}\b", lower):
|
|
206
|
+
goal_type, task_kind = "build_app", "new_app"
|
|
207
|
+
criteria = ("Requested implementation exists on disk", "Relevant build or tests pass", "Runtime behavior is verified when applicable")
|
|
208
|
+
elif re.search(r"\b(run|launch|start|serve|open)\b.{0,24}\b(app|server|site|project|it)\b", lower):
|
|
209
|
+
goal_type, task_kind = "run_or_launch", "run"
|
|
210
|
+
criteria = ("Target process is running", "Exact access URL or process status is reported")
|
|
211
|
+
elif re.search(r"\b(fix|change|edit|update|refactor|rename|remove|add)\b", lower):
|
|
212
|
+
goal_type, task_kind = "edit_existing", "edit"
|
|
213
|
+
criteria = ("Requested code change exists", "Relevant verification passes")
|
|
214
|
+
elif re.match(r"^(what|why|how|where|when|who|is|are|can|could|should|does|do)\b", lower) or text.endswith("?"):
|
|
215
|
+
goal_type, task_kind = "question", "question"
|
|
216
|
+
criteria = ("Question is answered from grounded evidence",)
|
|
217
|
+
else:
|
|
218
|
+
goal_type, task_kind = "general_task", "general_task"
|
|
219
|
+
criteria = ("Assistant makes concrete progress toward the user's request", "Assistant stops only on completion or a focused blocking question")
|
|
220
|
+
return GoalState(
|
|
221
|
+
raw_user_text=text,
|
|
222
|
+
goal_type=goal_type,
|
|
223
|
+
task_kind=task_kind,
|
|
224
|
+
task_slug=_make_task_slug(text),
|
|
225
|
+
goal_summary=text[:240],
|
|
226
|
+
success_criteria=criteria,
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def _make_task_slug(text: str) -> str:
|
|
231
|
+
"""Short internal task label, not a forced project directory."""
|
|
232
|
+
words = re.findall(r"[a-z0-9]+", (text or "").lower())
|
|
233
|
+
seen: set[str] = set()
|
|
234
|
+
content_words: list[str] = []
|
|
235
|
+
for word in words:
|
|
236
|
+
if word in _SLUG_STOPWORDS or len(word) <= 1 or word in seen:
|
|
237
|
+
continue
|
|
238
|
+
seen.add(word)
|
|
239
|
+
content_words.append(word)
|
|
240
|
+
selected = content_words[:4] or words[:3] or ["task"]
|
|
241
|
+
clean = "-".join(selected)
|
|
242
|
+
clean = re.sub(r"-+", "-", clean).strip("-") or "task"
|
|
243
|
+
clean = clean[:30].strip("-") or "task"
|
|
244
|
+
return clean
|