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,160 @@
|
|
|
1
|
+
"""Model-aware runtime profiles.
|
|
2
|
+
|
|
3
|
+
The harness knows each model's parameter size and provider but historically
|
|
4
|
+
applied one static set of knobs (num_ctx, temperature, reflection cadence) to
|
|
5
|
+
every model. A 4B local model and a 671B cloud model have very different needs:
|
|
6
|
+
small models want a tighter window, cooler sampling, and more frequent
|
|
7
|
+
reflection; large/cloud models can take a wider window and lighter supervision.
|
|
8
|
+
|
|
9
|
+
This module derives a :class:`ModelProfile` from model metadata. It only fills
|
|
10
|
+
in values the user has NOT explicitly changed from the Config defaults, so an
|
|
11
|
+
explicit ``/ctx``, ``/temp``, or ``/thinkevery`` always wins.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
from dataclasses import dataclass
|
|
17
|
+
from typing import Any
|
|
18
|
+
|
|
19
|
+
from . import model_info as _model_info_module
|
|
20
|
+
|
|
21
|
+
# Config-default sentinels. A field still equal to its default is treated as
|
|
22
|
+
# "untouched" and therefore eligible for adaptation. Kept in sync with
|
|
23
|
+
# config.Config; a mismatch only means adaptation is slightly more conservative.
|
|
24
|
+
DEFAULT_NUM_CTX = 8192
|
|
25
|
+
DEFAULT_TEMPERATURE = 0.4
|
|
26
|
+
DEFAULT_TOOL_THINK_EVERY = 10
|
|
27
|
+
|
|
28
|
+
# Size bands in billions of parameters.
|
|
29
|
+
SMALL_MAX_B = 9.0 # <=9B -> small
|
|
30
|
+
MEDIUM_MAX_B = 32.0 # <=32B -> medium; above -> large
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass(frozen=True)
|
|
34
|
+
class ModelProfile:
|
|
35
|
+
size_class: str # "small" | "medium" | "large" | "unknown"
|
|
36
|
+
provider: str # "local" | "cloud" | "xai" | "chatgpt"
|
|
37
|
+
num_ctx: int
|
|
38
|
+
temperature: float
|
|
39
|
+
tool_think_every: int
|
|
40
|
+
note: str # short human-readable rationale
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _size_class(size_b: float | None) -> str:
|
|
44
|
+
if size_b is None:
|
|
45
|
+
return "unknown"
|
|
46
|
+
if size_b <= SMALL_MAX_B:
|
|
47
|
+
return "small"
|
|
48
|
+
if size_b <= MEDIUM_MAX_B:
|
|
49
|
+
return "medium"
|
|
50
|
+
return "large"
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _provider(cfg: Any) -> str:
|
|
54
|
+
model = getattr(cfg, "model", "")
|
|
55
|
+
if _model_info_module.is_xai_model(model):
|
|
56
|
+
return "xai"
|
|
57
|
+
if _model_info_module.is_chatgpt_model(model):
|
|
58
|
+
return "chatgpt"
|
|
59
|
+
return "cloud" if getattr(cfg, "cloud", False) else "local"
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def recommend_profile(cfg: Any, model_info: dict[str, Any] | None) -> ModelProfile:
|
|
63
|
+
"""Compute a recommended profile for the active model.
|
|
64
|
+
|
|
65
|
+
The recommendation is bounded by the model's real native context window
|
|
66
|
+
when known, so we never recommend a window the model cannot serve.
|
|
67
|
+
"""
|
|
68
|
+
info = model_info or {}
|
|
69
|
+
size_b = _model_info_module.parameter_size_billions(info)
|
|
70
|
+
size_class = _size_class(size_b)
|
|
71
|
+
provider = _provider(cfg)
|
|
72
|
+
native_ctx = _model_info_module.get_context_length(info)
|
|
73
|
+
|
|
74
|
+
# Baseline recommendations by size class. Native context is a ceiling, not
|
|
75
|
+
# a default allocation: requesting a 128K-1M window for a short task wastes
|
|
76
|
+
# KV-cache memory and can sharply increase local prefill latency. Explicit
|
|
77
|
+
# /ctx user overrides still opt into wider windows in effective_params().
|
|
78
|
+
if size_class == "small":
|
|
79
|
+
num_ctx, temperature, think_every = 8192, 0.3, 6
|
|
80
|
+
note = "small model: tight fallback window, cooler sampling, frequent reflection"
|
|
81
|
+
elif size_class == "medium":
|
|
82
|
+
num_ctx, temperature, think_every = 16384, 0.4, 10
|
|
83
|
+
note = "medium model: standard fallback window and supervision"
|
|
84
|
+
elif size_class == "large":
|
|
85
|
+
num_ctx, temperature, think_every = 32768, 0.5, 14
|
|
86
|
+
note = "large model: wider fallback window, lighter supervision"
|
|
87
|
+
else: # unknown
|
|
88
|
+
# Remote models often report no size; assume capable but be moderate.
|
|
89
|
+
if provider in {"cloud", "xai", "chatgpt"}:
|
|
90
|
+
num_ctx, temperature, think_every = 32768, 0.4, 12
|
|
91
|
+
note = "unknown-size remote model: moderate-wide fallback window"
|
|
92
|
+
else:
|
|
93
|
+
num_ctx, temperature, think_every = DEFAULT_NUM_CTX, DEFAULT_TEMPERATURE, DEFAULT_TOOL_THINK_EVERY
|
|
94
|
+
note = "unknown model: conservative fallback defaults"
|
|
95
|
+
|
|
96
|
+
if isinstance(native_ctx, int) and native_ctx > 0:
|
|
97
|
+
if provider == "local":
|
|
98
|
+
num_ctx = min(num_ctx, native_ctx)
|
|
99
|
+
note = f"{note}; local allocation capped by native context"
|
|
100
|
+
else:
|
|
101
|
+
num_ctx = native_ctx
|
|
102
|
+
note = f"{note}; remote native context"
|
|
103
|
+
|
|
104
|
+
return ModelProfile(
|
|
105
|
+
size_class=size_class,
|
|
106
|
+
provider=provider,
|
|
107
|
+
num_ctx=num_ctx,
|
|
108
|
+
temperature=temperature,
|
|
109
|
+
tool_think_every=think_every,
|
|
110
|
+
note=note,
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
@dataclass(frozen=True)
|
|
115
|
+
class EffectiveParams:
|
|
116
|
+
"""The values to actually use this turn, after honoring user overrides."""
|
|
117
|
+
|
|
118
|
+
num_ctx: int
|
|
119
|
+
temperature: float
|
|
120
|
+
tool_think_every: int
|
|
121
|
+
adapted_fields: tuple[str, ...]
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def effective_params(cfg: Any, model_info: dict[str, Any] | None) -> EffectiveParams:
|
|
125
|
+
"""Resolve per-turn params: user overrides win, else the model profile.
|
|
126
|
+
|
|
127
|
+
A Config field still equal to its default is considered untouched and is
|
|
128
|
+
replaced by the profile recommendation. Any field the user changed via
|
|
129
|
+
``/ctx``, ``/temp``, or ``/thinkevery`` is preserved exactly.
|
|
130
|
+
"""
|
|
131
|
+
profile = recommend_profile(cfg, model_info)
|
|
132
|
+
adapted: list[str] = []
|
|
133
|
+
|
|
134
|
+
if int(getattr(cfg, "num_ctx", DEFAULT_NUM_CTX)) == DEFAULT_NUM_CTX:
|
|
135
|
+
num_ctx = profile.num_ctx
|
|
136
|
+
if num_ctx != DEFAULT_NUM_CTX:
|
|
137
|
+
adapted.append("num_ctx")
|
|
138
|
+
else:
|
|
139
|
+
num_ctx = int(cfg.num_ctx)
|
|
140
|
+
|
|
141
|
+
if float(getattr(cfg, "temperature", DEFAULT_TEMPERATURE)) == DEFAULT_TEMPERATURE:
|
|
142
|
+
temperature = profile.temperature
|
|
143
|
+
if temperature != DEFAULT_TEMPERATURE:
|
|
144
|
+
adapted.append("temperature")
|
|
145
|
+
else:
|
|
146
|
+
temperature = float(cfg.temperature)
|
|
147
|
+
|
|
148
|
+
if int(getattr(cfg, "tool_think_every", DEFAULT_TOOL_THINK_EVERY)) == DEFAULT_TOOL_THINK_EVERY:
|
|
149
|
+
think_every = profile.tool_think_every
|
|
150
|
+
if think_every != DEFAULT_TOOL_THINK_EVERY:
|
|
151
|
+
adapted.append("tool_think_every")
|
|
152
|
+
else:
|
|
153
|
+
think_every = int(cfg.tool_think_every)
|
|
154
|
+
|
|
155
|
+
return EffectiveParams(
|
|
156
|
+
num_ctx=max(1, num_ctx),
|
|
157
|
+
temperature=temperature,
|
|
158
|
+
tool_think_every=max(1, think_every),
|
|
159
|
+
adapted_fields=tuple(adapted),
|
|
160
|
+
)
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"""Model/host routing helpers (cloud vs local vs xAI)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
|
|
7
|
+
from .config import Config, load_runtime_env
|
|
8
|
+
from . import model_info as _model_info_module
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def is_cloud_model_name(name: str) -> bool:
|
|
12
|
+
return name.endswith(":cloud") or name.endswith("-cloud") or ":cloud-" in name
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _runtime_ollama_api_key() -> str:
|
|
16
|
+
"""Return OLLAMA_API_KEY after loading Algo CLI's runtime env file.
|
|
17
|
+
|
|
18
|
+
Tool/API subprocesses may start without inheriting the user's shell
|
|
19
|
+
environment. Loading here keeps routing/status checks consistent with
|
|
20
|
+
tool clients that already call ``load_runtime_env()``.
|
|
21
|
+
"""
|
|
22
|
+
load_runtime_env(override=True)
|
|
23
|
+
return os.environ.get("OLLAMA_API_KEY", "").strip()
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def uses_ollama_cloud(cfg: Config) -> bool:
|
|
27
|
+
"""Whether chat traffic should route through Ollama Cloud's direct API.
|
|
28
|
+
|
|
29
|
+
A ``:cloud`` model can also be served by a signed-in local Ollama daemon.
|
|
30
|
+
The first-run picker represents that case as ``cloud via local Ollama`` and
|
|
31
|
+
leaves ``cfg.cloud`` false. Even if stale config has ``cfg.cloud`` true, the
|
|
32
|
+
direct API route is active only when ``OLLAMA_API_KEY`` is present.
|
|
33
|
+
"""
|
|
34
|
+
if _model_info_module.is_xai_model(cfg.model) or _model_info_module.is_chatgpt_model(cfg.model):
|
|
35
|
+
return False
|
|
36
|
+
return bool(cfg.cloud and _runtime_ollama_api_key())
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def effective_runtime_host(cfg: Config) -> str:
|
|
40
|
+
"""Provider endpoint label for session_start / ops (not necessarily cfg.host)."""
|
|
41
|
+
if _model_info_module.is_xai_model(cfg.model):
|
|
42
|
+
return "xai"
|
|
43
|
+
if _model_info_module.is_chatgpt_model(cfg.model):
|
|
44
|
+
return "chatgpt"
|
|
45
|
+
if uses_ollama_cloud(cfg):
|
|
46
|
+
return "https://ollama.com"
|
|
47
|
+
return cfg.host
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def require_cloud_api_key(cfg: Config) -> None:
|
|
51
|
+
"""Fail fast before a direct Cloud API chat call when OLLAMA_API_KEY is missing."""
|
|
52
|
+
if not uses_ollama_cloud(cfg):
|
|
53
|
+
return
|
|
54
|
+
if not _runtime_ollama_api_key():
|
|
55
|
+
raise ValueError(
|
|
56
|
+
"OLLAMA_API_KEY is required for direct Ollama Cloud API mode. "
|
|
57
|
+
"Set it in the process environment or in ~/.algo_cli/env."
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def is_embedding_model_name(name: str) -> bool:
|
|
62
|
+
lowered = name.lower()
|
|
63
|
+
return (
|
|
64
|
+
"embed" in lowered
|
|
65
|
+
or "embedding" in lowered
|
|
66
|
+
or lowered.startswith("nomic-embed")
|
|
67
|
+
or "minilm" in lowered
|
|
68
|
+
or "paraphrase" in lowered
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def is_vision_model_name(name: str) -> bool:
|
|
73
|
+
lowered = name.lower()
|
|
74
|
+
return any(token in lowered for token in ("vision", "-vl", "llava", "qwen2.5-vl", "qwen3-vl"))
|
algo_cli/oneshot.py
ADDED
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
"""One-shot non-interactive JSON event mode.
|
|
2
|
+
|
|
3
|
+
Emits one JSON object per line to stdout, suitable for subprocess consumption
|
|
4
|
+
by an external bridge (Telegram bot, CI, scripts). The agent loop, tool
|
|
5
|
+
execution, policy, and contract code are unchanged; this module only swaps
|
|
6
|
+
the output sink and gates the approval flow.
|
|
7
|
+
|
|
8
|
+
Event schema (one JSON object per line, no embedded raw newlines):
|
|
9
|
+
{"type":"session_start","model":...,"host":...,"cwd":...,"approval_mode":...,"version":...}
|
|
10
|
+
{"type":"thinking","text":...}
|
|
11
|
+
{"type":"content","text":...}
|
|
12
|
+
{"type":"tool_call","call_id":...,"name":...,"args":{...}}
|
|
13
|
+
{"type":"tool_result","call_id":...,"name":...,"status":"ok|failed|denied|skipped",
|
|
14
|
+
"duration_ms":...,"summary":...,"truncated":...}
|
|
15
|
+
{"type":"tool_denied","call_id":...,"name":...,"reason":...}
|
|
16
|
+
{"type":"error","class":"timeout|policy|tool|model|internal","message":...}
|
|
17
|
+
{"type":"done","status":"complete|partial|failed","status_reason":...,
|
|
18
|
+
"tool_calls":...,"duration_ms":...}
|
|
19
|
+
|
|
20
|
+
Invariants:
|
|
21
|
+
- session_start is the first event; done is the last event.
|
|
22
|
+
- tool_result always follows the matching tool_call by call_id.
|
|
23
|
+
- No ANSI escape codes in stdout.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
from __future__ import annotations
|
|
27
|
+
|
|
28
|
+
import json
|
|
29
|
+
import sys
|
|
30
|
+
import time
|
|
31
|
+
from collections import deque
|
|
32
|
+
from typing import Any
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
DANGEROUS_TOOLS = {"run_shell", "write_file", "edit_file", "batch_edit", "update_user_profile", "model_delete", "model_create"}
|
|
36
|
+
SUMMARY_LIMIT = 600
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _summarize(text: str, limit: int = SUMMARY_LIMIT) -> tuple[str, bool]:
|
|
40
|
+
s = str(text).strip()
|
|
41
|
+
if len(s) <= limit:
|
|
42
|
+
return s, False
|
|
43
|
+
return s[:limit].rstrip() + "...", True
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _tool_status_from_result(result: str) -> str:
|
|
47
|
+
lowered = str(result).strip().lower()
|
|
48
|
+
if lowered.startswith("user denied"):
|
|
49
|
+
return "denied"
|
|
50
|
+
if lowered.startswith("skipped repeated"):
|
|
51
|
+
return "skipped"
|
|
52
|
+
if lowered.startswith(("error", "tool error", "tool argument error", "unknown tool")):
|
|
53
|
+
return "failed"
|
|
54
|
+
return "ok"
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class JsonEventSink:
|
|
58
|
+
"""Stdout writer for one-shot JSON events. Thread-safe enough for serial dispatch."""
|
|
59
|
+
|
|
60
|
+
def __init__(self, *, stream=None, approval_mode: str = "never") -> None:
|
|
61
|
+
self._stream = stream if stream is not None else sys.stdout
|
|
62
|
+
self.approval_mode = approval_mode
|
|
63
|
+
self.deny_dangerous = approval_mode == "never"
|
|
64
|
+
self._call_count = 0
|
|
65
|
+
self._pending_call_ids: dict[str, deque[str]] = {}
|
|
66
|
+
self._tool_calls_done = 0
|
|
67
|
+
self._prompt_tokens = 0
|
|
68
|
+
self._completion_tokens = 0
|
|
69
|
+
self._errors: list[dict[str, str]] = []
|
|
70
|
+
self._started_at = time.perf_counter()
|
|
71
|
+
|
|
72
|
+
def _write(self, event: dict[str, Any]) -> None:
|
|
73
|
+
line = json.dumps(event, ensure_ascii=False, default=str)
|
|
74
|
+
# JSON encoding already escapes embedded newlines; one event per line is invariant.
|
|
75
|
+
self._stream.write(line + "\n")
|
|
76
|
+
self._stream.flush()
|
|
77
|
+
|
|
78
|
+
# --- framing ---
|
|
79
|
+
|
|
80
|
+
def session_start(self, *, model: str, host: str, cwd: str, version: str) -> None:
|
|
81
|
+
self._write({
|
|
82
|
+
"type": "session_start",
|
|
83
|
+
"model": model,
|
|
84
|
+
"host": host,
|
|
85
|
+
"cwd": cwd,
|
|
86
|
+
"approval_mode": self.approval_mode,
|
|
87
|
+
"version": version,
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
def done(self, *, status: str, status_reason: str, duration_ms: float) -> None:
|
|
91
|
+
total_tokens = self._prompt_tokens + self._completion_tokens
|
|
92
|
+
self._write({
|
|
93
|
+
"type": "done",
|
|
94
|
+
"status": status,
|
|
95
|
+
"status_reason": status_reason,
|
|
96
|
+
"tool_calls": self._tool_calls_done,
|
|
97
|
+
"duration_ms": round(duration_ms, 2),
|
|
98
|
+
"usage": {
|
|
99
|
+
"prompt_tokens": self._prompt_tokens,
|
|
100
|
+
"completion_tokens": self._completion_tokens,
|
|
101
|
+
"total_tokens": total_tokens,
|
|
102
|
+
},
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
# --- model output ---
|
|
106
|
+
|
|
107
|
+
def thinking(self, text: str) -> None:
|
|
108
|
+
if not text:
|
|
109
|
+
return
|
|
110
|
+
self._write({"type": "thinking", "text": text})
|
|
111
|
+
|
|
112
|
+
def content(self, text: str) -> None:
|
|
113
|
+
if not text:
|
|
114
|
+
return
|
|
115
|
+
self._write({"type": "content", "text": text})
|
|
116
|
+
|
|
117
|
+
def chat_usage(self, *, prompt_tokens: Any, completion_tokens: Any) -> None:
|
|
118
|
+
"""Accumulate provider-reported usage once per completed chat turn."""
|
|
119
|
+
|
|
120
|
+
try:
|
|
121
|
+
prompt = int(prompt_tokens or 0)
|
|
122
|
+
completion = int(completion_tokens or 0)
|
|
123
|
+
except (TypeError, ValueError):
|
|
124
|
+
return
|
|
125
|
+
if prompt > 0 or completion > 0:
|
|
126
|
+
self._prompt_tokens += max(0, prompt)
|
|
127
|
+
self._completion_tokens += max(0, completion)
|
|
128
|
+
|
|
129
|
+
# --- tool dispatch ---
|
|
130
|
+
|
|
131
|
+
def next_call_id(self) -> str:
|
|
132
|
+
self._call_count += 1
|
|
133
|
+
return f"oneshot-{self._call_count}"
|
|
134
|
+
|
|
135
|
+
def tool_call(self, *, call_id: str, name: str, args: dict[str, Any]) -> None:
|
|
136
|
+
self._pending_call_ids.setdefault(name, deque()).append(call_id)
|
|
137
|
+
self._write({
|
|
138
|
+
"type": "tool_call",
|
|
139
|
+
"call_id": call_id,
|
|
140
|
+
"name": name,
|
|
141
|
+
"args": args,
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
def tool_result(
|
|
145
|
+
self,
|
|
146
|
+
*,
|
|
147
|
+
call_id: str | None,
|
|
148
|
+
name: str,
|
|
149
|
+
result: str,
|
|
150
|
+
duration_ms: float | None,
|
|
151
|
+
) -> None:
|
|
152
|
+
call_id = self._matching_call_id(name, call_id)
|
|
153
|
+
status = _tool_status_from_result(result)
|
|
154
|
+
summary, truncated = _summarize(result)
|
|
155
|
+
self._tool_calls_done += 1
|
|
156
|
+
self._write({
|
|
157
|
+
"type": "tool_result",
|
|
158
|
+
"call_id": call_id,
|
|
159
|
+
"name": name,
|
|
160
|
+
"status": status,
|
|
161
|
+
"duration_ms": round(duration_ms, 2) if duration_ms is not None else None,
|
|
162
|
+
"summary": summary,
|
|
163
|
+
"truncated": truncated,
|
|
164
|
+
})
|
|
165
|
+
|
|
166
|
+
def tool_denied(self, *, call_id: str | None, name: str, reason: str) -> None:
|
|
167
|
+
call_id = self._matching_call_id(name, call_id)
|
|
168
|
+
self._tool_calls_done += 1
|
|
169
|
+
self._write({
|
|
170
|
+
"type": "tool_denied",
|
|
171
|
+
"call_id": call_id,
|
|
172
|
+
"name": name,
|
|
173
|
+
"reason": reason,
|
|
174
|
+
})
|
|
175
|
+
|
|
176
|
+
def _matching_call_id(self, name: str, call_id: str | None) -> str:
|
|
177
|
+
"""Resolve a result to the oldest unmatched call of the same tool."""
|
|
178
|
+
|
|
179
|
+
pending = self._pending_call_ids.get(name)
|
|
180
|
+
if call_id:
|
|
181
|
+
if pending:
|
|
182
|
+
try:
|
|
183
|
+
pending.remove(call_id)
|
|
184
|
+
except ValueError:
|
|
185
|
+
pass
|
|
186
|
+
if not pending:
|
|
187
|
+
self._pending_call_ids.pop(name, None)
|
|
188
|
+
return call_id
|
|
189
|
+
if pending:
|
|
190
|
+
matched = pending.popleft()
|
|
191
|
+
if not pending:
|
|
192
|
+
self._pending_call_ids.pop(name, None)
|
|
193
|
+
return matched
|
|
194
|
+
return self.next_call_id()
|
|
195
|
+
|
|
196
|
+
# --- errors ---
|
|
197
|
+
|
|
198
|
+
def error(self, *, error_class: str, message: str) -> None:
|
|
199
|
+
self._errors.append({"class": str(error_class), "message": str(message)})
|
|
200
|
+
self._write({
|
|
201
|
+
"type": "error",
|
|
202
|
+
"class": error_class,
|
|
203
|
+
"message": message,
|
|
204
|
+
})
|
|
205
|
+
|
|
206
|
+
@property
|
|
207
|
+
def errors(self) -> tuple[dict[str, str], ...]:
|
|
208
|
+
return tuple(self._errors)
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def run_oneshot(
|
|
212
|
+
*,
|
|
213
|
+
prompt: str,
|
|
214
|
+
approval_mode: str = "never",
|
|
215
|
+
cfg_overrides: dict[str, Any] | None = None,
|
|
216
|
+
stream=None,
|
|
217
|
+
) -> int:
|
|
218
|
+
"""Run a single agent turn and emit JSON events to stdout. Returns exit code.
|
|
219
|
+
|
|
220
|
+
- approval_mode="never" (default): dangerous tools are denied; emits tool_denied.
|
|
221
|
+
- approval_mode="auto": equivalent to cfg.auto_mode=True for this run only.
|
|
222
|
+
- cfg_overrides: applied to the loaded Config before the run (e.g., {"model": "qwen3"}).
|
|
223
|
+
"""
|
|
224
|
+
# Imports deferred to avoid cycle with display + to keep import cost off the
|
|
225
|
+
# interactive path when --oneshot is not used.
|
|
226
|
+
from . import deliberation, display, harness, main, skills, tool_runtime
|
|
227
|
+
from .config import Config
|
|
228
|
+
from .model_routing import effective_runtime_host
|
|
229
|
+
from .tool_runtime import session_command_requires_approval
|
|
230
|
+
|
|
231
|
+
cfg = Config.load()
|
|
232
|
+
persistent_values: dict[str, Any] = {
|
|
233
|
+
"auto_mode": cfg.auto_mode,
|
|
234
|
+
"skill_crystallize_enabled": cfg.skill_crystallize_enabled,
|
|
235
|
+
"session_summary": cfg.session_summary,
|
|
236
|
+
"show_thinking": cfg.show_thinking,
|
|
237
|
+
"temperature": cfg.temperature,
|
|
238
|
+
}
|
|
239
|
+
if cfg_overrides:
|
|
240
|
+
for key, value in cfg_overrides.items():
|
|
241
|
+
if value is not None and hasattr(cfg, key):
|
|
242
|
+
persistent_values.setdefault(key, getattr(cfg, key))
|
|
243
|
+
setattr(cfg, key, value)
|
|
244
|
+
harness.configure_context_sources(
|
|
245
|
+
external=cfg.external_harness_sources_enabled,
|
|
246
|
+
index_compute_lab=cfg.index_compute_lab_auto_inject,
|
|
247
|
+
)
|
|
248
|
+
if approval_mode not in {"never", "auto"}:
|
|
249
|
+
raise ValueError("approval_mode must be 'never' or 'auto'")
|
|
250
|
+
cfg.auto_mode = approval_mode == "auto"
|
|
251
|
+
cfg.skill_crystallize_enabled = False # subprocess invocation must not mutate skill store
|
|
252
|
+
if not cfg_overrides or "show_thinking" not in cfg_overrides:
|
|
253
|
+
cfg.show_thinking = cfg.show_thinking and deliberation.needs_deliberation(prompt)
|
|
254
|
+
if not cfg_overrides or "temperature" not in cfg_overrides:
|
|
255
|
+
cfg.temperature = min(cfg.temperature, 0.2)
|
|
256
|
+
|
|
257
|
+
# Bridge runs (Telegram, CI) must not inherit interactive session_summary into prompts.
|
|
258
|
+
cfg.session_summary = ""
|
|
259
|
+
|
|
260
|
+
sink = JsonEventSink(stream=stream, approval_mode=approval_mode)
|
|
261
|
+
sink.session_start(
|
|
262
|
+
model=cfg.model,
|
|
263
|
+
host=effective_runtime_host(cfg),
|
|
264
|
+
cwd=cfg.cwd,
|
|
265
|
+
version=_resolve_version(),
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
display.install_json_sink(sink)
|
|
269
|
+
original_ask_approval = main.ask_approval
|
|
270
|
+
original_runtime_ask_approval = tool_runtime.ask_approval
|
|
271
|
+
|
|
272
|
+
def _oneshot_ask_approval(name: str, args: dict[str, Any], cfg: Config, *, force: bool = False) -> bool:
|
|
273
|
+
del cfg
|
|
274
|
+
requires_approval = name in DANGEROUS_TOOLS or (
|
|
275
|
+
name == "session_command"
|
|
276
|
+
and session_command_requires_approval(str(args.get("command") or ""))
|
|
277
|
+
)
|
|
278
|
+
if approval_mode == "never" and requires_approval:
|
|
279
|
+
# Sink's tool_denied is emitted in the agent_loop's denial path via show_tool_result.
|
|
280
|
+
# We just refuse here; the existing "User denied this operation." message flows
|
|
281
|
+
# through show_tool_result → sink.tool_denied conversion.
|
|
282
|
+
return False
|
|
283
|
+
if approval_mode == "auto" and not force:
|
|
284
|
+
return True
|
|
285
|
+
return True
|
|
286
|
+
|
|
287
|
+
main.ask_approval = _oneshot_ask_approval
|
|
288
|
+
tool_runtime.ask_approval = _oneshot_ask_approval
|
|
289
|
+
|
|
290
|
+
started = time.perf_counter()
|
|
291
|
+
status = "complete"
|
|
292
|
+
status_reason = ""
|
|
293
|
+
try:
|
|
294
|
+
client = main.create_client(cfg)
|
|
295
|
+
main.agent_loop(client, cfg, prompt)
|
|
296
|
+
except KeyboardInterrupt:
|
|
297
|
+
status = "failed"
|
|
298
|
+
status_reason = "interrupted"
|
|
299
|
+
sink.error(error_class="internal", message="KeyboardInterrupt")
|
|
300
|
+
except Exception as exc:
|
|
301
|
+
status = "failed"
|
|
302
|
+
status_reason = f"{type(exc).__name__}: {exc}"
|
|
303
|
+
sink.error(error_class="internal", message=status_reason)
|
|
304
|
+
finally:
|
|
305
|
+
main.ask_approval = original_ask_approval
|
|
306
|
+
tool_runtime.ask_approval = original_runtime_ask_approval
|
|
307
|
+
display.uninstall_json_sink()
|
|
308
|
+
skills.ensure_dirs() # restore any deferred dir state
|
|
309
|
+
# agent_loop saves config; restore bridge-only mutations and CLI override fields.
|
|
310
|
+
for key, value in persistent_values.items():
|
|
311
|
+
setattr(cfg, key, value)
|
|
312
|
+
cfg.save()
|
|
313
|
+
|
|
314
|
+
if status == "complete" and sink.errors:
|
|
315
|
+
status = "partial"
|
|
316
|
+
status_reason = sink.errors[-1].get("message", "one-shot run emitted an error event")
|
|
317
|
+
|
|
318
|
+
sink.done(
|
|
319
|
+
status=status,
|
|
320
|
+
status_reason=status_reason,
|
|
321
|
+
duration_ms=(time.perf_counter() - started) * 1000,
|
|
322
|
+
)
|
|
323
|
+
return 0 if status == "complete" else 2
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
def _resolve_version() -> str:
|
|
327
|
+
try:
|
|
328
|
+
from importlib.metadata import version
|
|
329
|
+
return version("algo-cli-runtime")
|
|
330
|
+
except Exception:
|
|
331
|
+
return "unknown"
|