loom-code 0.1.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- loom_code/__init__.py +22 -0
- loom_code/_post_commit.py +119 -0
- loom_code/agent.py +544 -0
- loom_code/approval.py +616 -0
- loom_code/browse/__init__.py +291 -0
- loom_code/browse/act.py +467 -0
- loom_code/browse/observe.py +249 -0
- loom_code/browse/session.py +96 -0
- loom_code/browse/verify.py +194 -0
- loom_code/checkpoint.py +283 -0
- loom_code/cli.py +495 -0
- loom_code/code_index.py +703 -0
- loom_code/compact.py +143 -0
- loom_code/consent.py +47 -0
- loom_code/credentials.py +527 -0
- loom_code/edit_tool.py +635 -0
- loom_code/extensions.py +522 -0
- loom_code/file_history.py +322 -0
- loom_code/file_tools.py +93 -0
- loom_code/git_hook.py +200 -0
- loom_code/grep_tool.py +430 -0
- loom_code/hooks.py +297 -0
- loom_code/loominit/__init__.py +23 -0
- loom_code/loominit/_ast_walk.py +429 -0
- loom_code/loominit/_files.py +284 -0
- loom_code/loominit/_graph.py +141 -0
- loom_code/loominit/_resolve.py +392 -0
- loom_code/loominit/_tests_map.py +108 -0
- loom_code/loominit/extractor.py +332 -0
- loom_code/loominit/repomap.py +225 -0
- loom_code/loominit/schema.py +242 -0
- loom_code/lsp_tools.py +396 -0
- loom_code/mcp_host.py +79 -0
- loom_code/operator.py +449 -0
- loom_code/paste.py +97 -0
- loom_code/paths.py +52 -0
- loom_code/permissions.py +177 -0
- loom_code/project.py +104 -0
- loom_code/prompts.py +451 -0
- loom_code/render.py +783 -0
- loom_code/repl.py +4080 -0
- loom_code/rules.py +267 -0
- loom_code/sandboxed_bash.py +176 -0
- loom_code/scribe.py +88 -0
- loom_code/skills/__init__.py +16 -0
- loom_code/skills/graphify/SKILL.md +97 -0
- loom_code/skills/graphify/tools.py +570 -0
- loom_code/trust.py +216 -0
- loom_code/turn.py +169 -0
- loom_code/web_fetch.py +370 -0
- loom_code/workers.py +758 -0
- loom_code/worktree.py +134 -0
- loom_code-0.1.1.dist-info/METADATA +224 -0
- loom_code-0.1.1.dist-info/RECORD +58 -0
- loom_code-0.1.1.dist-info/WHEEL +5 -0
- loom_code-0.1.1.dist-info/entry_points.txt +2 -0
- loom_code-0.1.1.dist-info/licenses/LICENSE +21 -0
- loom_code-0.1.1.dist-info/top_level.txt +1 -0
loom_code/compact.py
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
"""Conversation compaction — keeps long REPL sessions from blowing
|
|
2
|
+
through the model's context window.
|
|
3
|
+
|
|
4
|
+
The pattern:
|
|
5
|
+
|
|
6
|
+
1. After each REPL turn, the REPL accumulates the cumulative token
|
|
7
|
+
count and a list of ``(user_prompt, agent_output)`` exchanges.
|
|
8
|
+
2. When the cumulative count crosses a threshold (default: 80% of
|
|
9
|
+
the active model's context window; configurable per session via
|
|
10
|
+
the ``/compress_token_length`` slash command) the REPL hands
|
|
11
|
+
the exchanges to a :class:`Compactor`.
|
|
12
|
+
3. The compactor is a separate, single-shot loomflow ``Agent`` —
|
|
13
|
+
no tools, dedicated prompt — that produces a dense prose
|
|
14
|
+
summary preserving what was attempted, what worked, what
|
|
15
|
+
failed, and any constraints the user expressed.
|
|
16
|
+
4. The summary lands in ``agent.memory.update_block(
|
|
17
|
+
"session_summary", text)`` — a working block, which loomflow
|
|
18
|
+
auto-injects into every subsequent system prompt.
|
|
19
|
+
5. The REPL resets ``session_id`` so the next turn starts with a
|
|
20
|
+
fresh conversation thread but immediately "remembers" the
|
|
21
|
+
session-so-far through the working block.
|
|
22
|
+
|
|
23
|
+
Pure loomflow primitives — no framework changes.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
from __future__ import annotations
|
|
27
|
+
|
|
28
|
+
from loomflow import Agent
|
|
29
|
+
|
|
30
|
+
# Best-effort context-window lookup. Substring match against known
|
|
31
|
+
# model families; fallback for anything we don't recognise (local
|
|
32
|
+
# Ollama models, niche LiteLLM providers, future model names) —
|
|
33
|
+
# the user can always override via /compress_token_length.
|
|
34
|
+
#
|
|
35
|
+
# Keep this list short and obvious. We're not trying to be a
|
|
36
|
+
# tokenizer service — just give a sensible default that adapts to
|
|
37
|
+
# the model the user picked.
|
|
38
|
+
_KNOWN_CONTEXT_WINDOWS: dict[str, int] = {
|
|
39
|
+
# OpenAI 4.1 family — 1M context.
|
|
40
|
+
"gpt-4.1": 1_000_000,
|
|
41
|
+
# OpenAI 4o family — 128k.
|
|
42
|
+
"gpt-4o": 128_000,
|
|
43
|
+
# OpenAI o-series reasoning models — 200k.
|
|
44
|
+
"o4": 200_000,
|
|
45
|
+
"o3": 200_000,
|
|
46
|
+
# Anthropic 4.x family — 200k.
|
|
47
|
+
"claude-opus": 200_000,
|
|
48
|
+
"claude-sonnet": 200_000,
|
|
49
|
+
"claude-haiku": 200_000,
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
# Conservative default for unknown models — typical of small open
|
|
53
|
+
# local models (llama3 8B = 8k, qwen2.5 7B = 32k, etc.). Users on
|
|
54
|
+
# bigger local models or unusual providers can /compress_token_length
|
|
55
|
+
# upward.
|
|
56
|
+
_FALLBACK_CONTEXT_WINDOW = 32_000
|
|
57
|
+
|
|
58
|
+
# Trigger when cumulative usage crosses this fraction of the model's
|
|
59
|
+
# context window. 0.8 leaves headroom for the current turn's own
|
|
60
|
+
# prompt + tool I/O without bumping the actual limit.
|
|
61
|
+
_DEFAULT_TRIGGER_FRACTION = 0.8
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def context_window_for(model: str) -> int:
|
|
65
|
+
"""Best-effort context-window estimate for ``model``.
|
|
66
|
+
|
|
67
|
+
Substring match — ``"gpt-4.1-mini"`` and ``"gpt-4.1-nano"`` both
|
|
68
|
+
pick up the ``"gpt-4.1"`` entry. Returns
|
|
69
|
+
:data:`_FALLBACK_CONTEXT_WINDOW` when nothing matches; the user
|
|
70
|
+
can override via ``/compress_token_length`` if the fallback is
|
|
71
|
+
wrong for their model.
|
|
72
|
+
"""
|
|
73
|
+
lower = model.lower()
|
|
74
|
+
for key, ctx in _KNOWN_CONTEXT_WINDOWS.items():
|
|
75
|
+
if key in lower:
|
|
76
|
+
return ctx
|
|
77
|
+
return _FALLBACK_CONTEXT_WINDOW
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def default_compact_threshold(model: str) -> int:
|
|
81
|
+
"""Default token threshold at which the REPL should compact —
|
|
82
|
+
80% of the model's context window."""
|
|
83
|
+
return int(context_window_for(model) * _DEFAULT_TRIGGER_FRACTION)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
_COMPACTOR_PROMPT = """\
|
|
87
|
+
You are the CONVERSATION COMPACTOR. A user has been in a long REPL
|
|
88
|
+
session with a coding agent and the session is approaching its
|
|
89
|
+
context limit. Your job: compress the history into a dense running
|
|
90
|
+
summary the next turn will use as its ONLY memory of everything
|
|
91
|
+
before it.
|
|
92
|
+
|
|
93
|
+
You will receive a list of (USER, AGENT) exchanges. Write ONE
|
|
94
|
+
dense paragraph (300-500 words). Preserve, in roughly this order:
|
|
95
|
+
|
|
96
|
+
1. What the user is trying to accomplish — the overarching goal.
|
|
97
|
+
2. Each concrete change attempted and the outcome — succeeded /
|
|
98
|
+
failed / partially done.
|
|
99
|
+
3. Specific identifiers worth remembering: file paths, function
|
|
100
|
+
names, branch names, command outputs, error messages.
|
|
101
|
+
4. Decisions or constraints the user expressed ("don't touch X",
|
|
102
|
+
"we agreed on Y", "the API is Z").
|
|
103
|
+
5. Open questions or in-progress threads.
|
|
104
|
+
|
|
105
|
+
Do NOT:
|
|
106
|
+
- Include casual back-and-forth that didn't lead anywhere.
|
|
107
|
+
- Repeat boilerplate ("I'll help you with that...").
|
|
108
|
+
- Use markdown headers, bullet lists, or code blocks — just prose.
|
|
109
|
+
- Editorialise. Be factual.
|
|
110
|
+
|
|
111
|
+
Make it load-bearing. Future-you will reconstruct context from
|
|
112
|
+
this and nothing else.
|
|
113
|
+
"""
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
class Compactor:
|
|
117
|
+
"""A small loomflow ``Agent`` whose only job is to summarise a
|
|
118
|
+
conversation history. Builds once, reuses across compactions
|
|
119
|
+
in the same session."""
|
|
120
|
+
|
|
121
|
+
def __init__(self, *, model: str) -> None:
|
|
122
|
+
self._agent = Agent(
|
|
123
|
+
_COMPACTOR_PROMPT,
|
|
124
|
+
model=model,
|
|
125
|
+
# No tools — single-shot summarisation. prompt_caching
|
|
126
|
+
# helps if the agent runs multiple times in a session
|
|
127
|
+
# (the system prompt is stable).
|
|
128
|
+
prompt_caching=True,
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
async def compact(
|
|
132
|
+
self, exchanges: list[tuple[str, str]]
|
|
133
|
+
) -> str:
|
|
134
|
+
"""Run the compactor on a list of ``(user_prompt,
|
|
135
|
+
agent_output)`` pairs. Returns the summary as plain text."""
|
|
136
|
+
if not exchanges:
|
|
137
|
+
return ""
|
|
138
|
+
rendered = "\n\n".join(
|
|
139
|
+
f"USER:\n{user.strip()}\n\nAGENT:\n{out.strip()}"
|
|
140
|
+
for user, out in exchanges
|
|
141
|
+
)
|
|
142
|
+
result = await self._agent.run(rendered, user_id="loom-code")
|
|
143
|
+
return result.output.strip()
|
loom_code/consent.py
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"""Session registry of files the USER explicitly referenced.
|
|
2
|
+
|
|
3
|
+
The consent model in one line: **any path the user types or pastes IS
|
|
4
|
+
the permission to TARGET that file** — bare or ``@``-mentioned — but
|
|
5
|
+
not the permission to skip confirmation. A referenced file may be
|
|
6
|
+
``edit``/``multi_edit``ed even outside the project root; the
|
|
7
|
+
ApprovalGate STILL forces a diff preview + confirm for every
|
|
8
|
+
outside-project edit, in EVERY mode (accept-edits / yolo / allow-all /
|
|
9
|
+
--yes), so no out-of-tree file is ever mutated without a human seeing
|
|
10
|
+
the change. That gate is why granting on a bare paste is safe: a path
|
|
11
|
+
that merely appears in a pasted stack trace becomes a candidate, but
|
|
12
|
+
the user still sees + rejects the diff prompt before anything is
|
|
13
|
+
written — it is never silently mutated.
|
|
14
|
+
|
|
15
|
+
Module-level on purpose: the REPL registers paths as it expands
|
|
16
|
+
mentions, and the edit tools — built once at agent construction,
|
|
17
|
+
long before any mention exists — consult it lazily per call. A
|
|
18
|
+
plain set beats threading a handle through six build functions.
|
|
19
|
+
Lifetime is the process (one REPL session); nothing persists.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
from pathlib import Path
|
|
25
|
+
|
|
26
|
+
_granted: set[Path] = set()
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def grant(path: Path | str) -> None:
|
|
30
|
+
"""Record that the user explicitly referenced ``path``."""
|
|
31
|
+
try:
|
|
32
|
+
_granted.add(Path(path).expanduser().resolve())
|
|
33
|
+
except OSError:
|
|
34
|
+
pass
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def is_granted(path: Path | str) -> bool:
|
|
38
|
+
"""True if the user referenced exactly this file this session."""
|
|
39
|
+
try:
|
|
40
|
+
return Path(path).expanduser().resolve() in _granted
|
|
41
|
+
except OSError:
|
|
42
|
+
return False
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def reset() -> None:
|
|
46
|
+
"""Drop all grants (used by /clear and tests)."""
|
|
47
|
+
_granted.clear()
|