@prateek_ai/agents-maker 1.0.0
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.
- package/LICENSE +21 -0
- package/README.md +659 -0
- package/agents/architect_agent.md +175 -0
- package/agents/code_agent.md +178 -0
- package/agents/compression_agent.md +226 -0
- package/agents/execution_agent.md +157 -0
- package/agents/orchestrator.md +406 -0
- package/agents/reviewer_agent.md +134 -0
- package/agents/ui_agent.md +147 -0
- package/agents/ux_agent.md +144 -0
- package/bin/cli.js +79 -0
- package/config/agents.yaml +388 -0
- package/config/domain_profiles.yaml +394 -0
- package/config/project.yaml.example +16 -0
- package/config/token_policies.yaml +325 -0
- package/context_loaders/__init__.py +15 -0
- package/context_loaders/__pycache__/__init__.cpython-314.pyc +0 -0
- package/context_loaders/__pycache__/file_chunker.cpython-314.pyc +0 -0
- package/context_loaders/__pycache__/project_summary.cpython-314.pyc +0 -0
- package/context_loaders/__pycache__/repo_tree.cpython-314.pyc +0 -0
- package/context_loaders/file_chunker.py +252 -0
- package/context_loaders/project_summary.py +369 -0
- package/context_loaders/repo_tree.py +203 -0
- package/package.json +46 -0
- package/quickstart.ps1 +252 -0
- package/quickstart.sh +232 -0
- package/requirements.txt +3 -0
- package/skills/analyze_repo.md +86 -0
- package/skills/animated_website.md +285 -0
- package/skills/compare_approaches.md +86 -0
- package/skills/define_data_schema.md +99 -0
- package/skills/design_api.md +126 -0
- package/skills/improve_copy.md +69 -0
- package/skills/review_code.md +83 -0
- package/skills/review_layout.md +89 -0
- package/skills/suggest_next.md +105 -0
- package/skills/summarize_history.md +89 -0
- package/skills/write_process_map.md +105 -0
- package/skills/write_tests.md +97 -0
- package/token_optimization/__pycache__/compressor.cpython-314.pyc +0 -0
- package/token_optimization/compressor.py +502 -0
- package/token_optimization/output_styles.md +80 -0
- package/tools/__pycache__/domain_utils.cpython-314.pyc +0 -0
- package/tools/__pycache__/generate_claude_md.cpython-314.pyc +0 -0
- package/tools/__pycache__/validate_kit.cpython-314.pyc +0 -0
- package/tools/domain_utils.py +141 -0
- package/tools/generate_claude_md.py +269 -0
- package/tools/generate_platform_configs.py +467 -0
- package/tools/generate_prompt.py +461 -0
- package/tools/init_project.py +454 -0
- package/tools/test_kit.py +363 -0
- package/tools/validate_kit.py +504 -0
|
@@ -0,0 +1,502 @@
|
|
|
1
|
+
"""
|
|
2
|
+
token_optimization/compressor.py
|
|
3
|
+
|
|
4
|
+
Skeleton for the programmatic token optimization layer.
|
|
5
|
+
Applies token policies from config/token_policies.yaml before sending
|
|
6
|
+
a prompt to any LLM provider.
|
|
7
|
+
|
|
8
|
+
Usage (dry run, no API calls):
|
|
9
|
+
python token_optimization/compressor.py --dry-run \\
|
|
10
|
+
--policy feature_implementation \\
|
|
11
|
+
--context-file /path/to/context.txt \\
|
|
12
|
+
--query "Add soft-delete to UserService"
|
|
13
|
+
|
|
14
|
+
This module has NO live HTTP calls. Implement a ProviderAdapter subclass
|
|
15
|
+
and call adapter.send(compressed_context) to integrate with a real API.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import argparse
|
|
21
|
+
import json
|
|
22
|
+
import sys
|
|
23
|
+
from abc import ABC, abstractmethod
|
|
24
|
+
from dataclasses import dataclass, field
|
|
25
|
+
from pathlib import Path
|
|
26
|
+
from typing import Any
|
|
27
|
+
|
|
28
|
+
# ---------------------------------------------------------------------------
|
|
29
|
+
# Data types
|
|
30
|
+
# ---------------------------------------------------------------------------
|
|
31
|
+
|
|
32
|
+
@dataclass
|
|
33
|
+
class FileEntry:
|
|
34
|
+
path: str
|
|
35
|
+
content: str
|
|
36
|
+
relevance_score: float = 0.0
|
|
37
|
+
truncated: bool = False
|
|
38
|
+
lines_omitted: int = 0
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@dataclass
|
|
42
|
+
class TokenPolicy:
|
|
43
|
+
workflow: str
|
|
44
|
+
max_input_files: int = 8
|
|
45
|
+
max_input_tokens: int = 24000
|
|
46
|
+
history_summarize_after_turns: int = 6
|
|
47
|
+
output_style: str = "standard"
|
|
48
|
+
relevance_drop_threshold: float = 0.35
|
|
49
|
+
snippet_max_lines: int = 200
|
|
50
|
+
snippet_head_lines: int = 40
|
|
51
|
+
snippet_tail_lines: int = 40
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@dataclass
|
|
55
|
+
class ContextBlock:
|
|
56
|
+
project_state: str = ""
|
|
57
|
+
files: list[FileEntry] = field(default_factory=list)
|
|
58
|
+
conversation_state: str = ""
|
|
59
|
+
active_query: str = ""
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@dataclass
|
|
63
|
+
class CompressionReport:
|
|
64
|
+
turns_summarized: int = 0
|
|
65
|
+
files_dropped: list[tuple[str, float]] = field(default_factory=list)
|
|
66
|
+
files_truncated: list[tuple[str, int]] = field(default_factory=list)
|
|
67
|
+
files_retained: int = 0
|
|
68
|
+
estimated_token_reduction_pct: float = 0.0
|
|
69
|
+
warnings: list[str] = field(default_factory=list)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
# ---------------------------------------------------------------------------
|
|
73
|
+
# Policy loader
|
|
74
|
+
# ---------------------------------------------------------------------------
|
|
75
|
+
|
|
76
|
+
class PolicyLoader:
|
|
77
|
+
"""Loads token policies from config/token_policies.yaml."""
|
|
78
|
+
|
|
79
|
+
def __init__(self, config_path: str | Path = "config/token_policies.yaml") -> None:
|
|
80
|
+
self._config_path = Path(config_path)
|
|
81
|
+
self._raw: dict[str, Any] = {}
|
|
82
|
+
|
|
83
|
+
def load(self) -> None:
|
|
84
|
+
"""Parse the YAML config file. Requires PyYAML if used; falls back to empty defaults."""
|
|
85
|
+
try:
|
|
86
|
+
import yaml # optional dep
|
|
87
|
+
with self._config_path.open() as f:
|
|
88
|
+
self._raw = yaml.safe_load(f) or {}
|
|
89
|
+
except ImportError:
|
|
90
|
+
print(
|
|
91
|
+
"[compressor] PyYAML not installed — using built-in defaults. "
|
|
92
|
+
"Install pyyaml to load token_policies.yaml.",
|
|
93
|
+
file=sys.stderr,
|
|
94
|
+
)
|
|
95
|
+
except FileNotFoundError:
|
|
96
|
+
print(
|
|
97
|
+
f"[compressor] config file not found at {self._config_path} — using defaults.",
|
|
98
|
+
file=sys.stderr,
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
def get_workflow_policy(self, workflow: str) -> TokenPolicy:
|
|
102
|
+
"""Return a TokenPolicy for the given workflow name."""
|
|
103
|
+
defaults = self._raw.get("defaults", {})
|
|
104
|
+
workflow_overrides = self._raw.get("workflows", {}).get(workflow, {})
|
|
105
|
+
merged = {**defaults, **workflow_overrides}
|
|
106
|
+
return TokenPolicy(
|
|
107
|
+
workflow=workflow,
|
|
108
|
+
max_input_files=merged.get("max_input_files", 8),
|
|
109
|
+
max_input_tokens=merged.get("max_input_tokens", 24000),
|
|
110
|
+
history_summarize_after_turns=merged.get("history_summarize_after_turns", 6),
|
|
111
|
+
output_style=merged.get("output_style", "standard"),
|
|
112
|
+
relevance_drop_threshold=merged.get("relevance_drop_threshold", 0.35),
|
|
113
|
+
snippet_max_lines=merged.get("snippet_max_lines", 200),
|
|
114
|
+
snippet_head_lines=merged.get("snippet_head_lines", 40),
|
|
115
|
+
snippet_tail_lines=merged.get("snippet_tail_lines", 40),
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
# ---------------------------------------------------------------------------
|
|
120
|
+
# Relevance filter
|
|
121
|
+
# ---------------------------------------------------------------------------
|
|
122
|
+
|
|
123
|
+
class RelevanceFilter:
|
|
124
|
+
"""
|
|
125
|
+
Scores and selects files for inclusion in the context block.
|
|
126
|
+
See token_optimization/relevance_filter.md for the full scoring model.
|
|
127
|
+
"""
|
|
128
|
+
|
|
129
|
+
weights = {
|
|
130
|
+
"lexical_overlap": 0.35,
|
|
131
|
+
"direct_reference": 0.30,
|
|
132
|
+
"symbol_mention": 0.20,
|
|
133
|
+
"recency": 0.10,
|
|
134
|
+
"structural_importance": 0.05,
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
_structural_patterns = ("pyproject.toml", "package.json", "setup.py", "Makefile", "schema")
|
|
138
|
+
|
|
139
|
+
def __init__(self, policy: TokenPolicy) -> None:
|
|
140
|
+
self.policy = policy
|
|
141
|
+
|
|
142
|
+
def score_files(self, files: list[FileEntry], query: str) -> list[FileEntry]:
|
|
143
|
+
"""Compute relevance scores for each file in-place and return the list."""
|
|
144
|
+
query_tokens = set(query.lower().split())
|
|
145
|
+
for f in files:
|
|
146
|
+
f.relevance_score = self._score(f, query_tokens, query)
|
|
147
|
+
return files
|
|
148
|
+
|
|
149
|
+
def select(self, scored_files: list[FileEntry]) -> tuple[list[FileEntry], list[FileEntry]]:
|
|
150
|
+
"""
|
|
151
|
+
Partition scored files into (retained, dropped).
|
|
152
|
+
Retained files are sorted by score descending and capped at max_input_files.
|
|
153
|
+
"""
|
|
154
|
+
above_threshold = [
|
|
155
|
+
f for f in scored_files
|
|
156
|
+
if f.relevance_score >= self.policy.relevance_drop_threshold
|
|
157
|
+
]
|
|
158
|
+
dropped = [
|
|
159
|
+
f for f in scored_files
|
|
160
|
+
if f.relevance_score < self.policy.relevance_drop_threshold
|
|
161
|
+
]
|
|
162
|
+
above_threshold.sort(key=lambda f: f.relevance_score, reverse=True)
|
|
163
|
+
retained = above_threshold[: self.policy.max_input_files]
|
|
164
|
+
dropped += above_threshold[self.policy.max_input_files :]
|
|
165
|
+
return retained, dropped
|
|
166
|
+
|
|
167
|
+
# ------------------------------------------------------------------
|
|
168
|
+
# Private helpers
|
|
169
|
+
# ------------------------------------------------------------------
|
|
170
|
+
|
|
171
|
+
def _score(self, f: FileEntry, query_tokens: set[str], raw_query: str) -> float:
|
|
172
|
+
content_lower = f.content.lower()
|
|
173
|
+
path_lower = f.path.lower()
|
|
174
|
+
|
|
175
|
+
lexical = self._lexical_overlap(content_lower, query_tokens)
|
|
176
|
+
direct = 1.0 if f.path in raw_query or Path(f.path).stem.lower() in raw_query.lower() else 0.0
|
|
177
|
+
symbol = self._symbol_mention(content_lower, raw_query)
|
|
178
|
+
structural = 1.0 if any(p in path_lower for p in self._structural_patterns) else 0.0
|
|
179
|
+
|
|
180
|
+
# recency: caller must set f.relevance_score > 0 before calling this method
|
|
181
|
+
# to use it as a recency hint; otherwise defaults to 0.
|
|
182
|
+
recency = 0.0
|
|
183
|
+
|
|
184
|
+
return (
|
|
185
|
+
self.weights["lexical_overlap"] * lexical
|
|
186
|
+
+ self.weights["direct_reference"] * direct
|
|
187
|
+
+ self.weights["symbol_mention"] * symbol
|
|
188
|
+
+ self.weights["recency"] * recency
|
|
189
|
+
+ self.weights["structural_importance"] * structural
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
@staticmethod
|
|
193
|
+
def _lexical_overlap(content: str, query_tokens: set[str]) -> float:
|
|
194
|
+
if not query_tokens:
|
|
195
|
+
return 0.0
|
|
196
|
+
matches = sum(1 for t in query_tokens if t in content)
|
|
197
|
+
return matches / len(query_tokens)
|
|
198
|
+
|
|
199
|
+
@staticmethod
|
|
200
|
+
def _symbol_mention(content: str, query: str) -> float:
|
|
201
|
+
"""Detect if any identifier from content appears in the query."""
|
|
202
|
+
import re
|
|
203
|
+
identifiers = set(re.findall(r"\b[a-zA-Z_][a-zA-Z0-9_]{3,}\b", content))
|
|
204
|
+
query_lower = query.lower()
|
|
205
|
+
for ident in identifiers:
|
|
206
|
+
if ident.lower() in query_lower:
|
|
207
|
+
return 1.0
|
|
208
|
+
return 0.0
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
# ---------------------------------------------------------------------------
|
|
212
|
+
# Snippet truncator
|
|
213
|
+
# ---------------------------------------------------------------------------
|
|
214
|
+
|
|
215
|
+
class SnippetTruncator:
|
|
216
|
+
"""Truncates large file snippets according to the active token policy."""
|
|
217
|
+
|
|
218
|
+
def __init__(self, policy: TokenPolicy) -> None:
|
|
219
|
+
self.policy = policy
|
|
220
|
+
|
|
221
|
+
def truncate(self, f: FileEntry) -> FileEntry:
|
|
222
|
+
lines = f.content.splitlines()
|
|
223
|
+
if len(lines) <= self.policy.snippet_max_lines:
|
|
224
|
+
return f
|
|
225
|
+
|
|
226
|
+
head = lines[: self.policy.snippet_head_lines]
|
|
227
|
+
tail = lines[-self.policy.snippet_tail_lines :]
|
|
228
|
+
omitted = len(lines) - self.policy.snippet_head_lines - self.policy.snippet_tail_lines
|
|
229
|
+
gap = f"# ... [{omitted} lines omitted — request full file if needed] ..."
|
|
230
|
+
f.content = "\n".join(head + [gap] + tail)
|
|
231
|
+
f.truncated = True
|
|
232
|
+
f.lines_omitted = omitted
|
|
233
|
+
return f
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
# ---------------------------------------------------------------------------
|
|
237
|
+
# Context assembler
|
|
238
|
+
# ---------------------------------------------------------------------------
|
|
239
|
+
|
|
240
|
+
class ContextAssembler:
|
|
241
|
+
"""Builds the final context block string from its components."""
|
|
242
|
+
|
|
243
|
+
def build(
|
|
244
|
+
self,
|
|
245
|
+
project_state: str,
|
|
246
|
+
retained_files: list[FileEntry],
|
|
247
|
+
conversation_state: str,
|
|
248
|
+
total_files: int,
|
|
249
|
+
policy: TokenPolicy,
|
|
250
|
+
) -> str:
|
|
251
|
+
parts: list[str] = []
|
|
252
|
+
|
|
253
|
+
parts.append("## Project State")
|
|
254
|
+
parts.append(project_state.strip() or "(not provided)")
|
|
255
|
+
|
|
256
|
+
header = f"## Relevant Files ({len(retained_files)} of {total_files} retained; threshold: {policy.relevance_drop_threshold})"
|
|
257
|
+
parts.append(header)
|
|
258
|
+
for f in retained_files:
|
|
259
|
+
ext = Path(f.path).suffix.lstrip(".") or "text"
|
|
260
|
+
score_tag = f"(score: {f.relevance_score:.2f})"
|
|
261
|
+
parts.append(f"\n### {f.path} {score_tag}")
|
|
262
|
+
parts.append(f"```{ext}")
|
|
263
|
+
parts.append(f.content)
|
|
264
|
+
parts.append("```")
|
|
265
|
+
|
|
266
|
+
parts.append("## Conversation State")
|
|
267
|
+
parts.append(conversation_state.strip() or "Session start.")
|
|
268
|
+
|
|
269
|
+
return "\n\n".join(parts)
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
# ---------------------------------------------------------------------------
|
|
273
|
+
# Provider adapters (stubs — no live HTTP calls)
|
|
274
|
+
# ---------------------------------------------------------------------------
|
|
275
|
+
|
|
276
|
+
class ProviderAdapter(ABC):
|
|
277
|
+
"""
|
|
278
|
+
Base class for LLM provider adapters.
|
|
279
|
+
Subclass this to integrate with a specific provider's API.
|
|
280
|
+
"""
|
|
281
|
+
|
|
282
|
+
@abstractmethod
|
|
283
|
+
def format_messages(self, system_prompt: str, context: str, user_query: str) -> Any:
|
|
284
|
+
"""Convert context + query into the provider's message format."""
|
|
285
|
+
|
|
286
|
+
@abstractmethod
|
|
287
|
+
def send(self, messages: Any, output_style: str) -> str:
|
|
288
|
+
"""
|
|
289
|
+
Send messages to the provider and return the completion text.
|
|
290
|
+
Implement with httpx or the provider's SDK.
|
|
291
|
+
Raises NotImplementedError in this skeleton.
|
|
292
|
+
"""
|
|
293
|
+
raise NotImplementedError("Implement send() in your ProviderAdapter subclass.")
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
class ClaudeAdapter(ProviderAdapter):
|
|
297
|
+
"""
|
|
298
|
+
Adapter for Anthropic Claude (claude-sonnet-4-6, claude-opus-4-7, etc.).
|
|
299
|
+
See platforms/claude.md for full integration guide.
|
|
300
|
+
"""
|
|
301
|
+
|
|
302
|
+
def __init__(self, model: str = "claude-sonnet-4-6", api_key: str = "") -> None:
|
|
303
|
+
self.model = model
|
|
304
|
+
self.api_key = api_key
|
|
305
|
+
|
|
306
|
+
def format_messages(self, system_prompt: str, context: str, user_query: str) -> dict:
|
|
307
|
+
return {
|
|
308
|
+
"model": self.model,
|
|
309
|
+
"system": system_prompt,
|
|
310
|
+
"messages": [
|
|
311
|
+
{"role": "user", "content": f"{context}\n\n---\n\n{user_query}"},
|
|
312
|
+
],
|
|
313
|
+
"max_tokens": 4096,
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
def send(self, messages: dict, output_style: str) -> str:
|
|
317
|
+
raise NotImplementedError(
|
|
318
|
+
"ClaudeAdapter.send() is a stub. "
|
|
319
|
+
"Install anthropic SDK and implement: "
|
|
320
|
+
"client = anthropic.Anthropic(api_key=self.api_key); "
|
|
321
|
+
"return client.messages.create(**messages).content[0].text"
|
|
322
|
+
)
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
class OpenAIAdapter(ProviderAdapter):
|
|
326
|
+
"""
|
|
327
|
+
Adapter for OpenAI (gpt-4o, gpt-4-turbo, etc.).
|
|
328
|
+
See platforms/openai.md for full integration guide.
|
|
329
|
+
"""
|
|
330
|
+
|
|
331
|
+
def __init__(self, model: str = "gpt-4o", api_key: str = "") -> None:
|
|
332
|
+
self.model = model
|
|
333
|
+
self.api_key = api_key
|
|
334
|
+
|
|
335
|
+
def format_messages(self, system_prompt: str, context: str, user_query: str) -> list[dict]:
|
|
336
|
+
return [
|
|
337
|
+
{"role": "system", "content": system_prompt},
|
|
338
|
+
{"role": "user", "content": f"{context}\n\n---\n\n{user_query}"},
|
|
339
|
+
]
|
|
340
|
+
|
|
341
|
+
def send(self, messages: list[dict], output_style: str) -> str:
|
|
342
|
+
raise NotImplementedError(
|
|
343
|
+
"OpenAIAdapter.send() is a stub. "
|
|
344
|
+
"Install openai SDK and implement: "
|
|
345
|
+
"client = openai.OpenAI(api_key=self.api_key); "
|
|
346
|
+
"return client.chat.completions.create(model=self.model, messages=messages).choices[0].message.content"
|
|
347
|
+
)
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
class GenericAdapter(ProviderAdapter):
|
|
351
|
+
"""
|
|
352
|
+
Adapter for any OpenAI-compatible API endpoint.
|
|
353
|
+
Set base_url to the provider's chat completions URL.
|
|
354
|
+
"""
|
|
355
|
+
|
|
356
|
+
def __init__(self, base_url: str, model: str, api_key: str = "") -> None:
|
|
357
|
+
self.base_url = base_url
|
|
358
|
+
self.model = model
|
|
359
|
+
self.api_key = api_key
|
|
360
|
+
|
|
361
|
+
def format_messages(self, system_prompt: str, context: str, user_query: str) -> list[dict]:
|
|
362
|
+
return [
|
|
363
|
+
{"role": "system", "content": system_prompt},
|
|
364
|
+
{"role": "user", "content": f"{context}\n\n---\n\n{user_query}"},
|
|
365
|
+
]
|
|
366
|
+
|
|
367
|
+
def send(self, messages: list[dict], output_style: str) -> str:
|
|
368
|
+
raise NotImplementedError(
|
|
369
|
+
"GenericAdapter.send() is a stub. "
|
|
370
|
+
"Implement using httpx: "
|
|
371
|
+
"httpx.post(self.base_url, json={'model': self.model, 'messages': messages}, "
|
|
372
|
+
"headers={'Authorization': f'Bearer {self.api_key}'})"
|
|
373
|
+
)
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
# ---------------------------------------------------------------------------
|
|
377
|
+
# Main compressor
|
|
378
|
+
# ---------------------------------------------------------------------------
|
|
379
|
+
|
|
380
|
+
class Compressor:
|
|
381
|
+
"""
|
|
382
|
+
Orchestrates the full compression pipeline:
|
|
383
|
+
1. Relevance filtering
|
|
384
|
+
2. Snippet truncation
|
|
385
|
+
3. Context assembly
|
|
386
|
+
4. Report generation
|
|
387
|
+
"""
|
|
388
|
+
|
|
389
|
+
def __init__(self, policy: TokenPolicy) -> None:
|
|
390
|
+
self.policy = policy
|
|
391
|
+
self._filter = RelevanceFilter(policy)
|
|
392
|
+
self._truncator = SnippetTruncator(policy)
|
|
393
|
+
self._assembler = ContextAssembler()
|
|
394
|
+
|
|
395
|
+
def compress(
|
|
396
|
+
self,
|
|
397
|
+
context: ContextBlock,
|
|
398
|
+
) -> tuple[str, CompressionReport]:
|
|
399
|
+
"""
|
|
400
|
+
Apply the full compression pipeline to a ContextBlock.
|
|
401
|
+
Returns (compressed_context_string, report).
|
|
402
|
+
"""
|
|
403
|
+
report = CompressionReport()
|
|
404
|
+
|
|
405
|
+
# Score and filter files
|
|
406
|
+
scored = self._filter.score_files(context.files, context.active_query)
|
|
407
|
+
retained, dropped = self._filter.select(scored)
|
|
408
|
+
report.files_dropped = [(f.path, f.relevance_score) for f in dropped]
|
|
409
|
+
report.files_retained = len(retained)
|
|
410
|
+
|
|
411
|
+
# Truncate large files
|
|
412
|
+
truncated_files = [self._truncator.truncate(f) for f in retained]
|
|
413
|
+
report.files_truncated = [
|
|
414
|
+
(f.path, f.lines_omitted) for f in truncated_files if f.truncated
|
|
415
|
+
]
|
|
416
|
+
|
|
417
|
+
# Estimate token reduction from truncation only (not from file drops).
|
|
418
|
+
# Comparing retained files before vs after truncation gives an accurate
|
|
419
|
+
# measure of how much the snippet truncator saved.
|
|
420
|
+
pre_trunc_chars = sum(len(f.content) for f in retained)
|
|
421
|
+
post_trunc_chars = sum(len(f.content) for f in truncated_files)
|
|
422
|
+
if pre_trunc_chars > 0:
|
|
423
|
+
report.estimated_token_reduction_pct = round(
|
|
424
|
+
(1 - post_trunc_chars / pre_trunc_chars) * 100, 1
|
|
425
|
+
)
|
|
426
|
+
|
|
427
|
+
compressed = self._assembler.build(
|
|
428
|
+
project_state=context.project_state,
|
|
429
|
+
retained_files=truncated_files,
|
|
430
|
+
conversation_state=context.conversation_state,
|
|
431
|
+
total_files=len(context.files),
|
|
432
|
+
policy=self.policy,
|
|
433
|
+
)
|
|
434
|
+
|
|
435
|
+
return compressed, report
|
|
436
|
+
|
|
437
|
+
|
|
438
|
+
# ---------------------------------------------------------------------------
|
|
439
|
+
# CLI dry-run entry point
|
|
440
|
+
# ---------------------------------------------------------------------------
|
|
441
|
+
|
|
442
|
+
def _build_arg_parser() -> argparse.ArgumentParser:
|
|
443
|
+
p = argparse.ArgumentParser(
|
|
444
|
+
description="Dry-run the compressor: shows what would be sent to an LLM without making any API calls."
|
|
445
|
+
)
|
|
446
|
+
p.add_argument("--dry-run", action="store_true", help="Required to run without API calls.")
|
|
447
|
+
p.add_argument("--policy", default="defaults", help="Workflow name from token_policies.yaml.")
|
|
448
|
+
p.add_argument("--context-file", help="Path to a text file containing a raw context block.")
|
|
449
|
+
p.add_argument("--query", default="", help="The active user query.")
|
|
450
|
+
p.add_argument("--config", default="config/token_policies.yaml", help="Path to token_policies.yaml.")
|
|
451
|
+
return p
|
|
452
|
+
|
|
453
|
+
|
|
454
|
+
def main() -> None:
|
|
455
|
+
parser = _build_arg_parser()
|
|
456
|
+
args = parser.parse_args()
|
|
457
|
+
|
|
458
|
+
if not args.dry_run:
|
|
459
|
+
print("Pass --dry-run to use this script without API calls.", file=sys.stderr)
|
|
460
|
+
sys.exit(1)
|
|
461
|
+
|
|
462
|
+
loader = PolicyLoader(args.config)
|
|
463
|
+
loader.load()
|
|
464
|
+
policy = loader.get_workflow_policy(args.policy)
|
|
465
|
+
|
|
466
|
+
raw_context = ""
|
|
467
|
+
if args.context_file:
|
|
468
|
+
raw_context = Path(args.context_file).read_text(encoding="utf-8")
|
|
469
|
+
|
|
470
|
+
# Build a minimal ContextBlock from the raw text (single pseudo-file)
|
|
471
|
+
demo_file = FileEntry(path="<context>", content=raw_context)
|
|
472
|
+
block = ContextBlock(
|
|
473
|
+
project_state="(loaded from --context-file)",
|
|
474
|
+
files=[demo_file],
|
|
475
|
+
conversation_state="Session start.",
|
|
476
|
+
active_query=args.query,
|
|
477
|
+
)
|
|
478
|
+
|
|
479
|
+
compressor = Compressor(policy)
|
|
480
|
+
compressed, report = compressor.compress(block)
|
|
481
|
+
|
|
482
|
+
print("=== COMPRESSED CONTEXT START ===")
|
|
483
|
+
print(compressed)
|
|
484
|
+
print("=== COMPRESSED CONTEXT END ===\n")
|
|
485
|
+
|
|
486
|
+
print("=== COMPRESSION REPORT ===")
|
|
487
|
+
print(json.dumps(
|
|
488
|
+
{
|
|
489
|
+
"workflow": policy.workflow,
|
|
490
|
+
"output_style": policy.output_style,
|
|
491
|
+
"files_retained": report.files_retained,
|
|
492
|
+
"files_dropped": report.files_dropped,
|
|
493
|
+
"files_truncated": report.files_truncated,
|
|
494
|
+
"estimated_token_reduction_pct": report.estimated_token_reduction_pct,
|
|
495
|
+
"warnings": report.warnings,
|
|
496
|
+
},
|
|
497
|
+
indent=2,
|
|
498
|
+
))
|
|
499
|
+
|
|
500
|
+
|
|
501
|
+
if __name__ == "__main__":
|
|
502
|
+
main()
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# Output Styles
|
|
2
|
+
|
|
3
|
+
## Token Cost Tier Definitions
|
|
4
|
+
|
|
5
|
+
Used in skill cards and [Companion] block next-step options to set user expectations:
|
|
6
|
+
|
|
7
|
+
| Tier | Approx. output tokens | Typical context needed |
|
|
8
|
+
|------|----------------------|------------------------|
|
|
9
|
+
| **Low** | < 500 | Conversation only — no file reads required |
|
|
10
|
+
| **Medium** | 500–1,500 | 3–5 source files needed; normal session size |
|
|
11
|
+
| **High** | > 1,500 | Full repo scan or large diff; use `project_summary.py` first |
|
|
12
|
+
|
|
13
|
+
These tiers are intentionally coarse — they indicate session preparation cost, not response quality.
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## Purpose
|
|
18
|
+
|
|
19
|
+
Named verbosity presets that control the format and length of agent responses. Each style is referenced by name in `config/token_policies.yaml` and in agent spec default settings.
|
|
20
|
+
|
|
21
|
+
Full style definitions (rules, token limits, format templates) live in `config/token_policies.yaml` under `output_styles:`. This file is the usage guide — not the source of truth.
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## Style Quick Reference
|
|
26
|
+
|
|
27
|
+
| Style key | Best for |
|
|
28
|
+
|---|---|
|
|
29
|
+
| `concise_bullets` | UX critique, status updates, compression reports |
|
|
30
|
+
| `standard` | General Q&A, workflow explanations, mixed responses |
|
|
31
|
+
| `detailed_with_code` | Implementation, refactoring, test generation |
|
|
32
|
+
| `design_brief` | Architecture outputs, API contracts, ADRs |
|
|
33
|
+
| `review_checklist` | Code reviews, layout reviews, security audits |
|
|
34
|
+
| `qa_brief` | Phase 0 task framing — numbered question list |
|
|
35
|
+
| `requirements_spec` | Phase 1 requirements gathering |
|
|
36
|
+
| `solution_design` | Phase 2 solution design output |
|
|
37
|
+
| `implementation_slice` | Phase 3 incremental delivery |
|
|
38
|
+
| `critique_summary` | Phase 4 severity-rated review |
|
|
39
|
+
| `handoff_package` | Phase 5 packaging and handoff |
|
|
40
|
+
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
## How to Apply a Style
|
|
44
|
+
|
|
45
|
+
**In conversational use** — append to the agent's system prompt or first message:
|
|
46
|
+
|
|
47
|
+
```
|
|
48
|
+
Output style: <style_name>
|
|
49
|
+
Max response length: <max_response_tokens> tokens
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
**In programmatic use** — via `compressor.py`:
|
|
53
|
+
|
|
54
|
+
```python
|
|
55
|
+
policy = token_policies.get_workflow_policy("ux_critique")
|
|
56
|
+
style = policy.output_style # → "concise_bullets"
|
|
57
|
+
style_config = token_policies.get_output_style(style)
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
The user can override for any turn: "Use `<style_name>` for this response."
|
|
61
|
+
|
|
62
|
+
---
|
|
63
|
+
|
|
64
|
+
## Style Selection by Workflow
|
|
65
|
+
|
|
66
|
+
| Workflow | Phase | Default style |
|
|
67
|
+
|---|---|---|
|
|
68
|
+
| `code_review` | — | `review_checklist` |
|
|
69
|
+
| `feature_implementation` | — | `detailed_with_code` |
|
|
70
|
+
| `feature_design` | — | `design_brief` |
|
|
71
|
+
| `ui_improvement` | — | `design_brief` |
|
|
72
|
+
| `ux_critique` | — | `concise_bullets` |
|
|
73
|
+
| `refactoring` | — | `detailed_with_code` |
|
|
74
|
+
| `test_generation` | — | `detailed_with_code` |
|
|
75
|
+
| `generic_project_lifecycle` | task_framing | `qa_brief` |
|
|
76
|
+
| `generic_project_lifecycle` | requirements | `requirements_spec` |
|
|
77
|
+
| `generic_project_lifecycle` | solution_design | `solution_design` |
|
|
78
|
+
| `generic_project_lifecycle` | implementation | `implementation_slice` |
|
|
79
|
+
| `generic_project_lifecycle` | review_refinement | `critique_summary` |
|
|
80
|
+
| `generic_project_lifecycle` | handoff | `handoff_package` |
|
|
Binary file
|
|
Binary file
|
|
Binary file
|