@oneciel-ai/ciel-runtime 0.1.1 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +132 -0
- package/ciel-runtime-menu.py +56 -6
- package/ciel_runtime.py +9379 -35909
- package/ciel_runtime_support/advisor_client.py +193 -0
- package/ciel_runtime_support/advisor_policy.py +320 -0
- package/ciel_runtime_support/advisor_refinement.py +160 -0
- package/ciel_runtime_support/advisor_request_builder.py +261 -0
- package/ciel_runtime_support/agy_installer.py +169 -0
- package/ciel_runtime_support/agy_mcp_restore.py +182 -0
- package/ciel_runtime_support/anthropic_model_policy.py +186 -0
- package/ciel_runtime_support/anthropic_response_writer.py +255 -0
- package/ciel_runtime_support/anthropic_tool_turns.py +130 -0
- package/ciel_runtime_support/api_key_cooldown.py +159 -0
- package/ciel_runtime_support/architecture.py +488 -1
- package/ciel_runtime_support/architecture_budget.py +42 -0
- package/ciel_runtime_support/channel_backlog.py +90 -0
- package/ciel_runtime_support/channel_cli.py +119 -0
- package/ciel_runtime_support/channel_compact_injection.py +82 -0
- package/ciel_runtime_support/channel_compact_poll.py +67 -0
- package/ciel_runtime_support/channel_compact_request_repository.py +113 -0
- package/ciel_runtime_support/channel_config_service.py +281 -0
- package/ciel_runtime_support/channel_connection_lifecycle.py +180 -0
- package/ciel_runtime_support/channel_connection_registry.py +128 -0
- package/ciel_runtime_support/channel_connection_worker.py +284 -0
- package/ciel_runtime_support/channel_cursor_recovery.py +92 -0
- package/ciel_runtime_support/channel_cursor_repository.py +89 -0
- package/ciel_runtime_support/channel_cursor_service.py +178 -0
- package/ciel_runtime_support/channel_event_identity.py +212 -0
- package/ciel_runtime_support/channel_event_projection.py +315 -0
- package/ciel_runtime_support/channel_inflight.py +127 -0
- package/ciel_runtime_support/channel_injection.py +115 -0
- package/ciel_runtime_support/channel_launch_guard_repository.py +58 -0
- package/ciel_runtime_support/channel_launch_policy.py +180 -0
- package/ciel_runtime_support/channel_llm_context.py +156 -0
- package/ciel_runtime_support/channel_mcp_discovery.py +186 -0
- package/ciel_runtime_support/channel_mcp_http_controller.py +239 -0
- package/ciel_runtime_support/channel_mcp_ownership.py +148 -0
- package/ciel_runtime_support/channel_mcp_tools.py +240 -0
- package/ciel_runtime_support/channel_mcp_transport.py +394 -0
- package/ciel_runtime_support/channel_message_dedupe.py +65 -0
- package/ciel_runtime_support/channel_message_policy.py +256 -0
- package/ciel_runtime_support/channel_message_prompt.py +305 -0
- package/ciel_runtime_support/channel_message_repository.py +234 -0
- package/ciel_runtime_support/channel_notification_projection.py +217 -0
- package/ciel_runtime_support/channel_panel.py +162 -0
- package/ciel_runtime_support/channel_pending_injection.py +209 -0
- package/ciel_runtime_support/channel_pending_poll.py +109 -0
- package/ciel_runtime_support/channel_probe_cache.py +433 -0
- package/ciel_runtime_support/channel_probe_report.py +101 -0
- package/ciel_runtime_support/channel_runtime_environment.py +181 -0
- package/ciel_runtime_support/channel_session_lifecycle.py +114 -0
- package/ciel_runtime_support/channel_session_repository.py +90 -0
- package/ciel_runtime_support/channel_terminal_dispatch.py +115 -0
- package/ciel_runtime_support/channel_terminal_input.py +277 -0
- package/ciel_runtime_support/channel_terminal_proxy.py +447 -0
- package/ciel_runtime_support/channel_tool_context.py +166 -0
- package/ciel_runtime_support/channel_transcript.py +414 -0
- package/ciel_runtime_support/channel_transcript_repository.py +96 -0
- package/ciel_runtime_support/channel_wake_claim_repository.py +126 -0
- package/ciel_runtime_support/channel_wake_delivery_repository.py +88 -0
- package/ciel_runtime_support/chat_files.py +138 -0
- package/ciel_runtime_support/chat_http_controller.py +235 -0
- package/ciel_runtime_support/claude_environment.py +375 -0
- package/ciel_runtime_support/claude_router.py +247 -193
- package/ciel_runtime_support/cli_dispatch.py +792 -0
- package/ciel_runtime_support/cli_parser.py +165 -0
- package/ciel_runtime_support/cli_usage.py +100 -0
- package/ciel_runtime_support/codex_app_server.py +20 -5
- package/ciel_runtime_support/codex_channel_sse_launch.py +87 -0
- package/ciel_runtime_support/codex_cli.py +42 -6
- package/ciel_runtime_support/codex_config.py +323 -0
- package/ciel_runtime_support/codex_launch_configuration.py +240 -0
- package/ciel_runtime_support/codex_launch_policy.py +66 -0
- package/ciel_runtime_support/codex_mcp_integration.py +195 -0
- package/ciel_runtime_support/codex_mcp_restore.py +304 -0
- package/ciel_runtime_support/codex_model_catalog.py +133 -0
- package/ciel_runtime_support/codex_process_lifecycle.py +271 -0
- package/ciel_runtime_support/codex_router.py +147 -1
- package/ciel_runtime_support/codex_session_repository.py +115 -0
- package/ciel_runtime_support/codex_session_selection.py +114 -0
- package/ciel_runtime_support/command_asset_installer.py +103 -0
- package/ciel_runtime_support/compatibility_probe.py +295 -0
- package/ciel_runtime_support/compatibility_protocol.py +251 -0
- package/ciel_runtime_support/compatibility_runtime.py +166 -0
- package/ciel_runtime_support/compatibility_test.py +370 -0
- package/ciel_runtime_support/config_migrations.py +307 -0
- package/ciel_runtime_support/config_repository.py +175 -0
- package/ciel_runtime_support/config_value_codec.py +64 -0
- package/ciel_runtime_support/configuration_cli.py +374 -0
- package/ciel_runtime_support/context_compaction.py +280 -0
- package/ciel_runtime_support/context_setup.py +208 -0
- package/ciel_runtime_support/context_summary_policy.py +392 -0
- package/ciel_runtime_support/credential_cli.py +104 -0
- package/ciel_runtime_support/credential_management.py +261 -0
- package/ciel_runtime_support/credentials.py +269 -0
- package/ciel_runtime_support/executable_discovery.py +141 -0
- package/ciel_runtime_support/github_copilot_oauth.py +335 -0
- package/ciel_runtime_support/github_copilot_oauth_runtime.py +213 -0
- package/ciel_runtime_support/header_forwarding.py +73 -0
- package/ciel_runtime_support/headless_config.py +221 -0
- package/ciel_runtime_support/http_response.py +129 -0
- package/ciel_runtime_support/install_diagnostics.py +149 -0
- package/ciel_runtime_support/kimi_identity.py +123 -0
- package/ciel_runtime_support/launch_diagnostics.py +204 -0
- package/ciel_runtime_support/launch_state.py +127 -0
- package/ciel_runtime_support/live_api_key_controller.py +58 -0
- package/ciel_runtime_support/llm_config_http.py +148 -0
- package/ciel_runtime_support/llm_option_config.py +259 -0
- package/ciel_runtime_support/llm_presentation_data.py +447 -0
- package/ciel_runtime_support/llm_presets.py +773 -0
- package/ciel_runtime_support/lm_studio_runtime.py +401 -0
- package/ciel_runtime_support/managed_mcp_config.py +144 -0
- package/ciel_runtime_support/managed_mcp_discovery.py +151 -0
- package/ciel_runtime_support/managed_service_cleanup.py +89 -0
- package/ciel_runtime_support/mcp_config_reader.py +230 -0
- package/ciel_runtime_support/mcp_http_proxy.py +607 -0
- package/ciel_runtime_support/mcp_inventory.py +59 -0
- package/ciel_runtime_support/mcp_notification_wait_policy.py +159 -0
- package/ciel_runtime_support/mcp_probe_codec.py +136 -0
- package/ciel_runtime_support/mcp_probe_transport.py +328 -0
- package/ciel_runtime_support/mcp_proxy_codec.py +345 -0
- package/ciel_runtime_support/mcp_proxy_config.py +107 -0
- package/ciel_runtime_support/mcp_proxy_notifications.py +261 -0
- package/ciel_runtime_support/mcp_proxy_process.py +560 -0
- package/ciel_runtime_support/mcp_split_proxy_http.py +165 -0
- package/ciel_runtime_support/mcp_stdio_probe.py +240 -0
- package/ciel_runtime_support/mcp_transport.py +146 -0
- package/ciel_runtime_support/model_cache_lifecycle.py +78 -0
- package/ciel_runtime_support/model_catalog_projection.py +61 -0
- package/ciel_runtime_support/model_context_hints.py +109 -0
- package/ciel_runtime_support/model_panel.py +147 -0
- package/ciel_runtime_support/model_registry_repository.py +231 -0
- package/ciel_runtime_support/npm_runtime.py +191 -0
- package/ciel_runtime_support/ollama_catalog.py +462 -0
- package/ciel_runtime_support/ollama_catalog_cli.py +41 -0
- package/ciel_runtime_support/ollama_catalog_repository.py +75 -0
- package/ciel_runtime_support/ollama_context_sync.py +87 -0
- package/ciel_runtime_support/ollama_forwarding.py +449 -0
- package/ciel_runtime_support/openai_chat_passthrough.py +67 -0
- package/ciel_runtime_support/openai_chat_router.py +64 -0
- package/ciel_runtime_support/openai_forwarding.py +194 -0
- package/ciel_runtime_support/openai_responses_router.py +291 -0
- package/ciel_runtime_support/openai_responses_stream.py +135 -0
- package/ciel_runtime_support/output_budget.py +89 -0
- package/ciel_runtime_support/package_lifecycle.py +217 -0
- package/ciel_runtime_support/plan_artifact_controller.py +104 -0
- package/ciel_runtime_support/prelaunch.py +959 -0
- package/ciel_runtime_support/prelaunch_launch_preference.py +75 -0
- package/ciel_runtime_support/prelaunch_panel_projection.py +396 -0
- package/ciel_runtime_support/prelaunch_terminal.py +764 -0
- package/ciel_runtime_support/process_control.py +708 -0
- package/ciel_runtime_support/prompt_compaction.py +322 -0
- package/ciel_runtime_support/prompt_injection.py +176 -0
- package/ciel_runtime_support/protocols/__init__.py +24 -0
- package/ciel_runtime_support/protocols/anthropic_content.py +29 -0
- package/ciel_runtime_support/protocols/anthropic_thinking_policy.py +336 -0
- package/ciel_runtime_support/protocols/chat_projection.py +315 -0
- package/ciel_runtime_support/protocols/conversation_policy.py +217 -0
- package/ciel_runtime_support/protocols/conversation_turn_policy.py +972 -0
- package/ciel_runtime_support/protocols/ollama_chat.py +111 -0
- package/ciel_runtime_support/protocols/ollama_response.py +231 -0
- package/ciel_runtime_support/protocols/openai_reasoning.py +75 -0
- package/ciel_runtime_support/protocols/openai_responses.py +271 -0
- package/ciel_runtime_support/protocols/pseudo_tool_history.py +248 -0
- package/ciel_runtime_support/protocols/tool_result_projection.py +112 -0
- package/ciel_runtime_support/provider_adapters.py +160 -0
- package/ciel_runtime_support/provider_catalog_sources.py +316 -0
- package/ciel_runtime_support/provider_choice.py +201 -0
- package/ciel_runtime_support/provider_compatibility.py +165 -0
- package/ciel_runtime_support/provider_config_mutations.py +361 -0
- package/ciel_runtime_support/provider_configuration_service.py +162 -0
- package/ciel_runtime_support/provider_context.py +308 -0
- package/ciel_runtime_support/provider_contract_projection.py +73 -0
- package/ciel_runtime_support/provider_descriptor.py +82 -0
- package/ciel_runtime_support/provider_endpoint_policy.py +130 -0
- package/ciel_runtime_support/provider_endpoint_probe.py +165 -0
- package/ciel_runtime_support/provider_launch_endpoint.py +109 -0
- package/ciel_runtime_support/provider_limits.py +457 -0
- package/ciel_runtime_support/provider_model_identity.py +139 -0
- package/ciel_runtime_support/provider_model_selection.py +431 -0
- package/ciel_runtime_support/provider_model_specs.py +142 -0
- package/ciel_runtime_support/provider_models.py +263 -0
- package/ciel_runtime_support/provider_network.py +176 -0
- package/ciel_runtime_support/provider_option_cli.py +238 -0
- package/ciel_runtime_support/provider_option_panel.py +275 -0
- package/ciel_runtime_support/provider_option_status.py +192 -0
- package/ciel_runtime_support/provider_policy.py +101 -0
- package/ciel_runtime_support/provider_query_policy.py +67 -0
- package/ciel_runtime_support/provider_readiness.py +112 -0
- package/ciel_runtime_support/provider_request_access.py +131 -0
- package/ciel_runtime_support/provider_request_builder.py +250 -0
- package/ciel_runtime_support/provider_responses_passthrough.py +81 -0
- package/ciel_runtime_support/provider_runtime_info.py +113 -0
- package/ciel_runtime_support/provider_runtime_modes.py +150 -0
- package/ciel_runtime_support/provider_sampling_policy.py +46 -0
- package/ciel_runtime_support/provider_status.py +145 -0
- package/ciel_runtime_support/provider_timeout_policy.py +184 -0
- package/ciel_runtime_support/provider_tool_policy.py +145 -0
- package/ciel_runtime_support/providers/__init__.py +65 -0
- package/ciel_runtime_support/providers/anthropic.py +160 -0
- package/ciel_runtime_support/providers/anthropic_catalog.py +119 -0
- package/ciel_runtime_support/providers/base.py +232 -0
- package/ciel_runtime_support/providers/catalog.py +326 -0
- package/ciel_runtime_support/providers/cloud.py +194 -0
- package/ciel_runtime_support/providers/constants.py +54 -0
- package/ciel_runtime_support/providers/deepseek.py +115 -0
- package/ciel_runtime_support/providers/fireworks.py +158 -0
- package/ciel_runtime_support/providers/github_copilot_oauth.py +199 -0
- package/ciel_runtime_support/providers/kimi.py +297 -0
- package/ciel_runtime_support/providers/lm_studio.py +76 -0
- package/ciel_runtime_support/providers/meta.py +257 -0
- package/ciel_runtime_support/providers/native.py +194 -0
- package/ciel_runtime_support/providers/nim.py +69 -0
- package/ciel_runtime_support/providers/nvidia.py +158 -0
- package/ciel_runtime_support/providers/nvidia_runtime.py +285 -0
- package/ciel_runtime_support/providers/ollama.py +181 -0
- package/ciel_runtime_support/providers/ollama_context.py +195 -0
- package/ciel_runtime_support/providers/ollama_runtime.py +213 -0
- package/ciel_runtime_support/providers/opencode.py +220 -0
- package/ciel_runtime_support/providers/opencode_go.py +37 -0
- package/ciel_runtime_support/providers/openrouter.py +57 -0
- package/ciel_runtime_support/providers/vllm.py +65 -0
- package/ciel_runtime_support/providers/zai.py +119 -0
- package/ciel_runtime_support/pseudo_tool_parser.py +115 -0
- package/ciel_runtime_support/rate_limit_policy.py +117 -0
- package/ciel_runtime_support/rate_limit_repository.py +154 -0
- package/ciel_runtime_support/registry.py +46 -0
- package/ciel_runtime_support/request_shortcuts.py +253 -0
- package/ciel_runtime_support/request_trace.py +323 -0
- package/ciel_runtime_support/response_collection.py +209 -0
- package/ciel_runtime_support/router_access.py +238 -0
- package/ciel_runtime_support/router_client_lifecycle.py +366 -0
- package/ciel_runtime_support/router_health_policy.py +101 -0
- package/ciel_runtime_support/router_http.py +513 -0
- package/ciel_runtime_support/router_process_lifecycle.py +401 -0
- package/ciel_runtime_support/router_rate_limit_service.py +285 -0
- package/ciel_runtime_support/router_server_runtime.py +103 -0
- package/ciel_runtime_support/router_shortcuts.py +201 -0
- package/ciel_runtime_support/routing_fallback.py +73 -0
- package/ciel_runtime_support/runtime_activity_repository.py +143 -0
- package/ciel_runtime_support/runtime_adapters.py +104 -0
- package/ciel_runtime_support/runtime_command_factory.py +73 -0
- package/ciel_runtime_support/runtime_compatibility.py +50 -0
- package/ciel_runtime_support/runtime_constants.py +178 -0
- package/ciel_runtime_support/runtime_launch.py +1602 -0
- package/ciel_runtime_support/runtime_llm_options.py +312 -0
- package/ciel_runtime_support/runtime_logging.py +161 -0
- package/ciel_runtime_support/runtime_paths.py +157 -0
- package/ciel_runtime_support/runtime_restart.py +84 -0
- package/ciel_runtime_support/runtime_upgrade.py +149 -0
- package/ciel_runtime_support/secure_json_repository.py +55 -0
- package/ciel_runtime_support/session_import.py +356 -0
- package/ciel_runtime_support/settings_repository.py +8 -0
- package/ciel_runtime_support/slash_command_assets.py +211 -0
- package/ciel_runtime_support/sse_stream.py +57 -0
- package/ciel_runtime_support/sse_trace.py +225 -0
- package/ciel_runtime_support/statusline_script.py +593 -0
- package/ciel_runtime_support/statusline_settings.py +53 -0
- package/ciel_runtime_support/stream_chunk_policy.py +18 -0
- package/ciel_runtime_support/streaming_anthropic.py +1955 -0
- package/ciel_runtime_support/synthetic_tool_policy.py +105 -0
- package/ciel_runtime_support/terminal_platform_io.py +127 -0
- package/ciel_runtime_support/timeout_profile.py +196 -0
- package/ciel_runtime_support/tool_dialects.py +85 -0
- package/ciel_runtime_support/tool_exposure_policy.py +63 -0
- package/ciel_runtime_support/tool_guard_hooks.py +218 -0
- package/ciel_runtime_support/tool_request_projection.py +96 -0
- package/ciel_runtime_support/tool_schema.py +483 -0
- package/ciel_runtime_support/tool_side_effect_dedupe.py +95 -0
- package/ciel_runtime_support/ui_text.py +266 -0
- package/ciel_runtime_support/upstream_error_policy.py +104 -0
- package/ciel_runtime_support/upstream_retry.py +419 -0
- package/ciel_runtime_support/upstream_stream_io.py +106 -0
- package/ciel_runtime_support/usage_events.py +96 -0
- package/ciel_runtime_support/visible_stream_filters.py +130 -0
- package/ciel_runtime_support/web_endpoints.py +447 -0
- package/ciel_runtime_support/web_ui.py +915 -0
- package/ciel_runtime_support/web_ui_controller.py +189 -0
- package/ciel_runtime_support/windows_console_input.py +137 -0
- package/ciel_runtime_support/windows_console_mode.py +112 -0
- package/docs/Architecture.md +54 -0
- package/docs/Configuration.md +17 -0
- package/docs/Module-Map.md +1093 -26
- package/docs/Providers.md +46 -1
- package/docs/adr/0001-runtime-bounded-contexts.md +295 -0
- package/npm-bin/run-ciel-runtime.js +22 -2
- package/package.json +9 -2
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
"""Application clients for Advisor review and refinement provider calls."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from typing import Any, Callable
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass(frozen=True, slots=True)
|
|
10
|
+
class AdvisorClientPolicy:
|
|
11
|
+
model_enabled: Callable[[dict[str, Any]], str]
|
|
12
|
+
provider_supported: Callable[[str], bool]
|
|
13
|
+
upstream_model: Callable[[str, str], str]
|
|
14
|
+
provider_kind: Callable[..., str]
|
|
15
|
+
request: Callable[..., dict[str, Any]]
|
|
16
|
+
endpoint: Callable[[str, dict[str, Any]], str]
|
|
17
|
+
response_text: Callable[[str, Any], str]
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass(frozen=True, slots=True)
|
|
21
|
+
class AdvisorClientIO:
|
|
22
|
+
apply_rate_limit: Callable[..., None]
|
|
23
|
+
write_activity: Callable[..., None]
|
|
24
|
+
estimate_tokens: Callable[[Any], int]
|
|
25
|
+
post_json: Callable[..., Any]
|
|
26
|
+
headers: Callable[..., dict[str, str]]
|
|
27
|
+
provider_timeout: Callable[[dict[str, Any]], float]
|
|
28
|
+
ollama_timeout: Callable[[dict[str, Any]], float]
|
|
29
|
+
log: Callable[[str, str], None]
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class AdvisorClient:
|
|
33
|
+
def __init__(self, policy: AdvisorClientPolicy, io: AdvisorClientIO) -> None:
|
|
34
|
+
self.policy = policy
|
|
35
|
+
self.io = io
|
|
36
|
+
|
|
37
|
+
def review(
|
|
38
|
+
self,
|
|
39
|
+
provider: str,
|
|
40
|
+
config: dict[str, Any],
|
|
41
|
+
body: dict[str, Any],
|
|
42
|
+
focus: str = "",
|
|
43
|
+
inbound_headers: Any | None = None,
|
|
44
|
+
*,
|
|
45
|
+
allow_rate_limit_wait: bool = True,
|
|
46
|
+
retry_rate_limits: bool = True,
|
|
47
|
+
raise_errors: bool = False,
|
|
48
|
+
) -> str:
|
|
49
|
+
advisor_model = self.policy.model_enabled(config)
|
|
50
|
+
if not advisor_model or not self.policy.provider_supported(provider):
|
|
51
|
+
return ""
|
|
52
|
+
upstream_model = self.policy.upstream_model(provider, advisor_model)
|
|
53
|
+
request_body = self.policy.request(
|
|
54
|
+
provider, advisor_model, body, config, focus_override=focus
|
|
55
|
+
)
|
|
56
|
+
try:
|
|
57
|
+
if allow_rate_limit_wait:
|
|
58
|
+
self.io.apply_rate_limit(provider, config, upstream_model)
|
|
59
|
+
self.io.write_activity(
|
|
60
|
+
"advisor",
|
|
61
|
+
provider,
|
|
62
|
+
upstream_model,
|
|
63
|
+
tokens=self.io.estimate_tokens(request_body),
|
|
64
|
+
)
|
|
65
|
+
kind = self.policy.provider_kind(provider)
|
|
66
|
+
headers = (
|
|
67
|
+
self.io.headers(
|
|
68
|
+
provider,
|
|
69
|
+
config,
|
|
70
|
+
inbound_headers,
|
|
71
|
+
"anthropic_messages",
|
|
72
|
+
)
|
|
73
|
+
if kind == "anthropic"
|
|
74
|
+
else self.io.headers(provider, config)
|
|
75
|
+
)
|
|
76
|
+
timeout = (
|
|
77
|
+
self.io.ollama_timeout(config)
|
|
78
|
+
if kind == "ollama"
|
|
79
|
+
else self.io.provider_timeout(config)
|
|
80
|
+
)
|
|
81
|
+
data = self.io.post_json(
|
|
82
|
+
self.policy.endpoint(provider, config),
|
|
83
|
+
request_body,
|
|
84
|
+
headers,
|
|
85
|
+
timeout,
|
|
86
|
+
provider,
|
|
87
|
+
config,
|
|
88
|
+
upstream_model,
|
|
89
|
+
None,
|
|
90
|
+
retry_rate_limits=retry_rate_limits,
|
|
91
|
+
)
|
|
92
|
+
text = self.policy.response_text(provider, data)
|
|
93
|
+
if text:
|
|
94
|
+
self.io.log(
|
|
95
|
+
"INFO",
|
|
96
|
+
f"advisor_feedback provider={provider} "
|
|
97
|
+
f"advisor_model={upstream_model} chars={len(text)}",
|
|
98
|
+
)
|
|
99
|
+
return text
|
|
100
|
+
except Exception as exc:
|
|
101
|
+
self.io.log(
|
|
102
|
+
"WARN",
|
|
103
|
+
f"advisor_request_failed provider={provider} "
|
|
104
|
+
f"advisor_model={upstream_model} "
|
|
105
|
+
f"error={type(exc).__name__}: {exc}",
|
|
106
|
+
)
|
|
107
|
+
self.io.write_activity(
|
|
108
|
+
"advisor_error",
|
|
109
|
+
provider,
|
|
110
|
+
upstream_model,
|
|
111
|
+
error=type(exc).__name__,
|
|
112
|
+
)
|
|
113
|
+
if raise_errors:
|
|
114
|
+
raise
|
|
115
|
+
return ""
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
@dataclass(frozen=True, slots=True)
|
|
119
|
+
class ProviderChatPolicy:
|
|
120
|
+
normalize_thinking: Callable[..., dict[str, Any]]
|
|
121
|
+
normalize_tool_choice: Callable[..., dict[str, Any]]
|
|
122
|
+
provider_kind: Callable[..., str]
|
|
123
|
+
upstream_model: Callable[[str, dict[str, Any], str], str]
|
|
124
|
+
ollama_request: Callable[..., dict[str, Any]]
|
|
125
|
+
openai_request: Callable[..., dict[str, Any]]
|
|
126
|
+
ollama_response: Callable[..., dict[str, Any]]
|
|
127
|
+
openai_response: Callable[..., dict[str, Any]]
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
@dataclass(frozen=True, slots=True)
|
|
131
|
+
class ProviderChatIO:
|
|
132
|
+
apply_rate_limit: Callable[..., None]
|
|
133
|
+
post_json: Callable[..., Any]
|
|
134
|
+
join_url: Callable[[str, str], str]
|
|
135
|
+
request_base: Callable[[str, dict[str, Any]], str]
|
|
136
|
+
headers: Callable[..., dict[str, str]]
|
|
137
|
+
provider_timeout: Callable[[dict[str, Any]], float]
|
|
138
|
+
ollama_timeout: Callable[[dict[str, Any]], float]
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
class ProviderChatExecutor:
|
|
142
|
+
def __init__(self, policy: ProviderChatPolicy, io: ProviderChatIO) -> None:
|
|
143
|
+
self.policy = policy
|
|
144
|
+
self.io = io
|
|
145
|
+
|
|
146
|
+
def execute(
|
|
147
|
+
self,
|
|
148
|
+
provider: str,
|
|
149
|
+
config: dict[str, Any],
|
|
150
|
+
body: dict[str, Any],
|
|
151
|
+
model: str,
|
|
152
|
+
) -> dict[str, Any]:
|
|
153
|
+
body = self.policy.normalize_thinking(provider, config, body)
|
|
154
|
+
body = self.policy.normalize_tool_choice(provider, config, body)
|
|
155
|
+
transport = self.policy.provider_kind(provider, config)
|
|
156
|
+
if transport == "ollama":
|
|
157
|
+
request = self.policy.ollama_request(
|
|
158
|
+
model, body, config, stream=False, provider=provider
|
|
159
|
+
)
|
|
160
|
+
self.io.apply_rate_limit(provider, config, model)
|
|
161
|
+
data = self.io.post_json(
|
|
162
|
+
self.io.join_url(str(config.get("base_url") or "").rstrip("/"), "/api/chat"),
|
|
163
|
+
request,
|
|
164
|
+
self.io.headers(provider, config),
|
|
165
|
+
self.io.ollama_timeout(config),
|
|
166
|
+
provider,
|
|
167
|
+
config,
|
|
168
|
+
model,
|
|
169
|
+
None,
|
|
170
|
+
)
|
|
171
|
+
return self.policy.ollama_response(data, model, source_body=body)
|
|
172
|
+
if transport == "openai-compatible":
|
|
173
|
+
upstream_model = self.policy.upstream_model(provider, config, model)
|
|
174
|
+
request = self.policy.openai_request(
|
|
175
|
+
provider, upstream_model, body, config, stream=False
|
|
176
|
+
)
|
|
177
|
+
self.io.apply_rate_limit(provider, config, upstream_model)
|
|
178
|
+
data = self.io.post_json(
|
|
179
|
+
self.io.join_url(
|
|
180
|
+
self.io.request_base(provider, config), "/v1/chat/completions"
|
|
181
|
+
),
|
|
182
|
+
request,
|
|
183
|
+
self.io.headers(provider, config),
|
|
184
|
+
self.io.provider_timeout(config),
|
|
185
|
+
provider,
|
|
186
|
+
config,
|
|
187
|
+
upstream_model,
|
|
188
|
+
None,
|
|
189
|
+
)
|
|
190
|
+
return self.policy.openai_response(data, upstream_model, source_body=body)
|
|
191
|
+
raise RuntimeError(
|
|
192
|
+
f"Advisor refinement is not supported for provider {provider}"
|
|
193
|
+
)
|
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import Any, Callable
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@dataclass(frozen=True)
|
|
8
|
+
class AdvisorTextServices:
|
|
9
|
+
content_to_text: Callable[[Any], str]
|
|
10
|
+
tool_input_for_prompt: Callable[[Any], str]
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass(frozen=True)
|
|
14
|
+
class AdvisorDecisionServices:
|
|
15
|
+
advisor_enabled: Callable[[dict[str, Any]], bool]
|
|
16
|
+
is_advisor_request: Callable[[dict[str, Any]], bool]
|
|
17
|
+
completed_work: Callable[[dict[str, Any]], bool]
|
|
18
|
+
non_actionable_response: Callable[[str], bool]
|
|
19
|
+
plan_mode_active: Callable[[dict[str, Any]], bool]
|
|
20
|
+
provider_supported: Callable[[str], bool]
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass(frozen=True)
|
|
24
|
+
class AdvisorServices:
|
|
25
|
+
text: AdvisorTextServices
|
|
26
|
+
decisions: AdvisorDecisionServices
|
|
27
|
+
log: Callable[[str, str], None]
|
|
28
|
+
feedback_marker: str
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass(frozen=True, slots=True)
|
|
32
|
+
class AdvisorShortcutPorts:
|
|
33
|
+
should_intercept: Callable[[str, dict[str, Any]], bool]
|
|
34
|
+
is_request: Callable[[dict[str, Any]], bool]
|
|
35
|
+
provider_supported: Callable[[str], bool]
|
|
36
|
+
call_text: Callable[..., str]
|
|
37
|
+
write_anthropic: Callable[..., Any]
|
|
38
|
+
load_config: Callable[[], dict[str, Any]]
|
|
39
|
+
current_alias: Callable[[dict[str, Any]], str]
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@dataclass(frozen=True, slots=True)
|
|
43
|
+
class AdvisorShortcutController:
|
|
44
|
+
ports: AdvisorShortcutPorts
|
|
45
|
+
|
|
46
|
+
def handle(
|
|
47
|
+
self,
|
|
48
|
+
handler: Any,
|
|
49
|
+
provider: str,
|
|
50
|
+
provider_config: dict[str, Any],
|
|
51
|
+
body: dict[str, Any],
|
|
52
|
+
) -> bool:
|
|
53
|
+
if not self.ports.should_intercept(provider, provider_config):
|
|
54
|
+
return False
|
|
55
|
+
if not self.ports.is_request(body):
|
|
56
|
+
return False
|
|
57
|
+
advisor_model = str(provider_config.get("advisor_model") or "").strip()
|
|
58
|
+
stream = bool(body.get("stream", True))
|
|
59
|
+
if not advisor_model:
|
|
60
|
+
model = str(
|
|
61
|
+
body.get("model")
|
|
62
|
+
or self.ports.current_alias(self.ports.load_config())
|
|
63
|
+
)
|
|
64
|
+
self.ports.write_anthropic(
|
|
65
|
+
handler,
|
|
66
|
+
model,
|
|
67
|
+
"Advisor is off. Choose an Advisor Model in the ciel-runtime launch menu (item 5), or run `ciel-runtime advisor-model <model-id>`, then use `/advisor` again.",
|
|
68
|
+
stream,
|
|
69
|
+
)
|
|
70
|
+
return True
|
|
71
|
+
if not self.ports.provider_supported(provider):
|
|
72
|
+
self.ports.write_anthropic(
|
|
73
|
+
handler,
|
|
74
|
+
advisor_model,
|
|
75
|
+
f"Advisor Model is configured as `{advisor_model}`, but ciel-runtime advisor calling is not implemented for provider `{provider}`.",
|
|
76
|
+
stream,
|
|
77
|
+
)
|
|
78
|
+
return True
|
|
79
|
+
try:
|
|
80
|
+
text = self.ports.call_text(
|
|
81
|
+
provider,
|
|
82
|
+
provider_config,
|
|
83
|
+
body,
|
|
84
|
+
inbound_headers=handler.headers,
|
|
85
|
+
allow_rate_limit_wait=False,
|
|
86
|
+
retry_rate_limits=False,
|
|
87
|
+
raise_errors=True,
|
|
88
|
+
)
|
|
89
|
+
if not text:
|
|
90
|
+
text = "Advisor returned no text."
|
|
91
|
+
except Exception as exc:
|
|
92
|
+
text = f"Advisor request failed: {type(exc).__name__}: {exc}"
|
|
93
|
+
self.ports.write_anthropic(
|
|
94
|
+
handler, advisor_model, "Advisor guidance:\n\n" + text, stream
|
|
95
|
+
)
|
|
96
|
+
return True
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def advisor_messages_and_system(
|
|
100
|
+
body: dict[str, Any], services: AdvisorServices
|
|
101
|
+
) -> tuple[list[dict[str, Any]], list[str]]:
|
|
102
|
+
messages: list[dict[str, Any]] = []
|
|
103
|
+
system_texts: list[str] = []
|
|
104
|
+
for message in body.get("messages", []) or []:
|
|
105
|
+
if not isinstance(message, dict):
|
|
106
|
+
continue
|
|
107
|
+
role = str(message.get("role") or "").strip()
|
|
108
|
+
text = services.text.content_to_text(message.get("content")).strip()
|
|
109
|
+
if role == "system":
|
|
110
|
+
if text:
|
|
111
|
+
system_texts.append(text)
|
|
112
|
+
elif role in {"user", "assistant"}:
|
|
113
|
+
messages.append(message)
|
|
114
|
+
elif text:
|
|
115
|
+
messages.append(
|
|
116
|
+
{
|
|
117
|
+
"role": "user",
|
|
118
|
+
"content": [{"type": "text", "text": f"[{role or 'unknown'} message]\n{text}"}],
|
|
119
|
+
}
|
|
120
|
+
)
|
|
121
|
+
return messages, system_texts
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def advisor_tool_schema() -> dict[str, Any]:
|
|
125
|
+
return {
|
|
126
|
+
"name": "advisor",
|
|
127
|
+
"description": (
|
|
128
|
+
"Consult a second, stronger advisor model for an independent review before you make an important "
|
|
129
|
+
"plan, architecture, debugging, or final-completion decision. Use this when additional scrutiny could "
|
|
130
|
+
"catch gaps, risks, or better next steps."
|
|
131
|
+
),
|
|
132
|
+
"input_schema": {
|
|
133
|
+
"type": "object",
|
|
134
|
+
"properties": {
|
|
135
|
+
"question": {
|
|
136
|
+
"type": "string",
|
|
137
|
+
"description": "The specific decision, plan, code path, or risk you want the advisor to review.",
|
|
138
|
+
}
|
|
139
|
+
},
|
|
140
|
+
"required": ["question"],
|
|
141
|
+
},
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def body_with_advisor_tool(
|
|
146
|
+
body: dict[str, Any], provider_config: dict[str, Any], services: AdvisorServices
|
|
147
|
+
) -> dict[str, Any]:
|
|
148
|
+
if not services.decisions.advisor_enabled(provider_config):
|
|
149
|
+
return body
|
|
150
|
+
tools = body.get("tools") if isinstance(body.get("tools"), list) else []
|
|
151
|
+
if any(isinstance(tool, dict) and str(tool.get("name") or "") == "advisor" for tool in tools):
|
|
152
|
+
return body
|
|
153
|
+
output = dict(body)
|
|
154
|
+
output["tools"] = [*tools, advisor_tool_schema()]
|
|
155
|
+
return output
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def is_claude_code_advisor_server_tool(tool: Any) -> bool:
|
|
159
|
+
return bool(
|
|
160
|
+
isinstance(tool, dict)
|
|
161
|
+
and str(tool.get("name") or "") == "advisor"
|
|
162
|
+
and str(tool.get("type") or "").startswith("advisor_")
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def strip_autonomous_advisor_server_tools(
|
|
167
|
+
body: dict[str, Any],
|
|
168
|
+
services: AdvisorServices,
|
|
169
|
+
*,
|
|
170
|
+
server_tool_supported: bool,
|
|
171
|
+
) -> dict[str, Any]:
|
|
172
|
+
if server_tool_supported or services.decisions.is_advisor_request(body):
|
|
173
|
+
return body
|
|
174
|
+
if body_has_advisor_feedback(body, services):
|
|
175
|
+
return body
|
|
176
|
+
tools = body.get("tools")
|
|
177
|
+
if not isinstance(tools, list) or not tools:
|
|
178
|
+
return body
|
|
179
|
+
kept = [tool for tool in tools if not is_claude_code_advisor_server_tool(tool)]
|
|
180
|
+
removed = len(tools) - len(kept)
|
|
181
|
+
if not removed:
|
|
182
|
+
return body
|
|
183
|
+
output = dict(body)
|
|
184
|
+
output["tools"] = kept
|
|
185
|
+
services.log("INFO", f"stripped autonomous advisor server tool count={removed}")
|
|
186
|
+
return output
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def advisor_tool_focus_from_message(message: dict[str, Any]) -> str | None:
|
|
190
|
+
content = message.get("content")
|
|
191
|
+
if not isinstance(content, list):
|
|
192
|
+
return None
|
|
193
|
+
for block in content:
|
|
194
|
+
if not isinstance(block, dict) or block.get("type") != "tool_use":
|
|
195
|
+
continue
|
|
196
|
+
if str(block.get("name") or "") != "advisor":
|
|
197
|
+
continue
|
|
198
|
+
tool_input = block.get("input")
|
|
199
|
+
if isinstance(tool_input, dict):
|
|
200
|
+
for key in ("question", "prompt", "focus", "task"):
|
|
201
|
+
value = tool_input.get(key)
|
|
202
|
+
if isinstance(value, str) and value.strip():
|
|
203
|
+
return value.strip()
|
|
204
|
+
return "advisor tool call"
|
|
205
|
+
return None
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def tool_review_context_from_message(
|
|
209
|
+
message: dict[str, Any], trigger: str, services: AdvisorServices
|
|
210
|
+
) -> str:
|
|
211
|
+
content = message.get("content")
|
|
212
|
+
if not isinstance(content, list):
|
|
213
|
+
return ""
|
|
214
|
+
sections: list[str] = []
|
|
215
|
+
for block in content:
|
|
216
|
+
if not isinstance(block, dict) or block.get("type") != "tool_use":
|
|
217
|
+
continue
|
|
218
|
+
name = str(block.get("name") or "").strip()
|
|
219
|
+
if not name or name == "advisor":
|
|
220
|
+
continue
|
|
221
|
+
input_text = services.text.tool_input_for_prompt(block.get("input"))
|
|
222
|
+
if name == "ExitPlanMode":
|
|
223
|
+
sections.append(
|
|
224
|
+
"Review target: ExitPlanMode plan before user approval.\n"
|
|
225
|
+
"The executor is about to submit this plan. Review the actual plan/tool input below.\n"
|
|
226
|
+
f"Tool input:\n{input_text}"
|
|
227
|
+
)
|
|
228
|
+
elif trigger:
|
|
229
|
+
sections.append(f"Review target tool: {name} ({trigger}).\nTool input:\n{input_text}")
|
|
230
|
+
return "\n\n".join(sections)
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def advisor_focus_for_message(
|
|
234
|
+
message: dict[str, Any], trigger: str | None, services: AdvisorServices
|
|
235
|
+
) -> tuple[str | None, str | None]:
|
|
236
|
+
explicit_focus = advisor_tool_focus_from_message(message)
|
|
237
|
+
if explicit_focus:
|
|
238
|
+
return "advisor tool call", explicit_focus
|
|
239
|
+
if not trigger:
|
|
240
|
+
return None, None
|
|
241
|
+
review_context = tool_review_context_from_message(message, trigger, services)
|
|
242
|
+
return trigger, review_context or trigger
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def assistant_tool_call_summary_for_prompt(
|
|
246
|
+
message: dict[str, Any], services: AdvisorServices
|
|
247
|
+
) -> str:
|
|
248
|
+
content = message.get("content")
|
|
249
|
+
if not isinstance(content, list):
|
|
250
|
+
return ""
|
|
251
|
+
sections: list[str] = []
|
|
252
|
+
for block in content:
|
|
253
|
+
if not isinstance(block, dict) or block.get("type") != "tool_use":
|
|
254
|
+
continue
|
|
255
|
+
name = str(block.get("name") or "tool").strip() or "tool"
|
|
256
|
+
if name != "advisor":
|
|
257
|
+
sections.append(
|
|
258
|
+
f"Pending Claude Code tool call: {name}\nTool input:\n"
|
|
259
|
+
f"{services.text.tool_input_for_prompt(block.get('input'))}"
|
|
260
|
+
)
|
|
261
|
+
return "\n\n".join(sections)
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def body_has_advisor_feedback(body: dict[str, Any], services: AdvisorServices) -> bool:
|
|
265
|
+
for message in reversed(body.get("messages") or []):
|
|
266
|
+
if not isinstance(message, dict):
|
|
267
|
+
continue
|
|
268
|
+
if services.feedback_marker in services.text.content_to_text(message.get("content")):
|
|
269
|
+
return True
|
|
270
|
+
return False
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
def anthropic_message_tool_names(message: dict[str, Any]) -> list[str]:
|
|
274
|
+
content = message.get("content")
|
|
275
|
+
if not isinstance(content, list):
|
|
276
|
+
return []
|
|
277
|
+
names: list[str] = []
|
|
278
|
+
for block in content:
|
|
279
|
+
if isinstance(block, dict) and block.get("type") == "tool_use":
|
|
280
|
+
name = block.get("name")
|
|
281
|
+
if isinstance(name, str) and name:
|
|
282
|
+
names.append(name)
|
|
283
|
+
return names
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
def advisor_trigger_for_message(
|
|
287
|
+
body: dict[str, Any], message: dict[str, Any], services: AdvisorServices
|
|
288
|
+
) -> str | None:
|
|
289
|
+
names = set(anthropic_message_tool_names(message))
|
|
290
|
+
if "ExitPlanMode" in names:
|
|
291
|
+
return "before ExitPlanMode plan approval"
|
|
292
|
+
if names:
|
|
293
|
+
return None
|
|
294
|
+
text = services.text.content_to_text(message.get("content")).strip()
|
|
295
|
+
if (
|
|
296
|
+
text
|
|
297
|
+
and message.get("stop_reason") == "end_turn"
|
|
298
|
+
and services.decisions.completed_work(body)
|
|
299
|
+
and not services.decisions.non_actionable_response(text)
|
|
300
|
+
):
|
|
301
|
+
return "before completion/final response"
|
|
302
|
+
return None
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
def advisor_gate_reason_for_body(
|
|
306
|
+
provider: str,
|
|
307
|
+
provider_config: dict[str, Any],
|
|
308
|
+
body: dict[str, Any],
|
|
309
|
+
services: AdvisorServices,
|
|
310
|
+
) -> str:
|
|
311
|
+
decisions = services.decisions
|
|
312
|
+
if not decisions.advisor_enabled(provider_config) or not decisions.provider_supported(provider):
|
|
313
|
+
return ""
|
|
314
|
+
if decisions.is_advisor_request(body) or body_has_advisor_feedback(body, services):
|
|
315
|
+
return ""
|
|
316
|
+
if decisions.plan_mode_active(body):
|
|
317
|
+
return "plan_mode"
|
|
318
|
+
if decisions.completed_work(body):
|
|
319
|
+
return "completed_work"
|
|
320
|
+
return ""
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
"""Apply Advisor feedback as a bounded refinement decorator around a response."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
from collections.abc import Callable
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass(frozen=True, slots=True)
|
|
12
|
+
class AdvisorRefinementText:
|
|
13
|
+
content_text: Callable[[Any], str]
|
|
14
|
+
tool_names: Callable[[dict[str, Any]], list[str]]
|
|
15
|
+
tool_summary: Callable[[dict[str, Any]], str]
|
|
16
|
+
compact_text: Callable[[str], str]
|
|
17
|
+
prepend_text: Callable[[dict[str, Any], str], dict[str, Any]]
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass(frozen=True, slots=True)
|
|
21
|
+
class AdvisorRefinementPolicy:
|
|
22
|
+
model_enabled: Callable[[dict[str, Any]], bool]
|
|
23
|
+
provider_supported: Callable[[str], bool]
|
|
24
|
+
is_advisor_request: Callable[[dict[str, Any]], bool]
|
|
25
|
+
has_feedback: Callable[[dict[str, Any]], bool]
|
|
26
|
+
trigger: Callable[[dict[str, Any], dict[str, Any]], str]
|
|
27
|
+
focus: Callable[[dict[str, Any], str], tuple[str, str]]
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass(frozen=True, slots=True)
|
|
31
|
+
class AdvisorRefinementIO:
|
|
32
|
+
call_advisor: Callable[..., str]
|
|
33
|
+
call_provider: Callable[..., dict[str, Any]]
|
|
34
|
+
log: Callable[[str, str], None]
|
|
35
|
+
write_activity: Callable[..., None]
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class AdvisorRefinementService:
|
|
39
|
+
def __init__(
|
|
40
|
+
self,
|
|
41
|
+
feedback_marker: str,
|
|
42
|
+
text: AdvisorRefinementText,
|
|
43
|
+
policy: AdvisorRefinementPolicy,
|
|
44
|
+
io: AdvisorRefinementIO,
|
|
45
|
+
) -> None:
|
|
46
|
+
self.feedback_marker = feedback_marker
|
|
47
|
+
self.text = text
|
|
48
|
+
self.policy = policy
|
|
49
|
+
self.io = io
|
|
50
|
+
|
|
51
|
+
def body_with_feedback(
|
|
52
|
+
self,
|
|
53
|
+
body: dict[str, Any],
|
|
54
|
+
assistant_message: dict[str, Any],
|
|
55
|
+
advisor_text: str,
|
|
56
|
+
trigger: str,
|
|
57
|
+
) -> dict[str, Any]:
|
|
58
|
+
messages = [item for item in body.get("messages", []) if isinstance(item, dict)]
|
|
59
|
+
assistant_text = self.text.content_text(assistant_message.get("content")).strip()
|
|
60
|
+
tool_names = self.text.tool_names(assistant_message)
|
|
61
|
+
tool_summary = self.text.tool_summary(assistant_message)
|
|
62
|
+
if assistant_text:
|
|
63
|
+
summary = self.text.compact_text(assistant_text)
|
|
64
|
+
elif tool_summary:
|
|
65
|
+
summary = self.text.compact_text(tool_summary)
|
|
66
|
+
elif tool_names:
|
|
67
|
+
summary = (
|
|
68
|
+
"I was about to call Claude Code tool(s): "
|
|
69
|
+
+ ", ".join(tool_names)
|
|
70
|
+
+ "."
|
|
71
|
+
)
|
|
72
|
+
else:
|
|
73
|
+
summary = ""
|
|
74
|
+
if summary:
|
|
75
|
+
messages.append(
|
|
76
|
+
{"role": "assistant", "content": [{"type": "text", "text": summary}]}
|
|
77
|
+
)
|
|
78
|
+
feedback = (
|
|
79
|
+
f"{self.feedback_marker}\n"
|
|
80
|
+
f"Advisor review trigger: {trigger}.\n\n"
|
|
81
|
+
f"{advisor_text}\n\n"
|
|
82
|
+
"Apply this advisor feedback now. If the plan is still ready, call "
|
|
83
|
+
"ExitPlanMode with the improved plan. If the task is complete, provide "
|
|
84
|
+
"the final answer. If the advisor found a gap, continue with concrete "
|
|
85
|
+
"Claude Code tool calls instead of stopping after an announcement."
|
|
86
|
+
)
|
|
87
|
+
messages.append(
|
|
88
|
+
{"role": "user", "content": [{"type": "text", "text": feedback}]}
|
|
89
|
+
)
|
|
90
|
+
projected = dict(body)
|
|
91
|
+
projected["messages"] = messages
|
|
92
|
+
projected.pop("tool_choice", None)
|
|
93
|
+
return projected
|
|
94
|
+
|
|
95
|
+
@staticmethod
|
|
96
|
+
def visible_summary(advisor_text: str, trigger: str, limit: int = 700) -> str:
|
|
97
|
+
text = re.sub(r"\s+", " ", str(advisor_text or "")).strip()
|
|
98
|
+
if not text:
|
|
99
|
+
return ""
|
|
100
|
+
if len(text) > limit:
|
|
101
|
+
text = text[: max(0, limit - 1)].rstrip() + "…"
|
|
102
|
+
return f"Advisor review ({trigger}): {text}\n\n"
|
|
103
|
+
|
|
104
|
+
def refine(
|
|
105
|
+
self,
|
|
106
|
+
provider: str,
|
|
107
|
+
config: dict[str, Any],
|
|
108
|
+
original_body: dict[str, Any],
|
|
109
|
+
message: dict[str, Any],
|
|
110
|
+
main_model: str,
|
|
111
|
+
) -> dict[str, Any]:
|
|
112
|
+
if not self.policy.model_enabled(config):
|
|
113
|
+
return message
|
|
114
|
+
if not self.policy.provider_supported(provider):
|
|
115
|
+
return message
|
|
116
|
+
if self.policy.is_advisor_request(original_body) or self.policy.has_feedback(
|
|
117
|
+
original_body
|
|
118
|
+
):
|
|
119
|
+
return message
|
|
120
|
+
trigger = self.policy.trigger(original_body, message)
|
|
121
|
+
trigger, advisor_focus = self.policy.focus(message, trigger)
|
|
122
|
+
if not trigger:
|
|
123
|
+
return message
|
|
124
|
+
advisor_text = self.io.call_advisor(
|
|
125
|
+
provider,
|
|
126
|
+
config,
|
|
127
|
+
original_body,
|
|
128
|
+
focus=advisor_focus or trigger,
|
|
129
|
+
)
|
|
130
|
+
if not advisor_text:
|
|
131
|
+
return message
|
|
132
|
+
follow_body = self.body_with_feedback(
|
|
133
|
+
original_body, message, advisor_text, trigger
|
|
134
|
+
)
|
|
135
|
+
summary = self.visible_summary(advisor_text, trigger)
|
|
136
|
+
try:
|
|
137
|
+
self.io.log(
|
|
138
|
+
"INFO",
|
|
139
|
+
f"advisor_refinement_call trigger={trigger} main_model={main_model}",
|
|
140
|
+
)
|
|
141
|
+
refined = self.io.call_provider(provider, config, follow_body, main_model)
|
|
142
|
+
self.io.log(
|
|
143
|
+
"INFO",
|
|
144
|
+
f"advisor_refinement_done trigger={trigger} "
|
|
145
|
+
f"stop_reason={refined.get('stop_reason')}",
|
|
146
|
+
)
|
|
147
|
+
return self.text.prepend_text(refined, summary)
|
|
148
|
+
except Exception as exc:
|
|
149
|
+
self.io.log(
|
|
150
|
+
"WARN",
|
|
151
|
+
f"advisor_refinement_failed trigger={trigger} model={main_model} "
|
|
152
|
+
f"error={type(exc).__name__}: {exc}",
|
|
153
|
+
)
|
|
154
|
+
self.io.write_activity(
|
|
155
|
+
"advisor_refinement_error",
|
|
156
|
+
provider,
|
|
157
|
+
main_model,
|
|
158
|
+
error=type(exc).__name__,
|
|
159
|
+
)
|
|
160
|
+
return self.text.prepend_text(message, summary)
|