ouroboros-ai 0.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.
Potentially problematic release.
This version of ouroboros-ai might be problematic. Click here for more details.
- ouroboros/__init__.py +15 -0
- ouroboros/__main__.py +9 -0
- ouroboros/bigbang/__init__.py +39 -0
- ouroboros/bigbang/ambiguity.py +464 -0
- ouroboros/bigbang/interview.py +530 -0
- ouroboros/bigbang/seed_generator.py +610 -0
- ouroboros/cli/__init__.py +9 -0
- ouroboros/cli/commands/__init__.py +7 -0
- ouroboros/cli/commands/config.py +79 -0
- ouroboros/cli/commands/init.py +425 -0
- ouroboros/cli/commands/run.py +201 -0
- ouroboros/cli/commands/status.py +85 -0
- ouroboros/cli/formatters/__init__.py +31 -0
- ouroboros/cli/formatters/panels.py +157 -0
- ouroboros/cli/formatters/progress.py +112 -0
- ouroboros/cli/formatters/tables.py +166 -0
- ouroboros/cli/main.py +60 -0
- ouroboros/config/__init__.py +81 -0
- ouroboros/config/loader.py +292 -0
- ouroboros/config/models.py +332 -0
- ouroboros/core/__init__.py +62 -0
- ouroboros/core/ac_tree.py +401 -0
- ouroboros/core/context.py +472 -0
- ouroboros/core/errors.py +246 -0
- ouroboros/core/seed.py +212 -0
- ouroboros/core/types.py +205 -0
- ouroboros/evaluation/__init__.py +110 -0
- ouroboros/evaluation/consensus.py +350 -0
- ouroboros/evaluation/mechanical.py +351 -0
- ouroboros/evaluation/models.py +235 -0
- ouroboros/evaluation/pipeline.py +286 -0
- ouroboros/evaluation/semantic.py +302 -0
- ouroboros/evaluation/trigger.py +278 -0
- ouroboros/events/__init__.py +5 -0
- ouroboros/events/base.py +80 -0
- ouroboros/events/decomposition.py +153 -0
- ouroboros/events/evaluation.py +248 -0
- ouroboros/execution/__init__.py +44 -0
- ouroboros/execution/atomicity.py +451 -0
- ouroboros/execution/decomposition.py +481 -0
- ouroboros/execution/double_diamond.py +1386 -0
- ouroboros/execution/subagent.py +275 -0
- ouroboros/observability/__init__.py +63 -0
- ouroboros/observability/drift.py +383 -0
- ouroboros/observability/logging.py +504 -0
- ouroboros/observability/retrospective.py +338 -0
- ouroboros/orchestrator/__init__.py +78 -0
- ouroboros/orchestrator/adapter.py +391 -0
- ouroboros/orchestrator/events.py +278 -0
- ouroboros/orchestrator/runner.py +597 -0
- ouroboros/orchestrator/session.py +486 -0
- ouroboros/persistence/__init__.py +23 -0
- ouroboros/persistence/checkpoint.py +511 -0
- ouroboros/persistence/event_store.py +183 -0
- ouroboros/persistence/migrations/__init__.py +1 -0
- ouroboros/persistence/migrations/runner.py +100 -0
- ouroboros/persistence/migrations/scripts/001_initial.sql +20 -0
- ouroboros/persistence/schema.py +56 -0
- ouroboros/persistence/uow.py +230 -0
- ouroboros/providers/__init__.py +28 -0
- ouroboros/providers/base.py +133 -0
- ouroboros/providers/claude_code_adapter.py +212 -0
- ouroboros/providers/litellm_adapter.py +316 -0
- ouroboros/py.typed +0 -0
- ouroboros/resilience/__init__.py +67 -0
- ouroboros/resilience/lateral.py +595 -0
- ouroboros/resilience/stagnation.py +727 -0
- ouroboros/routing/__init__.py +60 -0
- ouroboros/routing/complexity.py +272 -0
- ouroboros/routing/downgrade.py +664 -0
- ouroboros/routing/escalation.py +340 -0
- ouroboros/routing/router.py +204 -0
- ouroboros/routing/tiers.py +247 -0
- ouroboros/secondary/__init__.py +40 -0
- ouroboros/secondary/scheduler.py +467 -0
- ouroboros/secondary/todo_registry.py +483 -0
- ouroboros_ai-0.1.0.dist-info/METADATA +607 -0
- ouroboros_ai-0.1.0.dist-info/RECORD +81 -0
- ouroboros_ai-0.1.0.dist-info/WHEEL +4 -0
- ouroboros_ai-0.1.0.dist-info/entry_points.txt +2 -0
- ouroboros_ai-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,472 @@
|
|
|
1
|
+
"""Context management and compression for Ouroboros.
|
|
2
|
+
|
|
3
|
+
This module provides context size monitoring, compression, and preservation
|
|
4
|
+
of critical information during long-running workflows.
|
|
5
|
+
|
|
6
|
+
Key features:
|
|
7
|
+
- Token counting using LiteLLM
|
|
8
|
+
- Automatic compression when context exceeds limits (100K tokens or 6 hours)
|
|
9
|
+
- Preservation of critical info (seed, current AC, recent history, key facts)
|
|
10
|
+
- Fallback to aggressive truncation on compression failures
|
|
11
|
+
- Comprehensive observability with before/after metrics
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from dataclasses import dataclass, field
|
|
15
|
+
from datetime import UTC, datetime
|
|
16
|
+
from typing import Any
|
|
17
|
+
|
|
18
|
+
import litellm
|
|
19
|
+
import structlog
|
|
20
|
+
|
|
21
|
+
from ouroboros.core.errors import ProviderError
|
|
22
|
+
from ouroboros.core.types import Result
|
|
23
|
+
from ouroboros.providers.base import CompletionConfig, Message, MessageRole
|
|
24
|
+
from ouroboros.providers.litellm_adapter import LiteLLMAdapter
|
|
25
|
+
|
|
26
|
+
log = structlog.get_logger()
|
|
27
|
+
|
|
28
|
+
# Context compression thresholds
|
|
29
|
+
MAX_TOKENS = 100_000 # NFR7: Max 100,000 tokens
|
|
30
|
+
MAX_AGE_HOURS = 6 # Trigger compression after 6 hours
|
|
31
|
+
RECENT_HISTORY_COUNT = 3 # Preserve last 3 iterations
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@dataclass(frozen=True, slots=True)
|
|
35
|
+
class ContextMetrics:
|
|
36
|
+
"""Metrics about context size and age.
|
|
37
|
+
|
|
38
|
+
Attributes:
|
|
39
|
+
token_count: Current token count.
|
|
40
|
+
age_hours: Age of context in hours.
|
|
41
|
+
created_at: When the context was created.
|
|
42
|
+
needs_compression: Whether compression is needed.
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
token_count: int
|
|
46
|
+
age_hours: float
|
|
47
|
+
created_at: datetime
|
|
48
|
+
needs_compression: bool
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@dataclass(frozen=True, slots=True)
|
|
52
|
+
class CompressionResult:
|
|
53
|
+
"""Result of a compression operation.
|
|
54
|
+
|
|
55
|
+
Attributes:
|
|
56
|
+
compressed_context: The compressed context data.
|
|
57
|
+
before_tokens: Token count before compression.
|
|
58
|
+
after_tokens: Token count after compression.
|
|
59
|
+
compression_ratio: Ratio of compression (after/before).
|
|
60
|
+
method: Compression method used ('llm' or 'truncate').
|
|
61
|
+
"""
|
|
62
|
+
|
|
63
|
+
compressed_context: dict[str, Any]
|
|
64
|
+
before_tokens: int
|
|
65
|
+
after_tokens: int
|
|
66
|
+
compression_ratio: float
|
|
67
|
+
method: str
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
@dataclass(slots=True)
|
|
71
|
+
class WorkflowContext:
|
|
72
|
+
"""Context for a running workflow.
|
|
73
|
+
|
|
74
|
+
Attributes:
|
|
75
|
+
seed_summary: Initial seed/goal for the workflow.
|
|
76
|
+
current_ac: Current acceptance criteria being worked on.
|
|
77
|
+
history: Historical iterations/events.
|
|
78
|
+
key_facts: Important facts extracted from history.
|
|
79
|
+
created_at: When this context was created.
|
|
80
|
+
metadata: Additional metadata.
|
|
81
|
+
"""
|
|
82
|
+
|
|
83
|
+
seed_summary: str
|
|
84
|
+
current_ac: str
|
|
85
|
+
history: list[dict[str, Any]] = field(default_factory=list)
|
|
86
|
+
key_facts: list[str] = field(default_factory=list)
|
|
87
|
+
created_at: datetime = field(default_factory=lambda: datetime.now(UTC))
|
|
88
|
+
metadata: dict[str, Any] = field(default_factory=dict)
|
|
89
|
+
|
|
90
|
+
def to_dict(self) -> dict[str, Any]:
|
|
91
|
+
"""Convert context to dictionary format.
|
|
92
|
+
|
|
93
|
+
Returns:
|
|
94
|
+
Dictionary representation of the context.
|
|
95
|
+
"""
|
|
96
|
+
return {
|
|
97
|
+
"seed_summary": self.seed_summary,
|
|
98
|
+
"current_ac": self.current_ac,
|
|
99
|
+
"history": self.history,
|
|
100
|
+
"key_facts": self.key_facts,
|
|
101
|
+
"created_at": self.created_at.isoformat(),
|
|
102
|
+
"metadata": self.metadata,
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
@classmethod
|
|
106
|
+
def from_dict(cls, data: dict[str, Any]) -> WorkflowContext:
|
|
107
|
+
"""Create context from dictionary.
|
|
108
|
+
|
|
109
|
+
Args:
|
|
110
|
+
data: Dictionary representation of context.
|
|
111
|
+
|
|
112
|
+
Returns:
|
|
113
|
+
WorkflowContext instance.
|
|
114
|
+
"""
|
|
115
|
+
created_at_str = data.get("created_at")
|
|
116
|
+
created_at = (
|
|
117
|
+
datetime.fromisoformat(created_at_str)
|
|
118
|
+
if created_at_str
|
|
119
|
+
else datetime.now(UTC)
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
return cls(
|
|
123
|
+
seed_summary=data.get("seed_summary", ""),
|
|
124
|
+
current_ac=data.get("current_ac", ""),
|
|
125
|
+
history=data.get("history", []),
|
|
126
|
+
key_facts=data.get("key_facts", []),
|
|
127
|
+
created_at=created_at,
|
|
128
|
+
metadata=data.get("metadata", {}),
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
@dataclass(frozen=True, slots=True)
|
|
133
|
+
class FilteredContext:
|
|
134
|
+
"""Filtered context for SubAgent isolation.
|
|
135
|
+
|
|
136
|
+
This contains only the information a SubAgent needs, isolating it
|
|
137
|
+
from the full workflow context. The frozen dataclass ensures immutability,
|
|
138
|
+
preventing SubAgent actions from modifying the main context (AC 3).
|
|
139
|
+
|
|
140
|
+
Attributes:
|
|
141
|
+
current_ac: The acceptance criteria for this SubAgent.
|
|
142
|
+
relevant_facts: Facts relevant to this SubAgent's task (key_facts).
|
|
143
|
+
parent_summary: Summary of parent context (seed_summary).
|
|
144
|
+
recent_history: Recent history items (last RECENT_HISTORY_COUNT).
|
|
145
|
+
"""
|
|
146
|
+
|
|
147
|
+
current_ac: str
|
|
148
|
+
relevant_facts: list[str]
|
|
149
|
+
parent_summary: str = ""
|
|
150
|
+
recent_history: list[dict[str, Any]] = field(default_factory=list)
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def count_tokens(text: str, model: str = "gpt-4") -> int:
|
|
154
|
+
"""Count tokens in text using LiteLLM's token counter.
|
|
155
|
+
|
|
156
|
+
Args:
|
|
157
|
+
text: The text to count tokens for.
|
|
158
|
+
model: The model to use for tokenization. Default 'gpt-4'.
|
|
159
|
+
|
|
160
|
+
Returns:
|
|
161
|
+
The number of tokens in the text.
|
|
162
|
+
"""
|
|
163
|
+
try:
|
|
164
|
+
return litellm.token_counter(model=model, text=text)
|
|
165
|
+
except Exception as e:
|
|
166
|
+
# Fallback to rough estimation if token counting fails
|
|
167
|
+
log.warning(
|
|
168
|
+
"context.token_count.failed",
|
|
169
|
+
error=str(e),
|
|
170
|
+
using_fallback=True,
|
|
171
|
+
)
|
|
172
|
+
# Rough estimate: ~4 characters per token
|
|
173
|
+
return len(text) // 4
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def count_context_tokens(context: WorkflowContext, model: str = "gpt-4") -> int:
|
|
177
|
+
"""Count total tokens in a workflow context.
|
|
178
|
+
|
|
179
|
+
Args:
|
|
180
|
+
context: The workflow context to measure.
|
|
181
|
+
model: The model to use for tokenization. Default 'gpt-4'.
|
|
182
|
+
|
|
183
|
+
Returns:
|
|
184
|
+
Total token count for the context.
|
|
185
|
+
"""
|
|
186
|
+
# Convert context to a string representation
|
|
187
|
+
context_str = f"""
|
|
188
|
+
Seed: {context.seed_summary}
|
|
189
|
+
|
|
190
|
+
Current AC: {context.current_ac}
|
|
191
|
+
|
|
192
|
+
Key Facts:
|
|
193
|
+
{chr(10).join(f"- {fact}" for fact in context.key_facts)}
|
|
194
|
+
|
|
195
|
+
History ({len(context.history)} items):
|
|
196
|
+
{chr(10).join(str(item) for item in context.history)}
|
|
197
|
+
"""
|
|
198
|
+
return count_tokens(context_str, model)
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def get_context_metrics(context: WorkflowContext, model: str = "gpt-4") -> ContextMetrics:
|
|
202
|
+
"""Get metrics about a workflow context.
|
|
203
|
+
|
|
204
|
+
Args:
|
|
205
|
+
context: The workflow context to analyze.
|
|
206
|
+
model: The model to use for tokenization. Default 'gpt-4'.
|
|
207
|
+
|
|
208
|
+
Returns:
|
|
209
|
+
ContextMetrics with size and age information.
|
|
210
|
+
"""
|
|
211
|
+
token_count = count_context_tokens(context, model)
|
|
212
|
+
age = datetime.now(UTC) - context.created_at
|
|
213
|
+
age_hours = age.total_seconds() / 3600
|
|
214
|
+
|
|
215
|
+
needs_compression = token_count > MAX_TOKENS or age_hours > MAX_AGE_HOURS
|
|
216
|
+
|
|
217
|
+
return ContextMetrics(
|
|
218
|
+
token_count=token_count,
|
|
219
|
+
age_hours=age_hours,
|
|
220
|
+
created_at=context.created_at,
|
|
221
|
+
needs_compression=needs_compression,
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
async def compress_context_with_llm(
|
|
226
|
+
context: WorkflowContext,
|
|
227
|
+
llm_adapter: LiteLLMAdapter,
|
|
228
|
+
model: str = "gpt-4",
|
|
229
|
+
) -> Result[str, ProviderError]:
|
|
230
|
+
"""Compress context using LLM summarization.
|
|
231
|
+
|
|
232
|
+
Args:
|
|
233
|
+
context: The workflow context to compress.
|
|
234
|
+
llm_adapter: LLM adapter for making completion requests.
|
|
235
|
+
model: The model to use for summarization.
|
|
236
|
+
|
|
237
|
+
Returns:
|
|
238
|
+
Result containing the compressed summary or a ProviderError.
|
|
239
|
+
"""
|
|
240
|
+
# Build summarization prompt
|
|
241
|
+
# Exclude recent history items from summarization
|
|
242
|
+
items_to_summarize = (
|
|
243
|
+
context.history[:-RECENT_HISTORY_COUNT] if len(context.history) > RECENT_HISTORY_COUNT else []
|
|
244
|
+
)
|
|
245
|
+
history_text = "\n".join(f"{i + 1}. {item}" for i, item in enumerate(items_to_summarize))
|
|
246
|
+
|
|
247
|
+
prompt = f"""Summarize the following workflow history, preserving key facts and decisions.
|
|
248
|
+
Focus on what was accomplished and what important information should be retained.
|
|
249
|
+
|
|
250
|
+
SEED/GOAL:
|
|
251
|
+
{context.seed_summary}
|
|
252
|
+
|
|
253
|
+
HISTORY TO SUMMARIZE:
|
|
254
|
+
{history_text}
|
|
255
|
+
|
|
256
|
+
CURRENT KEY FACTS:
|
|
257
|
+
{chr(10).join(f"- {fact}" for fact in context.key_facts)}
|
|
258
|
+
|
|
259
|
+
Provide a concise summary that captures:
|
|
260
|
+
1. Key accomplishments and milestones
|
|
261
|
+
2. Important decisions made
|
|
262
|
+
3. Critical facts that must be preserved
|
|
263
|
+
4. Any blockers or issues encountered
|
|
264
|
+
|
|
265
|
+
Keep the summary focused and factual. Omit unnecessary details."""
|
|
266
|
+
|
|
267
|
+
messages = [Message(role=MessageRole.USER, content=prompt)]
|
|
268
|
+
config = CompletionConfig(
|
|
269
|
+
model=model,
|
|
270
|
+
temperature=0.3, # Lower temperature for more consistent summaries
|
|
271
|
+
max_tokens=2000, # Limit summary size
|
|
272
|
+
)
|
|
273
|
+
|
|
274
|
+
log.debug(
|
|
275
|
+
"context.compression.llm.started",
|
|
276
|
+
model=model,
|
|
277
|
+
history_items=len(context.history),
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
result = await llm_adapter.complete(messages, config)
|
|
281
|
+
|
|
282
|
+
if result.is_ok:
|
|
283
|
+
log.debug(
|
|
284
|
+
"context.compression.llm.completed",
|
|
285
|
+
summary_tokens=result.value.usage.completion_tokens,
|
|
286
|
+
)
|
|
287
|
+
return Result.ok(result.value.content)
|
|
288
|
+
else:
|
|
289
|
+
log.warning(
|
|
290
|
+
"context.compression.llm.failed",
|
|
291
|
+
error=str(result.error),
|
|
292
|
+
)
|
|
293
|
+
return Result.err(result.error)
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
async def compress_context(
|
|
297
|
+
context: WorkflowContext,
|
|
298
|
+
llm_adapter: LiteLLMAdapter,
|
|
299
|
+
model: str = "gpt-4",
|
|
300
|
+
) -> Result[CompressionResult, str]:
|
|
301
|
+
"""Compress a workflow context when it exceeds limits.
|
|
302
|
+
|
|
303
|
+
This function implements the full compression logic:
|
|
304
|
+
1. Try LLM-based summarization of history
|
|
305
|
+
2. Preserve critical info (seed, current AC, recent history, key facts)
|
|
306
|
+
3. Fall back to aggressive truncation on LLM failure
|
|
307
|
+
4. Log compression events with metrics
|
|
308
|
+
|
|
309
|
+
Args:
|
|
310
|
+
context: The workflow context to compress.
|
|
311
|
+
llm_adapter: LLM adapter for making completion requests.
|
|
312
|
+
model: The model to use for summarization and token counting.
|
|
313
|
+
|
|
314
|
+
Returns:
|
|
315
|
+
Result containing CompressionResult or error message.
|
|
316
|
+
"""
|
|
317
|
+
before_tokens = count_context_tokens(context, model)
|
|
318
|
+
|
|
319
|
+
log.info(
|
|
320
|
+
"context.compression.started",
|
|
321
|
+
before_tokens=before_tokens,
|
|
322
|
+
history_items=len(context.history),
|
|
323
|
+
age_hours=get_context_metrics(context, model).age_hours,
|
|
324
|
+
)
|
|
325
|
+
|
|
326
|
+
# Try LLM-based compression first
|
|
327
|
+
summary_result = await compress_context_with_llm(context, llm_adapter, model)
|
|
328
|
+
|
|
329
|
+
if summary_result.is_ok:
|
|
330
|
+
# LLM compression succeeded
|
|
331
|
+
summary = summary_result.value
|
|
332
|
+
|
|
333
|
+
# Build compressed context preserving critical info
|
|
334
|
+
compressed_context = {
|
|
335
|
+
"seed_summary": context.seed_summary, # Always preserve
|
|
336
|
+
"current_ac": context.current_ac, # Always preserve
|
|
337
|
+
"history_summary": summary, # Compressed history
|
|
338
|
+
"recent_history": context.history[-RECENT_HISTORY_COUNT:], # Last 3 items
|
|
339
|
+
"key_facts": context.key_facts, # Always preserve
|
|
340
|
+
"metadata": {
|
|
341
|
+
**context.metadata,
|
|
342
|
+
"compression_timestamp": datetime.now(UTC).isoformat(),
|
|
343
|
+
"original_history_count": len(context.history),
|
|
344
|
+
},
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
# Count tokens in compressed context
|
|
348
|
+
compressed_str = f"""
|
|
349
|
+
Seed: {compressed_context['seed_summary']}
|
|
350
|
+
Current AC: {compressed_context['current_ac']}
|
|
351
|
+
Summary: {compressed_context['history_summary']}
|
|
352
|
+
Recent: {compressed_context['recent_history']}
|
|
353
|
+
Facts: {compressed_context['key_facts']}
|
|
354
|
+
"""
|
|
355
|
+
after_tokens = count_tokens(compressed_str, model)
|
|
356
|
+
compression_ratio = after_tokens / before_tokens if before_tokens > 0 else 1.0
|
|
357
|
+
|
|
358
|
+
log.info(
|
|
359
|
+
"context.compression.completed",
|
|
360
|
+
method="llm",
|
|
361
|
+
before_tokens=before_tokens,
|
|
362
|
+
after_tokens=after_tokens,
|
|
363
|
+
compression_ratio=compression_ratio,
|
|
364
|
+
reduction_percent=int((1 - compression_ratio) * 100),
|
|
365
|
+
)
|
|
366
|
+
|
|
367
|
+
return Result.ok(
|
|
368
|
+
CompressionResult(
|
|
369
|
+
compressed_context=compressed_context,
|
|
370
|
+
before_tokens=before_tokens,
|
|
371
|
+
after_tokens=after_tokens,
|
|
372
|
+
compression_ratio=compression_ratio,
|
|
373
|
+
method="llm",
|
|
374
|
+
)
|
|
375
|
+
)
|
|
376
|
+
else:
|
|
377
|
+
# LLM compression failed - fall back to aggressive truncation
|
|
378
|
+
log.warning(
|
|
379
|
+
"context.compression.llm_failed.using_fallback",
|
|
380
|
+
error=str(summary_result.error),
|
|
381
|
+
)
|
|
382
|
+
|
|
383
|
+
# Fallback: Keep only seed + current AC (most critical info)
|
|
384
|
+
compressed_context = {
|
|
385
|
+
"seed_summary": context.seed_summary,
|
|
386
|
+
"current_ac": context.current_ac,
|
|
387
|
+
"key_facts": context.key_facts[:5], # Keep top 5 facts only
|
|
388
|
+
"metadata": {
|
|
389
|
+
**context.metadata,
|
|
390
|
+
"compression_timestamp": datetime.now(UTC).isoformat(),
|
|
391
|
+
"compression_method": "aggressive_truncation",
|
|
392
|
+
"compression_reason": "llm_failure",
|
|
393
|
+
"original_history_count": len(context.history),
|
|
394
|
+
},
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
compressed_str = f"""
|
|
398
|
+
Seed: {compressed_context['seed_summary']}
|
|
399
|
+
Current AC: {compressed_context['current_ac']}
|
|
400
|
+
Facts: {compressed_context['key_facts']}
|
|
401
|
+
"""
|
|
402
|
+
after_tokens = count_tokens(compressed_str, model)
|
|
403
|
+
compression_ratio = after_tokens / before_tokens if before_tokens > 0 else 1.0
|
|
404
|
+
|
|
405
|
+
log.warning(
|
|
406
|
+
"context.compression.completed.fallback",
|
|
407
|
+
method="truncate",
|
|
408
|
+
before_tokens=before_tokens,
|
|
409
|
+
after_tokens=after_tokens,
|
|
410
|
+
compression_ratio=compression_ratio,
|
|
411
|
+
reduction_percent=int((1 - compression_ratio) * 100),
|
|
412
|
+
)
|
|
413
|
+
|
|
414
|
+
return Result.ok(
|
|
415
|
+
CompressionResult(
|
|
416
|
+
compressed_context=compressed_context,
|
|
417
|
+
before_tokens=before_tokens,
|
|
418
|
+
after_tokens=after_tokens,
|
|
419
|
+
compression_ratio=compression_ratio,
|
|
420
|
+
method="truncate",
|
|
421
|
+
)
|
|
422
|
+
)
|
|
423
|
+
|
|
424
|
+
|
|
425
|
+
def create_filtered_context(
|
|
426
|
+
context: WorkflowContext,
|
|
427
|
+
subagent_ac: str,
|
|
428
|
+
relevant_fact_keywords: list[str] | None = None,
|
|
429
|
+
) -> FilteredContext:
|
|
430
|
+
"""Create a filtered context for SubAgent isolation.
|
|
431
|
+
|
|
432
|
+
Creates an immutable FilteredContext containing only the information
|
|
433
|
+
a SubAgent needs to execute its task, isolating it from the full
|
|
434
|
+
workflow context. This prevents context pollution (AC 3).
|
|
435
|
+
|
|
436
|
+
The filtered context includes (per AC 2):
|
|
437
|
+
- current_ac: The SubAgent's specific acceptance criterion
|
|
438
|
+
- relevant_facts: Filtered key_facts (all or by keywords)
|
|
439
|
+
- parent_summary: Summary including seed_summary
|
|
440
|
+
- recent_history: Last RECENT_HISTORY_COUNT history items
|
|
441
|
+
|
|
442
|
+
Args:
|
|
443
|
+
context: The full workflow context.
|
|
444
|
+
subagent_ac: The acceptance criteria for this SubAgent.
|
|
445
|
+
relevant_fact_keywords: Optional keywords to filter relevant facts.
|
|
446
|
+
|
|
447
|
+
Returns:
|
|
448
|
+
FilteredContext with only information relevant to the SubAgent.
|
|
449
|
+
"""
|
|
450
|
+
# Filter facts by keywords if provided
|
|
451
|
+
if relevant_fact_keywords:
|
|
452
|
+
relevant_facts = [
|
|
453
|
+
fact
|
|
454
|
+
for fact in context.key_facts
|
|
455
|
+
if any(keyword.lower() in fact.lower() for keyword in relevant_fact_keywords)
|
|
456
|
+
]
|
|
457
|
+
else:
|
|
458
|
+
# If no keywords, include all facts (SubAgent might need them)
|
|
459
|
+
relevant_facts = list(context.key_facts) # Copy to ensure isolation
|
|
460
|
+
|
|
461
|
+
# Create parent summary from seed
|
|
462
|
+
parent_summary = f"Parent Goal: {context.seed_summary}"
|
|
463
|
+
|
|
464
|
+
# Extract recent history (last RECENT_HISTORY_COUNT items)
|
|
465
|
+
recent_history = list(context.history[-RECENT_HISTORY_COUNT:])
|
|
466
|
+
|
|
467
|
+
return FilteredContext(
|
|
468
|
+
current_ac=subagent_ac,
|
|
469
|
+
relevant_facts=relevant_facts,
|
|
470
|
+
parent_summary=parent_summary,
|
|
471
|
+
recent_history=recent_history,
|
|
472
|
+
)
|