algo-cli-runtime 0.14.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.
- algo_cli/__init__.py +3 -0
- algo_cli/__main__.py +7 -0
- algo_cli/_internal/__init__.py +12 -0
- algo_cli/_internal/policy_chain.py +259 -0
- algo_cli/action_registry.py +1047 -0
- algo_cli/agent_blocks.py +550 -0
- algo_cli/agent_pipeline.py +1457 -0
- algo_cli/agent_threads.py +308 -0
- algo_cli/animations.py +316 -0
- algo_cli/cache_admission.py +209 -0
- algo_cli/capability_mask.py +66 -0
- algo_cli/chat_protocol.py +116 -0
- algo_cli/chatgpt_auth.py +510 -0
- algo_cli/chatgpt_client.py +657 -0
- algo_cli/code_rag.py +479 -0
- algo_cli/config.py +651 -0
- algo_cli/context_budget.py +679 -0
- algo_cli/credential_helpers.py +315 -0
- algo_cli/deliberation.py +29 -0
- algo_cli/display.py +1470 -0
- algo_cli/evals/__init__.py +21 -0
- algo_cli/evals/algorithm_effectiveness.py +560 -0
- algo_cli/evals/competitive_harness_rating.py +702 -0
- algo_cli/evals/cot_quality.py +220 -0
- algo_cli/evals/harness_retrieval_benchmark.py +401 -0
- algo_cli/evals/performance_regression.py +136 -0
- algo_cli/evals/scorecard_grading.py +308 -0
- algo_cli/evals/session_distribution.py +84 -0
- algo_cli/execution_guardrails.py +806 -0
- algo_cli/extensions_manifest.py +84 -0
- algo_cli/git_evidence.py +227 -0
- algo_cli/google_workspace.py +407 -0
- algo_cli/google_workspace_auth.py +523 -0
- algo_cli/harness.py +2587 -0
- algo_cli/identity.py +557 -0
- algo_cli/index_compute_lab.py +228 -0
- algo_cli/inference_harness.py +70 -0
- algo_cli/intelligence/__init__.py +1103 -0
- algo_cli/intelligence/acrobat_config.py +307 -0
- algo_cli/intelligence/acrobat_manifests.py +338 -0
- algo_cli/intelligence/acrobat_models.py +195 -0
- algo_cli/intelligence/acrobat_pipeline.py +295 -0
- algo_cli/intelligence/acrobat_runtime.py +302 -0
- algo_cli/intelligence/acrobat_security.py +261 -0
- algo_cli/intelligence/acrobat_workflows.py +226 -0
- algo_cli/intelligence/actionability.py +165 -0
- algo_cli/intelligence/adversarial_audit.py +136 -0
- algo_cli/intelligence/agent_arena.py +92 -0
- algo_cli/intelligence/agent_benchmark.py +236 -0
- algo_cli/intelligence/agent_runtime.py +171 -0
- algo_cli/intelligence/agents_as_tools.py +70 -0
- algo_cli/intelligence/artifact_binding.py +80 -0
- algo_cli/intelligence/autonomous_engineer.py +1976 -0
- algo_cli/intelligence/backpressure.py +99 -0
- algo_cli/intelligence/bloom_filter.py +186 -0
- algo_cli/intelligence/bonferroni.py +66 -0
- algo_cli/intelligence/boundary_compaction.py +98 -0
- algo_cli/intelligence/catalog_verifier.py +172 -0
- algo_cli/intelligence/cavecrew.py +118 -0
- algo_cli/intelligence/changelog.py +176 -0
- algo_cli/intelligence/checkpoint_resume.py +92 -0
- algo_cli/intelligence/circuit_breaker.py +88 -0
- algo_cli/intelligence/clarification_gate.py +101 -0
- algo_cli/intelligence/code_graph.py +180 -0
- algo_cli/intelligence/coderank.py +97 -0
- algo_cli/intelligence/consistent_hash.py +150 -0
- algo_cli/intelligence/consortium_synthesis.py +139 -0
- algo_cli/intelligence/construction/__init__.py +241 -0
- algo_cli/intelligence/construction/common.py +273 -0
- algo_cli/intelligence/construction/documents.py +496 -0
- algo_cli/intelligence/construction/labor_units.py +1395 -0
- algo_cli/intelligence/construction/payments.py +470 -0
- algo_cli/intelligence/construction/risk.py +784 -0
- algo_cli/intelligence/content_extractor.py +132 -0
- algo_cli/intelligence/context_adaptive.py +102 -0
- algo_cli/intelligence/context_ops.py +95 -0
- algo_cli/intelligence/count_min.py +145 -0
- algo_cli/intelligence/cow_state.py +103 -0
- algo_cli/intelligence/critic_loop.py +119 -0
- algo_cli/intelligence/cross_source.py +113 -0
- algo_cli/intelligence/daemon_mode.py +99 -0
- algo_cli/intelligence/dag_orchestration.py +151 -0
- algo_cli/intelligence/deep_research.py +155 -0
- algo_cli/intelligence/degenerate_detector.py +78 -0
- algo_cli/intelligence/delta_report.py +92 -0
- algo_cli/intelligence/discovery_event_log.py +92 -0
- algo_cli/intelligence/document_ingest.py +298 -0
- algo_cli/intelligence/dual_layer_validate.py +151 -0
- algo_cli/intelligence/echo_fidelity.py +73 -0
- algo_cli/intelligence/ema_tuning.py +104 -0
- algo_cli/intelligence/event_log.py +92 -0
- algo_cli/intelligence/evidence_graph.py +114 -0
- algo_cli/intelligence/extension_host.py +162 -0
- algo_cli/intelligence/extension_manifest.py +115 -0
- algo_cli/intelligence/falsification_suite.py +178 -0
- algo_cli/intelligence/finance/__init__.py +169 -0
- algo_cli/intelligence/finance/anomalies.py +135 -0
- algo_cli/intelligence/finance/ap_ar.py +351 -0
- algo_cli/intelligence/finance/cash.py +162 -0
- algo_cli/intelligence/finance/close.py +332 -0
- algo_cli/intelligence/finance/common.py +244 -0
- algo_cli/intelligence/finance/construction.py +135 -0
- algo_cli/intelligence/finance/controls.py +172 -0
- algo_cli/intelligence/finance/evidence.py +119 -0
- algo_cli/intelligence/finance/exceptions.py +157 -0
- algo_cli/intelligence/finance/reconciliations.py +254 -0
- algo_cli/intelligence/finance/revenue.py +109 -0
- algo_cli/intelligence/finance/tax.py +74 -0
- algo_cli/intelligence/finance/workpapers.py +111 -0
- algo_cli/intelligence/finding_record.py +120 -0
- algo_cli/intelligence/flow_dag.py +267 -0
- algo_cli/intelligence/gatherer.py +223 -0
- algo_cli/intelligence/golden_master.py +98 -0
- algo_cli/intelligence/graph_rag.py +195 -0
- algo_cli/intelligence/group_chat.py +143 -0
- algo_cli/intelligence/hash_dedup.py +145 -0
- algo_cli/intelligence/hyperloglog.py +128 -0
- algo_cli/intelligence/incremental_index.py +316 -0
- algo_cli/intelligence/index_store.py +16 -0
- algo_cli/intelligence/iteration_plan.py +133 -0
- algo_cli/intelligence/kernel_plugins.py +167 -0
- algo_cli/intelligence/lesson_catalog.py +135 -0
- algo_cli/intelligence/llm_fallback.py +169 -0
- algo_cli/intelligence/log2_histogram.py +267 -0
- algo_cli/intelligence/lsp_integration.py +147 -0
- algo_cli/intelligence/memory_evolution.py +117 -0
- algo_cli/intelligence/minhash_lsh.py +182 -0
- algo_cli/intelligence/multi_model_score.py +174 -0
- algo_cli/intelligence/multi_tier_grade.py +211 -0
- algo_cli/intelligence/negative_controls.py +113 -0
- algo_cli/intelligence/numeric_clamp.py +63 -0
- algo_cli/intelligence/occ_editor.py +66 -0
- algo_cli/intelligence/output_normalize.py +112 -0
- algo_cli/intelligence/parallel_delegation.py +98 -0
- algo_cli/intelligence/parallel_fanout.py +104 -0
- algo_cli/intelligence/permission_modes.py +105 -0
- algo_cli/intelligence/pre_push_gate.py +68 -0
- algo_cli/intelligence/prefetch.py +171 -0
- algo_cli/intelligence/process_framework.py +217 -0
- algo_cli/intelligence/project_graph.py +387 -0
- algo_cli/intelligence/query_expansion.py +146 -0
- algo_cli/intelligence/ralph_loop.py +117 -0
- algo_cli/intelligence/rate_limiter.py +153 -0
- algo_cli/intelligence/refactor_transaction.py +94 -0
- algo_cli/intelligence/research_workspace.py +108 -0
- algo_cli/intelligence/retraction_ledger.py +72 -0
- algo_cli/intelligence/saga_pattern.py +88 -0
- algo_cli/intelligence/session_fork.py +100 -0
- algo_cli/intelligence/shadow_editor.py +67 -0
- algo_cli/intelligence/shell_session.py +213 -0
- algo_cli/intelligence/source_registry.py +143 -0
- algo_cli/intelligence/spawn_scales.py +99 -0
- algo_cli/intelligence/stat_stability.py +104 -0
- algo_cli/intelligence/structural_validator.py +148 -0
- algo_cli/intelligence/subagent_spawner.py +111 -0
- algo_cli/intelligence/symmetric_verify.py +70 -0
- algo_cli/intelligence/task_classifier.py +129 -0
- algo_cli/intelligence/team_execution.py +122 -0
- algo_cli/intelligence/tiered_access.py +121 -0
- algo_cli/intelligence/utility_registry.py +159 -0
- algo_cli/intuition_engine.py +560 -0
- algo_cli/intuition_injector.py +82 -0
- algo_cli/kernels/__init__.py +5 -0
- algo_cli/kernels/manifest.py +763 -0
- algo_cli/main.py +3903 -0
- algo_cli/memory_candidates.py +541 -0
- algo_cli/memory_echo_veil.py +394 -0
- algo_cli/memory_runtime.py +112 -0
- algo_cli/model_info.py +548 -0
- algo_cli/model_profile.py +160 -0
- algo_cli/model_routing.py +74 -0
- algo_cli/oneshot.py +331 -0
- algo_cli/perf_telemetry.py +389 -0
- algo_cli/plugins.py +245 -0
- algo_cli/private_event_store.py +654 -0
- algo_cli/quantization/__init__.py +24 -0
- algo_cli/quantization/lloyd_max.py +98 -0
- algo_cli/quantization/turbo_quant.py +308 -0
- algo_cli/reasoning/__init__.py +46 -0
- algo_cli/reasoning/combinatorial.py +356 -0
- algo_cli/reasoning/graph_of_thought.py +297 -0
- algo_cli/reasoning/mcts.py +220 -0
- algo_cli/reasoning/neuro_symbolic.py +250 -0
- algo_cli/reasoning/react.py +246 -0
- algo_cli/reasoning/reflexion.py +225 -0
- algo_cli/reasoning/tree_of_thought.py +241 -0
- algo_cli/reasoning_bridge.py +150 -0
- algo_cli/reconciliation.py +284 -0
- algo_cli/reflex.py +385 -0
- algo_cli/resources/docs/ALGO.md +13958 -0
- algo_cli/resources/docs/algo-cli-algorithm-evidence-contract.md +60 -0
- algo_cli/resources/docs/algo-cli-execution-verification-contract.md +59 -0
- algo_cli/resources/docs/algo-cli-memory-lifecycle-contract.md +72 -0
- algo_cli/resources/docs/harness-extension-cleanup-recommendation.md +41 -0
- algo_cli/resources/docs/index-compute-lab-integration.md +32 -0
- algo_cli/resources/docs/inference-harness-loop-blueprint-2026-06.md +55 -0
- algo_cli/resources/docs/main-split-map.md +35 -0
- algo_cli/resources/docs/privacy-and-context.md +48 -0
- algo_cli/resources/docs/reflex-loop-v0.2.md +354 -0
- algo_cli/resources/skills/README.md +26 -0
- algo_cli/resources/skills/algo-cli.md +59 -0
- algo_cli/resources/skills/edit-file-precision.md +49 -0
- algo_cli/resources/skills/harness-search-first.md +47 -0
- algo_cli/resources/skills/memory-recall-ritual.md +51 -0
- algo_cli/resources/skills/qol-algorithms.md +224 -0
- algo_cli/resources/skills/smart-error-recovery.md +56 -0
- algo_cli/resources/skills/tool-selection-cheatsheet.md +65 -0
- algo_cli/retrieval_algorithms.py +127 -0
- algo_cli/runtime_qos.py +236 -0
- algo_cli/runtime_services.py +320 -0
- algo_cli/session_commands.py +95 -0
- algo_cli/session_mode.py +113 -0
- algo_cli/skills.py +430 -0
- algo_cli/slash_dispatch.py +1265 -0
- algo_cli/small_context.py +206 -0
- algo_cli/spawn_budget.py +89 -0
- algo_cli/task_ledger.py +84 -0
- algo_cli/task_router.py +197 -0
- algo_cli/tool_context.py +94 -0
- algo_cli/tool_contract.py +99 -0
- algo_cli/tool_policy.py +357 -0
- algo_cli/tool_runtime.py +647 -0
- algo_cli/tools.py +3056 -0
- algo_cli/url_scheme.py +174 -0
- algo_cli/verify.py +154 -0
- algo_cli/version_manifest.py +178 -0
- algo_cli/vision_screenshot_verify.py +76 -0
- algo_cli/workspace_resolver.py +68 -0
- algo_cli/x_account.py +209 -0
- algo_cli/xai_auth.py +374 -0
- algo_cli/xai_client.py +600 -0
- algo_cli_runtime-0.14.0.dist-info/METADATA +369 -0
- algo_cli_runtime-0.14.0.dist-info/RECORD +237 -0
- algo_cli_runtime-0.14.0.dist-info/WHEEL +4 -0
- algo_cli_runtime-0.14.0.dist-info/entry_points.txt +3 -0
- algo_cli_runtime-0.14.0.dist-info/licenses/LICENSE +21 -0
- ollama_cli/__init__.py +67 -0
|
@@ -0,0 +1,679 @@
|
|
|
1
|
+
"""Context window estimates, pruning, compaction, and system prompt assembly."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
import sys
|
|
8
|
+
import time
|
|
9
|
+
from dataclasses import dataclass
|
|
10
|
+
from typing import Any, Callable
|
|
11
|
+
|
|
12
|
+
from ollama import Client
|
|
13
|
+
|
|
14
|
+
from .config import Config, load_runtime_env
|
|
15
|
+
from . import harness
|
|
16
|
+
from . import identity
|
|
17
|
+
from . import model_info as _model_info_module
|
|
18
|
+
from . import reflex
|
|
19
|
+
from .chat_protocol import get_attr
|
|
20
|
+
from .display import json_sink
|
|
21
|
+
|
|
22
|
+
CONTEXT_COMPACT_THRESHOLD = 0.85
|
|
23
|
+
CONTEXT_KEEP_MESSAGES = 12
|
|
24
|
+
SMALL_CONTEXT_COMPACT_THRESHOLD = 0.70
|
|
25
|
+
SMALL_CONTEXT_KEEP_MESSAGES = 8
|
|
26
|
+
MEDIUM_CONTEXT_COMPACT_THRESHOLD = 0.78
|
|
27
|
+
MEDIUM_CONTEXT_KEEP_MESSAGES = 10
|
|
28
|
+
FOOTER_METRICS_FRESHNESS_SECONDS = 30.0
|
|
29
|
+
ATTEMPT_PROMPT_LIMIT = 24
|
|
30
|
+
OPTIONAL_CONTEXT_MIN_TOKENS = 96
|
|
31
|
+
OPTIONAL_CONTEXT_TRUNCATION_SUFFIX = "\n...[truncated by context budget]"
|
|
32
|
+
|
|
33
|
+
_SMALL_MODEL_THRESHOLD_B = 70.0
|
|
34
|
+
|
|
35
|
+
_CALIBRATION_BLOCK = (
|
|
36
|
+
"\n\n## Accuracy Constraints (small-model mode)\n"
|
|
37
|
+
"You are running as a compact model. Apply these rules strictly:\n"
|
|
38
|
+
"- Never invent file paths, function names, version numbers, URLs, or command flags.\n"
|
|
39
|
+
"- If you lack clear evidence for a specific fact, say 'I'm not certain — let me check' "
|
|
40
|
+
"and use search_files, read_file, or harness_search to verify before stating it.\n"
|
|
41
|
+
"- Prefer 'I don't know' over a confident wrong answer.\n"
|
|
42
|
+
"- For code claims, verify with a tool call rather than relying on memory."
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
CONTEXT_USAGE_CACHE: tuple[tuple[Any, ...], int] | None = None
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def invalidate_context_usage_cache() -> None:
|
|
49
|
+
global CONTEXT_USAGE_CACHE
|
|
50
|
+
CONTEXT_USAGE_CACHE = None
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _context_usage_cache_key(
|
|
54
|
+
cfg: Config,
|
|
55
|
+
*,
|
|
56
|
+
lessons_fingerprint: int = 0,
|
|
57
|
+
model_info_fingerprint: int = 0,
|
|
58
|
+
user_message_fingerprint: int = 0,
|
|
59
|
+
) -> tuple[Any, ...]:
|
|
60
|
+
last_message = cfg.messages[-1] if cfg.messages else {}
|
|
61
|
+
return (
|
|
62
|
+
len(cfg.messages),
|
|
63
|
+
len(str(last_message.get("content", ""))),
|
|
64
|
+
len(str(last_message.get("thinking", ""))),
|
|
65
|
+
len(json.dumps(last_message.get("tool_calls", []), ensure_ascii=False, default=str)),
|
|
66
|
+
len(cfg.session_summary),
|
|
67
|
+
len(cfg.attempt_ledger),
|
|
68
|
+
len(cfg.memories),
|
|
69
|
+
cfg.num_ctx,
|
|
70
|
+
cfg.system,
|
|
71
|
+
identity.identity_mtime_key(),
|
|
72
|
+
lessons_fingerprint,
|
|
73
|
+
model_info_fingerprint,
|
|
74
|
+
user_message_fingerprint,
|
|
75
|
+
cfg.session_mode,
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def estimate_text_tokens(text: str) -> int:
|
|
80
|
+
if not text:
|
|
81
|
+
return 0
|
|
82
|
+
return max(1, (len(text) + 3) // 4)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def estimate_message_tokens(message: dict[str, Any]) -> int:
|
|
86
|
+
total = 12
|
|
87
|
+
total += estimate_text_tokens(str(message.get("role", "")))
|
|
88
|
+
total += estimate_text_tokens(str(message.get("content", "")))
|
|
89
|
+
total += estimate_text_tokens(str(message.get("thinking", "")))
|
|
90
|
+
total += estimate_text_tokens(json.dumps(message.get("tool_calls", []), ensure_ascii=False, default=str))
|
|
91
|
+
total += estimate_text_tokens(str(message.get("tool_name", "")))
|
|
92
|
+
return total
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
@dataclass(frozen=True)
|
|
96
|
+
class OptionalContextBlock:
|
|
97
|
+
name: str
|
|
98
|
+
title: str
|
|
99
|
+
body: str
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def context_compaction_policy(model_info: dict[str, Any] | None = None) -> tuple[float, int]:
|
|
103
|
+
"""Return (threshold, keep_messages) for the active model/window."""
|
|
104
|
+
info = model_info or {}
|
|
105
|
+
size_b = _model_info_module.parameter_size_billions(info)
|
|
106
|
+
native_ctx = _model_info_module.get_context_length(info)
|
|
107
|
+
if (size_b is not None and size_b <= 9.0) or (native_ctx is not None and native_ctx <= 8192):
|
|
108
|
+
return SMALL_CONTEXT_COMPACT_THRESHOLD, SMALL_CONTEXT_KEEP_MESSAGES
|
|
109
|
+
if size_b is not None and size_b <= 32.0:
|
|
110
|
+
return MEDIUM_CONTEXT_COMPACT_THRESHOLD, MEDIUM_CONTEXT_KEEP_MESSAGES
|
|
111
|
+
return CONTEXT_COMPACT_THRESHOLD, CONTEXT_KEEP_MESSAGES
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def context_response_reserve(runtime_cap: int, model_info: dict[str, Any] | None = None) -> int:
|
|
115
|
+
"""Reserve room for the assistant reply and tool-call metadata."""
|
|
116
|
+
cap = max(1, int(runtime_cap or 1))
|
|
117
|
+
size_b = _model_info_module.parameter_size_billions(model_info or {})
|
|
118
|
+
if cap <= 4096 or (size_b is not None and size_b <= 9.0):
|
|
119
|
+
reserve = max(384, cap // 6)
|
|
120
|
+
elif size_b is not None and size_b <= 32.0:
|
|
121
|
+
reserve = max(768, cap // 8)
|
|
122
|
+
else:
|
|
123
|
+
reserve = max(1024, cap // 10)
|
|
124
|
+
return min(reserve, max(1, cap // 3))
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def render_optional_context_block(block: OptionalContextBlock) -> str:
|
|
128
|
+
body = (block.body or "").strip()
|
|
129
|
+
if not body:
|
|
130
|
+
return ""
|
|
131
|
+
title = (block.title or "").strip()
|
|
132
|
+
return f"## {title}\n{body}" if title else body
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def _truncate_to_token_budget(text: str, token_budget: int) -> str:
|
|
136
|
+
if token_budget < OPTIONAL_CONTEXT_MIN_TOKENS:
|
|
137
|
+
return ""
|
|
138
|
+
if estimate_text_tokens(text) <= token_budget:
|
|
139
|
+
return text
|
|
140
|
+
suffix_tokens = estimate_text_tokens(OPTIONAL_CONTEXT_TRUNCATION_SUFFIX)
|
|
141
|
+
char_limit = max(0, (token_budget - suffix_tokens) * 4)
|
|
142
|
+
if char_limit < 120:
|
|
143
|
+
return ""
|
|
144
|
+
candidate = text[:char_limit].rstrip() + OPTIONAL_CONTEXT_TRUNCATION_SUFFIX
|
|
145
|
+
while estimate_text_tokens(candidate) > token_budget and char_limit > 120:
|
|
146
|
+
char_limit -= 80
|
|
147
|
+
candidate = text[:char_limit].rstrip() + OPTIONAL_CONTEXT_TRUNCATION_SUFFIX
|
|
148
|
+
if estimate_text_tokens(candidate) > token_budget:
|
|
149
|
+
return ""
|
|
150
|
+
return candidate
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def fit_optional_context_blocks(
|
|
154
|
+
base_message: str,
|
|
155
|
+
blocks: list[OptionalContextBlock],
|
|
156
|
+
*,
|
|
157
|
+
base_used_tokens: int,
|
|
158
|
+
runtime_cap: int,
|
|
159
|
+
model_info: dict[str, Any] | None = None,
|
|
160
|
+
) -> tuple[str, list[str], list[str], int]:
|
|
161
|
+
"""Append optional context blocks only while the request stays within budget."""
|
|
162
|
+
budget = int(runtime_cap) - int(base_used_tokens) - context_response_reserve(runtime_cap, model_info)
|
|
163
|
+
budget = max(0, budget)
|
|
164
|
+
included: list[str] = []
|
|
165
|
+
omitted: list[str] = []
|
|
166
|
+
rendered_parts: list[str] = []
|
|
167
|
+
used_tokens = 0
|
|
168
|
+
for block in blocks:
|
|
169
|
+
rendered = render_optional_context_block(block)
|
|
170
|
+
if not rendered:
|
|
171
|
+
continue
|
|
172
|
+
cost = estimate_text_tokens("\n\n" + rendered)
|
|
173
|
+
if cost <= budget:
|
|
174
|
+
rendered_parts.append(rendered)
|
|
175
|
+
included.append(block.name)
|
|
176
|
+
budget -= cost
|
|
177
|
+
used_tokens += cost
|
|
178
|
+
continue
|
|
179
|
+
truncated = _truncate_to_token_budget(rendered, budget)
|
|
180
|
+
if truncated:
|
|
181
|
+
truncated_cost = estimate_text_tokens("\n\n" + truncated)
|
|
182
|
+
rendered_parts.append(truncated)
|
|
183
|
+
included.append(block.name)
|
|
184
|
+
budget = max(0, budget - truncated_cost)
|
|
185
|
+
used_tokens += truncated_cost
|
|
186
|
+
else:
|
|
187
|
+
omitted.append(block.name)
|
|
188
|
+
if not rendered_parts:
|
|
189
|
+
return base_message, included, omitted, used_tokens
|
|
190
|
+
return f"{base_message}\n\n" + "\n\n".join(rendered_parts), included, omitted, used_tokens
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def build_system_prompt(
|
|
194
|
+
cfg: Config,
|
|
195
|
+
*,
|
|
196
|
+
retrieved_lessons: list[str] | None = None,
|
|
197
|
+
active_model_info: dict[str, Any] | None = None,
|
|
198
|
+
user_message: str | None = None,
|
|
199
|
+
) -> str:
|
|
200
|
+
identity_block = identity.build_identity_block(retrieved_lessons=retrieved_lessons)
|
|
201
|
+
prompt = (identity_block + "\n\n" if identity_block else "") + cfg.system
|
|
202
|
+
load_runtime_env(override=True)
|
|
203
|
+
if _model_info_module.is_xai_model(cfg.model):
|
|
204
|
+
provider = "xAI Grok OAuth"
|
|
205
|
+
elif cfg.cloud and os.environ.get("OLLAMA_API_KEY", "").strip():
|
|
206
|
+
provider = "Ollama Cloud direct API"
|
|
207
|
+
elif _model_info_module.is_cloud_model_name(cfg.model):
|
|
208
|
+
provider = "local Ollama (cloud model via login)"
|
|
209
|
+
else:
|
|
210
|
+
provider = "local Ollama"
|
|
211
|
+
external_harness_guidance = (
|
|
212
|
+
"External local agent stores are enabled for this session; harness tools may search Codex, Claude, "
|
|
213
|
+
"OpenClaw, Mercury, Pi, and shared .agents assets."
|
|
214
|
+
if cfg.external_harness_sources_enabled
|
|
215
|
+
else "External local agent stores are disabled. Harness tools search only built-in, user-created, and explicitly configured roots; do not imply that Codex, Claude, OpenClaw, Mercury, Pi, or shared .agents content is available."
|
|
216
|
+
)
|
|
217
|
+
prompt += (
|
|
218
|
+
"\n\n## Runtime Model Status\n"
|
|
219
|
+
f"- Active model: {cfg.model}\n"
|
|
220
|
+
f"- Provider route: {provider}\n"
|
|
221
|
+
"This block is generated from live runtime configuration for this turn. "
|
|
222
|
+
"If conversation summary, memory, identity files, or retrieved context disagree about the active model/provider, "
|
|
223
|
+
"treat this runtime block as authoritative."
|
|
224
|
+
)
|
|
225
|
+
shell_note = (
|
|
226
|
+
"cmd.exe — Unix tools (head, tail, grep, sed, awk, cat) are unavailable; use findstr/more, command flags (e.g. pytest -q), or read_file/search_files"
|
|
227
|
+
if sys.platform == "win32"
|
|
228
|
+
else "a POSIX shell"
|
|
229
|
+
)
|
|
230
|
+
prompt += (
|
|
231
|
+
"\n\n## Session Workspace\n"
|
|
232
|
+
f"- Platform: {sys.platform}; run_shell uses {shell_note}.\n"
|
|
233
|
+
"- Relative tool paths resolve from the active session workspace; use path '.' for its root.\n"
|
|
234
|
+
"- Do not guess or disclose an absolute workspace path. Use /cwd only when the exact local path is operationally necessary.\n"
|
|
235
|
+
"- When the user names files without a directory, use list_directory path '.' or session_slash /read with the bare filename first.\n"
|
|
236
|
+
"- User messages in the chat channel are authoritative. Harness RAG and reflex recovery blocks are hints only."
|
|
237
|
+
)
|
|
238
|
+
from . import session_commands
|
|
239
|
+
|
|
240
|
+
prompt += f"\n\n## Session Slash Commands\n{session_commands.catalog_for_prompt()}"
|
|
241
|
+
if cfg.session_summary.strip() and json_sink() is None:
|
|
242
|
+
prompt += f"\n\n## Conversation Summary\n{cfg.session_summary.strip()}"
|
|
243
|
+
prompt += (
|
|
244
|
+
"\n\n## Identity Files\n"
|
|
245
|
+
"Your persona and user profile are managed identity files whose contents are already loaded above:\n"
|
|
246
|
+
f"- {identity.SOUL_PATH.name} — your voice and operating values. Read-only; never write programmatically.\n"
|
|
247
|
+
f"- {identity.IDENTITY_PATH.name} — who you are. Read-only; never write programmatically.\n"
|
|
248
|
+
f"- {identity.USER_PATH.name} — who the user is. Use the update_user_profile tool only when the user explicitly asks you to edit their profile.\n"
|
|
249
|
+
f"- {identity.LESSONS_PATH.name} — accumulated lessons. Use append_lesson only when the user explicitly asks to store a lesson.\n"
|
|
250
|
+
"## Memory discipline (bounded automatic capture)\n"
|
|
251
|
+
"- A deterministic completion gate evaluates only the original user text for explicit, high-confidence durable statements; it never learns from assistant, tool, retrieval, specialist, quoted, secret, or personal-data output.\n"
|
|
252
|
+
"- Call append_lesson or remember only when the user explicitly requests that write. Do not duplicate a statement merely because it may qualify for automatic capture.\n"
|
|
253
|
+
"- Automatic capture is bounded, deduplicated, and reviewable with /memories; inspect or toggle it with /memory-auto status|on|off.\n"
|
|
254
|
+
"- Long-term memories, lessons, harness RAG, and index-compute-lab graph blocks are navigation hints — verify with read_file or tools before acting.\n"
|
|
255
|
+
"- Never store secrets, credentials, private keys, tokens, or inferred sensitive personal data.\n"
|
|
256
|
+
"## Terminal efficiency\n"
|
|
257
|
+
"- Prefer one decisive tool call over several speculative ones when the target is already known.\n"
|
|
258
|
+
"- Do not narrate every tool call; summarize outcomes in plain language after work completes.\n"
|
|
259
|
+
"- index-compute-lab canonical for this product is concept:algo-cli (legacy ollama-cli / ollama-cli-concept names in graph output are retired).\n"
|
|
260
|
+
"The contents of all four files are already loaded into this system prompt above; do not read_file them just to see what they say. "
|
|
261
|
+
"When the user references 'my wiki', 'my notes', or asks you to learn from their knowledge base, note that the harness RAG layer already injected the most relevant entries into the Relevant Context section (if present); use harness_search/harness_read only for explicit deep dives the retrieval missed."
|
|
262
|
+
"\n\n## Local Harness Bridge\n"
|
|
263
|
+
f"{external_harness_guidance} Use available_actions, harness_search, harness_read, harness_stats, and harness_refresh. "
|
|
264
|
+
"These tools are read-only and should be preferred before broad filesystem scans when the user asks about skills, tools, prompts, memory, or wiki context."
|
|
265
|
+
"\n\n## index-compute-lab (knowledge graph)\n"
|
|
266
|
+
"When enabled, each user turn may include a ## Knowledge Graph (index-compute-lab) block: "
|
|
267
|
+
"ranked associations from the user's configured index-compute-lab sources. "
|
|
268
|
+
"Treat it like harness RAG — navigation and relationship hints, not proof that files exist. "
|
|
269
|
+
"Use query_knowledge_graph for ranked co-occurrence (not prose biographies), and use harness_search for supporting documents. "
|
|
270
|
+
"To persist a correction or contact before the next full reindex: write_knowledge_graph_note then harness_refresh. "
|
|
271
|
+
"Use reindex_knowledge_graph only when the user explicitly asks to rebuild configured graph sources."
|
|
272
|
+
"\n\n## Grok / xAI model compatibility\n"
|
|
273
|
+
"When the user asks whether a grok-* model works in this harness, do not scan the repo blindly: "
|
|
274
|
+
"tell them to run /model-check NAME (or run it yourself via session slash if available), "
|
|
275
|
+
"and search the currently enabled harness for Algo CLI xAI/X-account guidance; do not assume external record IDs exist. "
|
|
276
|
+
"Multi-agent grok models use xAI /v1/responses; other Grok models use /v1/chat/completions. "
|
|
277
|
+
"Auth is optional subscription OAuth only: the user must provide XAI_CLIENT_ID, and tokens live in "
|
|
278
|
+
"~/.algo_cli/xai_auth.json. Algo CLI bundles no xAI client identity."
|
|
279
|
+
"\n\n## Capability Awareness\n"
|
|
280
|
+
"When the user asks what you can do, what actions are available, what internal stats exist, or what tools/skills/memory/wiki you can access, call available_actions first."
|
|
281
|
+
"\n\n## PDF Handling\n"
|
|
282
|
+
"For PDF files, call read_pdf first. Do not claim Python is unavailable and do not improvise shell-based PDF extraction before trying read_pdf. "
|
|
283
|
+
"If read_pdf says the document is scanned or image-only, call render_pdf_pages next and then pass one returned PNG path to vision_describe or another OCR-capable path. "
|
|
284
|
+
"If a vision/OCR model returns unsupported-image or insufficient-memory errors, do not repeat the same model across more PDF pages or sibling PDFs without changing the model or approach."
|
|
285
|
+
)
|
|
286
|
+
prompt += (
|
|
287
|
+
"\n\n## Reflection Checkpoints\n"
|
|
288
|
+
f"Every {max(1, int(cfg.tool_think_every))} tool calls, pause to reassess the objective, what has been completed, what remains, "
|
|
289
|
+
"whether web research is needed, whether the user explicitly requested a memory write, and the next best action. "
|
|
290
|
+
"Treat any internal checkpoint note as a planning pause, not user-facing output. "
|
|
291
|
+
"After a checkpoint, continue with the next necessary tool call unless the user's task is actually complete."
|
|
292
|
+
)
|
|
293
|
+
# One-shot tool results already carry failures in the message history.
|
|
294
|
+
# Rebuilding the system prompt with a growing ledger invalidates Ollama's
|
|
295
|
+
# prefix/KV cache on every tool turn, so keep automation prompts stable.
|
|
296
|
+
if cfg.attempt_ledger and json_sink() is None:
|
|
297
|
+
ledger_lines = []
|
|
298
|
+
for item in cfg.attempt_ledger[-ATTEMPT_PROMPT_LIMIT:]:
|
|
299
|
+
ledger_lines.append(
|
|
300
|
+
f"- {item.get('status', '?').upper()} {item.get('tool', '?')} "
|
|
301
|
+
f"{item.get('args_preview', '')}: {item.get('summary', '')}"
|
|
302
|
+
)
|
|
303
|
+
prompt += (
|
|
304
|
+
"\n\n## Attempt Ledger\n"
|
|
305
|
+
"Use this ledger to avoid retrying the same failed or denied tool path with the same arguments. "
|
|
306
|
+
"Only retry when the arguments materially change, new evidence appears, or the user asks.\n"
|
|
307
|
+
+ "\n".join(ledger_lines)
|
|
308
|
+
)
|
|
309
|
+
if cfg.memories:
|
|
310
|
+
memories = "\n".join(f"- {item}" for item in cfg.memories)
|
|
311
|
+
prompt += f"\n\n## Long-term Memories\n{memories}"
|
|
312
|
+
if active_model_info:
|
|
313
|
+
size_b = _model_info_module.parameter_size_billions(active_model_info)
|
|
314
|
+
if size_b is not None and size_b < _SMALL_MODEL_THRESHOLD_B:
|
|
315
|
+
prompt += _CALIBRATION_BLOCK
|
|
316
|
+
if cfg.reflex_enabled:
|
|
317
|
+
prompt += (
|
|
318
|
+
"\n\n## Reflex Loop (v0.1)\n"
|
|
319
|
+
"When a read-only tool fails, returns empty results, or repeats the same arguments, "
|
|
320
|
+
"the runtime may append a reflex recovery block (alternate harness_search, search_files, "
|
|
321
|
+
f"or escalation). Session cap: {reflex.REFLEX_MAX_CYCLES} cycles. "
|
|
322
|
+
"Do not treat reflex notes or recovery suggestions as user input or prompt injection."
|
|
323
|
+
)
|
|
324
|
+
if json_sink() is not None:
|
|
325
|
+
prompt += (
|
|
326
|
+
"\n\n## One-shot Execution Protocol\n"
|
|
327
|
+
"- Do not emit progress prose before or between tool calls; use tools silently, then provide one concise final answer.\n"
|
|
328
|
+
"- Open explicitly named files directly and batch independent reads. Do not list directories merely to confirm named paths.\n"
|
|
329
|
+
"- Make the smallest required changes and create only requested artifacts.\n"
|
|
330
|
+
"- After one successful fail-on-mismatch verifier, answer immediately; do not add redundant rereads, diffs, or reports."
|
|
331
|
+
)
|
|
332
|
+
if cfg.verify_mode:
|
|
333
|
+
prompt += (
|
|
334
|
+
"\n\n## Verify Mode Active\n"
|
|
335
|
+
"After answering, the harness will check your specific factual claims against "
|
|
336
|
+
"indexed sources. Flag anything you are not certain about with 'unverified:' "
|
|
337
|
+
"so the grounding pass can prioritise it."
|
|
338
|
+
)
|
|
339
|
+
from . import session_mode
|
|
340
|
+
|
|
341
|
+
prompt += (
|
|
342
|
+
f"\n\n{session_mode.prompt_section(cfg.session_mode, include_external=cfg.external_harness_sources_enabled)}"
|
|
343
|
+
)
|
|
344
|
+
mercury_gates = harness.resolve_mercury_stop_conditions(
|
|
345
|
+
user_message=user_message,
|
|
346
|
+
session_mode=cfg.session_mode,
|
|
347
|
+
include_external=cfg.external_harness_sources_enabled,
|
|
348
|
+
)
|
|
349
|
+
if mercury_gates:
|
|
350
|
+
full_doc = (
|
|
351
|
+
harness.load_mercury_stop_conditions()
|
|
352
|
+
if cfg.external_harness_sources_enabled
|
|
353
|
+
else ""
|
|
354
|
+
)
|
|
355
|
+
is_full = mercury_gates == full_doc and bool(full_doc)
|
|
356
|
+
title = (
|
|
357
|
+
"Stop Conditions (Mercury harness — full gates)"
|
|
358
|
+
if is_full
|
|
359
|
+
else "Stop Conditions (Mercury harness — compact)"
|
|
360
|
+
)
|
|
361
|
+
prompt += (
|
|
362
|
+
f"\n\n## {title}\n"
|
|
363
|
+
"Apply full gates only for external send/post, financial commitments, destructive actions, "
|
|
364
|
+
"or unsourced consequential facts. For read-only file work, read live files before refusing.\n\n"
|
|
365
|
+
f"{mercury_gates}"
|
|
366
|
+
)
|
|
367
|
+
return prompt
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
def estimate_context_usage(
|
|
371
|
+
cfg: Config,
|
|
372
|
+
*,
|
|
373
|
+
prebuilt_system: str | None = None,
|
|
374
|
+
lessons_fingerprint: int = 0,
|
|
375
|
+
model_info_fingerprint: int = 0,
|
|
376
|
+
user_message_fingerprint: int = 0,
|
|
377
|
+
) -> int:
|
|
378
|
+
global CONTEXT_USAGE_CACHE
|
|
379
|
+
cache_key = _context_usage_cache_key(
|
|
380
|
+
cfg,
|
|
381
|
+
lessons_fingerprint=lessons_fingerprint,
|
|
382
|
+
model_info_fingerprint=model_info_fingerprint,
|
|
383
|
+
user_message_fingerprint=user_message_fingerprint,
|
|
384
|
+
)
|
|
385
|
+
if CONTEXT_USAGE_CACHE and CONTEXT_USAGE_CACHE[0] == cache_key:
|
|
386
|
+
return CONTEXT_USAGE_CACHE[1]
|
|
387
|
+
system_text = prebuilt_system if prebuilt_system is not None else build_system_prompt(cfg)
|
|
388
|
+
total = estimate_text_tokens(system_text)
|
|
389
|
+
for message in cfg.messages:
|
|
390
|
+
total += estimate_message_tokens(message)
|
|
391
|
+
CONTEXT_USAGE_CACHE = (cache_key, total)
|
|
392
|
+
return total
|
|
393
|
+
|
|
394
|
+
|
|
395
|
+
def estimate_usage_with_system_prompt(system_prompt: str, cfg: Config) -> int:
|
|
396
|
+
"""Token estimate for a fully-built system prompt plus current messages."""
|
|
397
|
+
total = estimate_text_tokens(system_prompt)
|
|
398
|
+
for message in cfg.messages:
|
|
399
|
+
total += estimate_message_tokens(message)
|
|
400
|
+
return total
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
def _last_chat_token_usage(runtime_status: dict[str, Any]) -> int | None:
|
|
404
|
+
metrics = runtime_status.get("last_metrics") or {}
|
|
405
|
+
if not isinstance(metrics, dict):
|
|
406
|
+
return None
|
|
407
|
+
timestamp = metrics.get("timestamp")
|
|
408
|
+
if not timestamp or (time.time() - float(timestamp)) > FOOTER_METRICS_FRESHNESS_SECONDS:
|
|
409
|
+
return None
|
|
410
|
+
try:
|
|
411
|
+
prompt = int(metrics.get("prompt_eval_count") or 0)
|
|
412
|
+
completion = int(metrics.get("eval_count") or 0)
|
|
413
|
+
except (TypeError, ValueError):
|
|
414
|
+
return None
|
|
415
|
+
total = prompt + completion
|
|
416
|
+
return total if total > 0 else None
|
|
417
|
+
|
|
418
|
+
|
|
419
|
+
def context_status(
|
|
420
|
+
cfg: Config,
|
|
421
|
+
*,
|
|
422
|
+
client: Any | None = None,
|
|
423
|
+
model_info: dict[str, Any] | None = None,
|
|
424
|
+
runtime_status: dict[str, Any] | None = None,
|
|
425
|
+
usage_override: int | None = None,
|
|
426
|
+
) -> tuple[int, int, int, int, int | None]:
|
|
427
|
+
"""Return (used, display_total, remaining, runtime_cap, native_ctx)."""
|
|
428
|
+
if runtime_status is None:
|
|
429
|
+
from . import main as _main
|
|
430
|
+
|
|
431
|
+
runtime_status = _main.RUNTIME_STATUS
|
|
432
|
+
if model_info is None:
|
|
433
|
+
model_info = runtime_status.get("model_info")
|
|
434
|
+
if not isinstance(model_info, dict) or not model_info:
|
|
435
|
+
model_info = _model_info_module.resolve_model_info(cfg, client)
|
|
436
|
+
|
|
437
|
+
runtime_cap, native_ctx = _model_info_module.effective_context_limits(cfg, model_info)
|
|
438
|
+
display_total = runtime_cap
|
|
439
|
+
display_total = max(int(display_total), 1)
|
|
440
|
+
|
|
441
|
+
estimated = usage_override if usage_override is not None else estimate_context_usage(cfg)
|
|
442
|
+
api_used = _last_chat_token_usage(runtime_status)
|
|
443
|
+
used = api_used if api_used is not None else estimated
|
|
444
|
+
if api_used is not None:
|
|
445
|
+
used = max(api_used, estimated)
|
|
446
|
+
used = min(max(int(used), 0), display_total)
|
|
447
|
+
remaining = max(display_total - used, 0)
|
|
448
|
+
return used, display_total, remaining, runtime_cap, native_ctx
|
|
449
|
+
|
|
450
|
+
|
|
451
|
+
def summarize_message_batch(
|
|
452
|
+
cfg: Config,
|
|
453
|
+
batch: list[dict[str, Any]],
|
|
454
|
+
fallback_client: Client | None = None,
|
|
455
|
+
*,
|
|
456
|
+
maintenance_client_fn: Callable[[Config, Client | None], tuple[Client, str]] | None = None,
|
|
457
|
+
) -> str:
|
|
458
|
+
if not batch:
|
|
459
|
+
return cfg.session_summary
|
|
460
|
+
source_lines = []
|
|
461
|
+
if cfg.session_summary.strip():
|
|
462
|
+
source_lines.append("CURRENT SUMMARY:")
|
|
463
|
+
source_lines.append(cfg.session_summary.strip())
|
|
464
|
+
source_lines.append("")
|
|
465
|
+
source_lines.append("MESSAGES TO COMPRESS:")
|
|
466
|
+
for item in batch:
|
|
467
|
+
role = item.get("role", "message")
|
|
468
|
+
content = item.get("content") or item.get("thinking") or ""
|
|
469
|
+
tool_name = item.get("tool_name")
|
|
470
|
+
if tool_name:
|
|
471
|
+
source_lines.append(f"- {role} [{tool_name}]: {content}")
|
|
472
|
+
else:
|
|
473
|
+
source_lines.append(f"- {role}: {content}")
|
|
474
|
+
prompt = "\n".join(source_lines)
|
|
475
|
+
system = (
|
|
476
|
+
"You compress conversation state for a terminal coding assistant.\n"
|
|
477
|
+
'Return JSON with a single key "summary" whose value is a concise continuation '
|
|
478
|
+
"with only stable facts, decisions, file paths, tool results, and unresolved tasks. "
|
|
479
|
+
"Omit filler and repeated content. Keep it under 500 words."
|
|
480
|
+
)
|
|
481
|
+
try:
|
|
482
|
+
if maintenance_client_fn is None:
|
|
483
|
+
from . import main as _main
|
|
484
|
+
|
|
485
|
+
maintenance_client_fn = _main.small_maintenance_client
|
|
486
|
+
summary_client, summary_model = maintenance_client_fn(cfg, fallback_client)
|
|
487
|
+
response = summary_client.chat(
|
|
488
|
+
model=summary_model,
|
|
489
|
+
messages=[
|
|
490
|
+
{"role": "system", "content": system},
|
|
491
|
+
{"role": "user", "content": prompt},
|
|
492
|
+
],
|
|
493
|
+
stream=False,
|
|
494
|
+
think=False,
|
|
495
|
+
format="json",
|
|
496
|
+
keep_alive=cfg.keep_alive,
|
|
497
|
+
options={"temperature": 0.1, "num_ctx": min(cfg.num_ctx, 4096), "num_predict": 600},
|
|
498
|
+
)
|
|
499
|
+
content = get_attr(get_attr(response, "message", {}), "content", "")
|
|
500
|
+
if content:
|
|
501
|
+
try:
|
|
502
|
+
return str(json.loads(content).get("summary", content)).strip()
|
|
503
|
+
except (json.JSONDecodeError, AttributeError):
|
|
504
|
+
return str(content).strip()
|
|
505
|
+
except Exception:
|
|
506
|
+
pass
|
|
507
|
+
fallback = []
|
|
508
|
+
if cfg.session_summary.strip():
|
|
509
|
+
fallback.append(cfg.session_summary.strip())
|
|
510
|
+
for item in batch[-4:]:
|
|
511
|
+
role = item.get("role", "message")
|
|
512
|
+
content = (item.get("content") or item.get("thinking") or "")[:240]
|
|
513
|
+
fallback.append(f"{role}: {content}")
|
|
514
|
+
return "\n".join(fallback).strip()
|
|
515
|
+
|
|
516
|
+
|
|
517
|
+
def _tool_call_id(call: Any) -> str | None:
|
|
518
|
+
if isinstance(call, dict):
|
|
519
|
+
return call.get("id") or (call.get("function") or {}).get("id")
|
|
520
|
+
return getattr(call, "id", None)
|
|
521
|
+
|
|
522
|
+
|
|
523
|
+
def _strip_tool_call_at(
|
|
524
|
+
messages: list[dict[str, Any]],
|
|
525
|
+
assistant_index: int,
|
|
526
|
+
*,
|
|
527
|
+
call_id: str | None = None,
|
|
528
|
+
) -> bool:
|
|
529
|
+
if assistant_index < 0 or assistant_index >= len(messages):
|
|
530
|
+
return False
|
|
531
|
+
prev = messages[assistant_index]
|
|
532
|
+
calls = list(prev.get("tool_calls") or [])
|
|
533
|
+
if not calls:
|
|
534
|
+
return False
|
|
535
|
+
if call_id:
|
|
536
|
+
filtered = [c for c in calls if _tool_call_id(c) != call_id]
|
|
537
|
+
if len(filtered) == len(calls):
|
|
538
|
+
return False
|
|
539
|
+
else:
|
|
540
|
+
filtered = calls[1:]
|
|
541
|
+
new_prev = dict(prev)
|
|
542
|
+
if filtered:
|
|
543
|
+
new_prev["tool_calls"] = filtered
|
|
544
|
+
else:
|
|
545
|
+
new_prev.pop("tool_calls", None)
|
|
546
|
+
messages[assistant_index] = new_prev
|
|
547
|
+
return True
|
|
548
|
+
|
|
549
|
+
|
|
550
|
+
def prune_stale_tool_messages(cfg: Config) -> int:
|
|
551
|
+
total = len(cfg.messages)
|
|
552
|
+
if total <= cfg.prune_after_messages:
|
|
553
|
+
return 0
|
|
554
|
+
keep_from = max(0, total - cfg.prune_keep_recent)
|
|
555
|
+
if keep_from == 0:
|
|
556
|
+
return 0
|
|
557
|
+
|
|
558
|
+
removed = 0
|
|
559
|
+
new_messages: list[dict[str, Any]] = []
|
|
560
|
+
call_id_to_assistant: dict[str, int] = {}
|
|
561
|
+
pending_assistants: list[int] = []
|
|
562
|
+
|
|
563
|
+
for index, message in enumerate(cfg.messages):
|
|
564
|
+
if index >= keep_from:
|
|
565
|
+
new_messages.append(message)
|
|
566
|
+
continue
|
|
567
|
+
role = message.get("role")
|
|
568
|
+
if role == "assistant":
|
|
569
|
+
tool_calls = message.get("tool_calls") or []
|
|
570
|
+
if tool_calls:
|
|
571
|
+
pending_assistants.append(len(new_messages))
|
|
572
|
+
for call in tool_calls:
|
|
573
|
+
cid = _tool_call_id(call)
|
|
574
|
+
if cid:
|
|
575
|
+
call_id_to_assistant[cid] = len(new_messages)
|
|
576
|
+
new_messages.append(message)
|
|
577
|
+
continue
|
|
578
|
+
if role != "tool":
|
|
579
|
+
new_messages.append(message)
|
|
580
|
+
continue
|
|
581
|
+
call_id = message.get("tool_call_id")
|
|
582
|
+
if call_id and call_id in call_id_to_assistant:
|
|
583
|
+
idx = call_id_to_assistant.pop(call_id)
|
|
584
|
+
_strip_tool_call_at(new_messages, idx, call_id=call_id)
|
|
585
|
+
elif not call_id:
|
|
586
|
+
while pending_assistants:
|
|
587
|
+
idx = pending_assistants[0]
|
|
588
|
+
if idx >= len(new_messages) or not (new_messages[idx].get("tool_calls") or []):
|
|
589
|
+
pending_assistants.pop(0)
|
|
590
|
+
continue
|
|
591
|
+
_strip_tool_call_at(new_messages, idx)
|
|
592
|
+
break
|
|
593
|
+
removed += 1
|
|
594
|
+
|
|
595
|
+
if removed:
|
|
596
|
+
cfg.messages = new_messages
|
|
597
|
+
from . import perf_telemetry
|
|
598
|
+
|
|
599
|
+
perf_telemetry.record_perf_event(
|
|
600
|
+
"prune",
|
|
601
|
+
removed=removed,
|
|
602
|
+
kept=len(new_messages),
|
|
603
|
+
threshold=cfg.prune_after_messages,
|
|
604
|
+
)
|
|
605
|
+
return removed
|
|
606
|
+
|
|
607
|
+
|
|
608
|
+
def _split_for_compaction(messages: list[dict[str, Any]], keep_count: int) -> tuple[list[dict[str, Any]], list[dict[str, Any]]]:
|
|
609
|
+
keep_from = max(0, len(messages) - keep_count)
|
|
610
|
+
while keep_from > 0 and messages[keep_from].get("role") == "tool":
|
|
611
|
+
keep_from -= 1
|
|
612
|
+
while keep_from > 0 and messages[keep_from].get("role") == "tool":
|
|
613
|
+
keep_from -= 1
|
|
614
|
+
return messages[:keep_from], messages[keep_from:]
|
|
615
|
+
|
|
616
|
+
|
|
617
|
+
def maybe_compact_context(
|
|
618
|
+
client: Client,
|
|
619
|
+
cfg: Config,
|
|
620
|
+
*,
|
|
621
|
+
precomputed_used: int | None = None,
|
|
622
|
+
model_info: dict[str, Any] | None = None,
|
|
623
|
+
) -> bool:
|
|
624
|
+
used, total, _remaining, _runtime_cap, _native = context_status(
|
|
625
|
+
cfg,
|
|
626
|
+
client=client,
|
|
627
|
+
model_info=model_info,
|
|
628
|
+
usage_override=precomputed_used,
|
|
629
|
+
)
|
|
630
|
+
if total <= 0:
|
|
631
|
+
return False
|
|
632
|
+
compact_threshold, keep_messages = context_compaction_policy(model_info)
|
|
633
|
+
if used < total * compact_threshold:
|
|
634
|
+
return False
|
|
635
|
+
if len(cfg.messages) <= keep_messages:
|
|
636
|
+
return False
|
|
637
|
+
batch, kept = _split_for_compaction(cfg.messages, keep_messages)
|
|
638
|
+
if not batch:
|
|
639
|
+
return False
|
|
640
|
+
started = time.perf_counter()
|
|
641
|
+
from . import main as _main
|
|
642
|
+
from . import perf_telemetry
|
|
643
|
+
|
|
644
|
+
summary = summarize_message_batch(
|
|
645
|
+
cfg, batch, client, maintenance_client_fn=_main.small_maintenance_client
|
|
646
|
+
)
|
|
647
|
+
duration_ms = round((time.perf_counter() - started) * 1000, 2)
|
|
648
|
+
cfg.session_summary = summary
|
|
649
|
+
cfg.messages = kept
|
|
650
|
+
cfg.save()
|
|
651
|
+
perf_telemetry.record_perf_event(
|
|
652
|
+
"compaction",
|
|
653
|
+
duration_ms=duration_ms,
|
|
654
|
+
messages_compacted=len(batch),
|
|
655
|
+
keep_messages=keep_messages,
|
|
656
|
+
threshold=compact_threshold,
|
|
657
|
+
)
|
|
658
|
+
return True
|
|
659
|
+
|
|
660
|
+
|
|
661
|
+
def rebuild_context_summary(client: Client, cfg: Config) -> tuple[bool, str]:
|
|
662
|
+
if len(cfg.messages) <= CONTEXT_KEEP_MESSAGES:
|
|
663
|
+
return False, f"Need more than {CONTEXT_KEEP_MESSAGES} messages to compact."
|
|
664
|
+
batch, kept = _split_for_compaction(cfg.messages, CONTEXT_KEEP_MESSAGES)
|
|
665
|
+
if not batch:
|
|
666
|
+
return False, "No safe message boundary found for compaction."
|
|
667
|
+
started = time.perf_counter()
|
|
668
|
+
from . import main as _main
|
|
669
|
+
from . import perf_telemetry
|
|
670
|
+
|
|
671
|
+
summary = summarize_message_batch(
|
|
672
|
+
cfg, batch, client, maintenance_client_fn=_main.small_maintenance_client
|
|
673
|
+
)
|
|
674
|
+
duration_ms = round((time.perf_counter() - started) * 1000, 2)
|
|
675
|
+
cfg.session_summary = summary
|
|
676
|
+
cfg.messages = kept
|
|
677
|
+
cfg.save()
|
|
678
|
+
perf_telemetry.record_perf_event("compaction", duration_ms=duration_ms, messages_compacted=len(batch), manual=True)
|
|
679
|
+
return True, f"Context summary rebuilt from {len(batch)} messages; kept {len(kept)} recent messages."
|