@oneciel-ai/ciel-runtime 0.2.22 → 0.2.23
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/CHANGELOG.md +108 -0
- package/README.md +150 -57
- package/ciel-runtime-stop.cmd +1 -0
- package/ciel-runtime.cmd +1 -0
- package/ciel-runtimectl.cmd +1 -0
- package/ciel_runtime.py +56 -53
- package/ciel_runtime_support/anthropic_tool_turns.py +81 -0
- package/ciel_runtime_support/architecture.py +17 -0
- package/ciel_runtime_support/channel_injection.py +113 -3
- package/ciel_runtime_support/channel_message_prompt.py +15 -0
- package/ciel_runtime_support/channel_pending_injection.py +28 -4
- package/ciel_runtime_support/channel_pending_poll.py +2 -0
- package/ciel_runtime_support/channel_terminal_context.py +6 -0
- package/ciel_runtime_support/channel_terminal_dispatch.py +2 -0
- package/ciel_runtime_support/channel_terminal_proxy.py +8 -2
- package/ciel_runtime_support/channel_wake_claim_repository.py +7 -0
- package/ciel_runtime_support/channel_wake_context.py +7 -3
- package/ciel_runtime_support/cli_parser.py +6 -0
- package/ciel_runtime_support/codex_backend_context.py +4 -0
- package/ciel_runtime_support/codex_turn_recovery.py +75 -12
- package/ciel_runtime_support/config_migrations.py +102 -10
- package/ciel_runtime_support/config_repository.py +9 -0
- package/ciel_runtime_support/event_settings_cli.py +284 -2
- package/ciel_runtime_support/openai_chat_passthrough.py +2 -0
- package/ciel_runtime_support/protocols/openai_responses.py +22 -18
- package/ciel_runtime_support/provider_adapters.py +7 -0
- package/ciel_runtime_support/provider_policy.py +10 -1
- package/ciel_runtime_support/provider_request_builder.py +9 -2
- package/ciel_runtime_support/provider_responses_passthrough.py +4 -0
- package/ciel_runtime_support/providers/__init__.py +2 -0
- package/ciel_runtime_support/providers/alibaba.py +33 -10
- package/ciel_runtime_support/providers/constants.py +1 -0
- package/ciel_runtime_support/providers/opencode.py +25 -5
- package/ciel_runtime_support/providers/opencode_go.py +3 -3
- package/ciel_runtime_support/providers/openrouter.py +76 -3
- package/ciel_runtime_support/providers/tabitoken.py +130 -0
- package/ciel_runtime_support/remote_instructions.py +20 -6
- package/ciel_runtime_support/remote_memory.py +515 -55
- package/ciel_runtime_support/responses_input_compatibility.py +14 -0
- package/ciel_runtime_support/router_http.py +5 -0
- package/ciel_runtime_support/runtime_constants.py +1 -1
- package/ciel_runtime_support/runtime_launch.py +9 -2
- package/ciel_runtime_support/runtime_paths.py +2 -0
- package/ciel_runtime_support/tool_side_effect_dedupe.py +7 -4
- package/ciel_runtime_support/transcript_delta_delivery.py +333 -0
- package/ciel_runtime_support/usage_events.py +45 -3
- package/ciel_runtime_support/usage_service.py +871 -0
- package/ciel_runtime_support/windows_conpty.py +108 -2
- package/ciel_runtime_support/workspace_mcp.py +4 -0
- package/docs/CLI-Reference.md +11 -0
- package/docs/Configuration.md +7 -2
- package/docs/MCP-Channels.md +7 -3
- package/docs/Module-Map.md +4 -0
- package/docs/Providers.md +32 -0
- package/docs/Remote-Memory.md +29 -10
- package/docs/Test-Suite.md +1 -0
- package/docs/journal/2026/08/20/kevin-wake-orphan-tool-repair.md +161 -0
- package/docs/journal/2026/08/20/nightly-push-local-deployment.md +59 -2
- package/docs/journal/2026/08/20/wing-remote-memory-replication-verification.md +107 -0
- package/docs/journal/2026/08/21/alibaba-singapore-qwen38-official-alignment.md +64 -0
- package/docs/journal/2026/08/21/codex-visible-sse-wake-submit-regression.md +85 -0
- package/docs/journal/2026/08/21/external-sse-wake-only-change-analysis.md +131 -0
- package/docs/journal/2026/08/21/hyundai-invalid-replayed-tool-name-repair.okf +88 -0
- package/docs/journal/2026/08/21/local-stale-temporary-runtime-pin-repair.okf +118 -0
- package/docs/journal/2026/08/21/transcript-compaction-external-events.okf +131 -0
- package/docs/journal/2026/08/21/wing-ai-net-mcp-boundary-diagnosis.okf +129 -0
- package/docs/journal/2026/08/21/wing-cold-start-wake-submit-repair.okf +161 -0
- package/docs/journal/2026/08/21/wing-powershell-memory-runaway.okf +101 -0
- package/docs/journal/2026/08/21/wing-repeated-reasoning-tool-turn-repair.okf +112 -0
- package/docs/journal/2026/08/21/wing-repeated-tool-guard-finalization.okf +81 -0
- package/docs/journal/2026/08/21/wing-working-directory-memory-deployment.md +110 -0
- package/docs/journal/2026/08/21/wing-workspace-state-memory-prompt-injection.md +88 -0
- package/docs/journal/2026/08/22/alibaba-singapore-dual-endpoint-wire-alignment.okf +142 -0
- package/docs/journal/2026/08/22/delluhiold-claude-compaction-empty-tool-name.okf +168 -0
- package/docs/journal/2026/08/22/remote-memory-relative-prompt-paths.okf +79 -0
- package/docs/journal/2026/08/22/remote-memory-root-system-prompt.okf +121 -0
- package/docs/journal/2026/08/23/claude-web-chat-stateless-mcp-repair.okf +97 -0
- package/docs/journal/2026/08/23/cross-runtime-token-usage-interfaces-research.okf +378 -0
- package/docs/journal/2026/08/23/main-0.2.23-readme-release.okf +81 -0
- package/docs/journal/2026/08/23/mia-openrouter-ox-alpha-latency-diagnosis.okf +146 -0
- package/docs/journal/2026/08/23/openrouter-opencode-ox-alpha-support.okf +114 -0
- package/docs/journal/2026/08/23/tabitoken-provider-model-catalog.okf +97 -0
- package/docs/journal/2026/08/23/usage-stream-snapshot-implementation.okf +134 -0
- package/docs/usage-observability.md +141 -0
- package/install.ps1 +24 -3
- package/package.json +2 -1
package/ciel_runtime.py
CHANGED
|
@@ -45,7 +45,7 @@ from ciel_runtime_support.agy_installer import AgyInstaller
|
|
|
45
45
|
from ciel_runtime_support.anthropic_response_writer import AnthropicResponseWriter
|
|
46
46
|
from ciel_runtime_support.anthropic_response_writer import anthropic_text_response as project_anthropic_text_response
|
|
47
47
|
from ciel_runtime_support.anthropic_response_writer import prepend_anthropic_text as project_prepend_anthropic_text
|
|
48
|
-
from ciel_runtime_support.anthropic_tool_turns import AnthropicToolTurnServices, normalize_historical_anthropic_tool_turns
|
|
48
|
+
from ciel_runtime_support.anthropic_tool_turns import AnthropicToolTurnServices, normalize_historical_anthropic_tool_turns, sanitize_invalid_anthropic_tool_history
|
|
49
49
|
from ciel_runtime_support.api_key_cooldown import API_KEY_COOLDOWN_DEFAULT_SECONDS # noqa: F401 - compatibility export
|
|
50
50
|
from ciel_runtime_support.api_key_cooldown import API_KEY_COOLDOWN_MAX_SECONDS # noqa: F401 - compatibility export
|
|
51
51
|
from ciel_runtime_support.api_key_cooldown import RATE_LIMIT_RESET_HEADER_NAMES as _RATE_LIMIT_RESET_HEADER_NAMES # noqa: F401 - compatibility export
|
|
@@ -73,8 +73,7 @@ from ciel_runtime_support.channel_message_policy import message_has_external_pro
|
|
|
73
73
|
from ciel_runtime_support.channel_message_policy import message_is_web_chat_request as _channel_message_is_web_chat_request
|
|
74
74
|
from ciel_runtime_support.channel_message_policy import string_list as _as_string_list
|
|
75
75
|
from ciel_runtime_support.channel_message_policy import superseded_message_ids as _channel_superseded_message_ids
|
|
76
|
-
from ciel_runtime_support.channel_message_prompt import format_llm_batch_prompt as format_channel_llm_batch_prompt
|
|
77
|
-
from ciel_runtime_support.channel_message_prompt import format_llm_delivery_wake_prompt as format_channel_llm_delivery_wake_prompt
|
|
76
|
+
from ciel_runtime_support.channel_message_prompt import format_llm_batch_prompt as format_channel_llm_batch_prompt, format_llm_delivery_wake_prompt as format_channel_llm_delivery_wake_prompt, format_visible_llm_delivery_wake_prompt as format_channel_visible_llm_delivery_wake_prompt
|
|
78
77
|
from ciel_runtime_support.channel_message_prompt import format_wake_batch_prompt as format_channel_wake_batch_prompt
|
|
79
78
|
from ciel_runtime_support.channel_message_prompt import format_wake_prompt as format_channel_wake_prompt # noqa: F401 - compatibility export
|
|
80
79
|
from ciel_runtime_support.channel_message_prompt import format_web_chat_wake_batch_prompt as format_channel_web_chat_wake_batch_prompt
|
|
@@ -103,6 +102,7 @@ from ciel_runtime_support.channel_transcript import queued_age_seconds_from_text
|
|
|
103
102
|
from ciel_runtime_support.channel_transcript import queued_command_ids_from_text as analyze_channel_queued_ids
|
|
104
103
|
from ciel_runtime_support.channel_transcript import wake_state_evidence_from_text as analyze_channel_wake_evidence
|
|
105
104
|
from ciel_runtime_support.channel_transcript_repository import ChannelTranscriptRepository
|
|
105
|
+
from ciel_runtime_support.transcript_delta_delivery import TranscriptDeliveryPorts, TranscriptDeltaDeliveryService
|
|
106
106
|
from ciel_runtime_support.channel_wake_claim_repository import ChannelWakeClaimRepository
|
|
107
107
|
from ciel_runtime_support.channel_wake_claim_repository import prompt_message_ids as _channel_prompt_message_ids
|
|
108
108
|
from ciel_runtime_support.channel_wake_claim_repository import prompt_references_message_id as analyze_prompt_message_reference
|
|
@@ -244,7 +244,7 @@ from ciel_runtime_support.prompt_injection import normalize_anthropic_system_rol
|
|
|
244
244
|
from ciel_runtime_support.prompt_injection import normalize_anthropic_system_role_messages_by_strategy as project_normalize_anthropic_system_role_messages_by_strategy
|
|
245
245
|
from ciel_runtime_support.remote_instructions import RemoteInstructionResult, RemoteInstructionSynchronizer, SynchronizedLaunch
|
|
246
246
|
from ciel_runtime_support.remote_instructions import panel_rows as project_remote_instruction_panel_rows
|
|
247
|
-
from ciel_runtime_support.remote_memory import RemoteMemoryResult, RemoteMemorySynchronizer, sync_all_memory_pointers as project_sync_all_memory_pointers, sync_instruction_with_memory_pointer as project_sync_instruction_with_memory_pointer, sync_launch_assets as project_sync_launch_assets
|
|
247
|
+
from ciel_runtime_support.remote_memory import RemoteMemoryResult, RemoteMemorySynchronizer, current_memory_prompt as project_current_memory_prompt, inject_current_memory_prompt as project_inject_current_memory_prompt, move_memory_pointer_to_system_end as project_move_memory_pointer_to_system_end, sync_all_memory_pointers as project_sync_all_memory_pointers, sync_instruction_with_memory_pointer as project_sync_instruction_with_memory_pointer, sync_launch_assets as project_sync_launch_assets, without_memory_pointer as project_without_memory_pointer
|
|
248
248
|
from ciel_runtime_support.protocols import PROTOCOL_ADAPTERS
|
|
249
249
|
from ciel_runtime_support.protocols.anthropic_content import content_to_text as anthropic_content_to_text
|
|
250
250
|
from ciel_runtime_support.protocols.anthropic_thinking_policy import AnthropicThinkingPolicy, SuppressedThinkingRepository, ThinkingPolicyPorts
|
|
@@ -509,7 +509,8 @@ from ciel_runtime_support.upstream_stream_io import iter_lines_until_disconnect
|
|
|
509
509
|
from ciel_runtime_support.upstream_stream_io import set_stream_read_timeout as project_set_stream_read_timeout
|
|
510
510
|
from ciel_runtime_support.upstream_stream_io import sleep_until_disconnect as project_sleep_until_disconnect
|
|
511
511
|
from ciel_runtime_support.upstream_stream_io import stream_idle_timeout as project_stream_idle_timeout
|
|
512
|
-
from ciel_runtime_support.usage_events import JsonlUsageEventSink
|
|
512
|
+
from ciel_runtime_support.usage_events import CompositeUsageEventSink, JsonlUsageEventSink
|
|
513
|
+
from ciel_runtime_support.usage_service import SqliteUsageLedger, UsageApiKeyRepository, UsageHttpAdapter, UsagePushDeliveryService, UsageRuntimeServices, usage_jsonl_enabled
|
|
513
514
|
from ciel_runtime_support.visible_stream_filters import VISIBLE_THINKING_MARKUP_PREFIXES # noqa: F401 - compatibility export
|
|
514
515
|
from ciel_runtime_support.visible_stream_filters import VISIBLE_THINKING_MARKUP_TAG_RE # noqa: F401 - compatibility export
|
|
515
516
|
from ciel_runtime_support.visible_stream_filters import VISIBLE_TOOL_CALL_ARTIFACT_HOLD_CHARS # noqa: F401 - compatibility export
|
|
@@ -586,17 +587,12 @@ _CHANNEL_COMPACT_REQUEST_LOCK = threading.Lock()
|
|
|
586
587
|
_TOOL_SIDE_EFFECT_DEDUP_TTL_SECONDS = 10 * 60.0
|
|
587
588
|
_TOOL_SIDE_EFFECT_DEDUP_LOCK = threading.Lock()
|
|
588
589
|
_TOOL_SIDE_EFFECT_DEDUP_RECENT: dict[str, float] = {}
|
|
589
|
-
EVENT_BUS = EventBus()
|
|
590
|
-
|
|
591
|
-
USAGE_EVENT_SINK = JsonlUsageEventSink(
|
|
592
|
-
USAGE_EVENTS_PATH,
|
|
593
|
-
enabled=lambda: str(os.environ.get("CIEL_RUNTIME_USAGE_LOG", "1")).strip().lower()
|
|
594
|
-
not in {"0", "false", "off", "no", ""},
|
|
595
|
-
)
|
|
590
|
+
EVENT_BUS, TUI_OBSERVATION_BUS = EventBus(), TuiObservationBus()
|
|
591
|
+
USAGE_API_KEYS = UsageApiKeyRepository(USAGE_LEDGER := SqliteUsageLedger(WORKSPACE_STATE_DIR / "usage" / "usage.sqlite3", ROUTER_WORKSPACE_ID), WORKSPACE_STATE_DIR / "usage" / "api-key.pepper", os.environ)
|
|
592
|
+
USAGE_EVENT_SINK = CompositeUsageEventSink(JsonlUsageEventSink(USAGE_EVENTS_PATH, enabled=lambda: usage_jsonl_enabled(load_config(), os.environ)), USAGE_LEDGER)
|
|
596
593
|
# Tools Claude Code injects into every model's tool list that misfire when called
|
|
597
594
|
# by non-Anthropic models. See docs/notes from anthropics/claude-code issues
|
|
598
595
|
# #25720, #29950 and Piebald-AI/claude-code-system-prompts for tool semantics.
|
|
599
|
-
|
|
600
596
|
def positive_env_int(name: str, default: int) -> int: return runtime_primitives.positive_environment_int(os.environ, name, default)
|
|
601
597
|
SUPPRESSED_THINKING_PASSBACK_MAX = positive_env_int("CIEL_RUNTIME_THINKING_PASSBACK_MAX", 4096)
|
|
602
598
|
SUPPRESSED_THINKING_PASSBACK_CACHE: list[dict[str, Any]] = []
|
|
@@ -934,9 +930,9 @@ def select_provider_protocol(provider: str, pcfg: dict[str, Any], operation: Mes
|
|
|
934
930
|
adapter = configured_provider_adapter(provider, pcfg)
|
|
935
931
|
return adapter.select_protocol(operation, provider_contract_config(provider, pcfg), model)
|
|
936
932
|
|
|
937
|
-
def apply_provider_adapter_request_policy(provider: str, pcfg: dict[str, Any], body: dict[str, Any]) -> dict[str, Any]:
|
|
933
|
+
def apply_provider_adapter_request_policy(provider: str, pcfg: dict[str, Any], body: dict[str, Any], protocol: MessageProtocol | None = None) -> dict[str, Any]:
|
|
938
934
|
adapter = configured_provider_adapter(provider, pcfg)
|
|
939
|
-
normalized = adapter.
|
|
935
|
+
normalized = adapter.normalize_request_options_for_protocol(provider_contract_config(provider, pcfg), body, protocol)
|
|
940
936
|
return dict(normalized)
|
|
941
937
|
|
|
942
938
|
def provider_has_api_key(provider: str, pcfg: dict[str, Any]) -> bool: return bool(provider_config_api_keys(provider, pcfg))
|
|
@@ -1727,10 +1723,12 @@ def parse_json_body(raw: bytes) -> dict[str, Any]:
|
|
|
1727
1723
|
except Exception:
|
|
1728
1724
|
return {}
|
|
1729
1725
|
return value if isinstance(value, dict) else {}
|
|
1730
|
-
|
|
1731
1726
|
query_int = EventHttpAdapter.query_int
|
|
1732
1727
|
def event_http_adapter() -> EventHttpAdapter: return EventHttpAdapter(EventHttpPorts(EVENT_BUS.recent, EVENT_BUS.wait_after, render_events_html, write_text_response, write_json, router_log))
|
|
1733
1728
|
def handle_events_get(handler: BaseHTTPRequestHandler, path: str, query: dict[str, list[str]]) -> bool: return event_http_adapter().handle_get(handler, path, query)
|
|
1729
|
+
_USAGE_HTTP_ADAPTER = UsageHttpAdapter(USAGE_LEDGER, USAGE_API_KEYS, write_json, reject_external_router_request, load_config, router_log)
|
|
1730
|
+
def handle_usage_post(handler: BaseHTTPRequestHandler, path: str, body: dict[str, Any]) -> bool: return _USAGE_HTTP_ADAPTER.handle_post(handler, path, body)
|
|
1731
|
+
def handle_observability_get(handler: BaseHTTPRequestHandler, path: str, query: dict[str, list[str]]) -> bool: return _USAGE_HTTP_ADAPTER.handle_get(handler, path, query) or handle_events_get(handler, path, query)
|
|
1734
1732
|
def tui_observation_http_adapter() -> TuiObservationHttpAdapter: return TuiObservationHttpAdapter(TuiObservationHttpPorts(TUI_OBSERVATION_BUS, write_json, write_text_response, router_log))
|
|
1735
1733
|
def handle_tui_observation_get(handler: BaseHTTPRequestHandler, path: str, query: dict[str, list[str]]) -> bool: return tui_observation_http_adapter().handle_get(handler, path, query)
|
|
1736
1734
|
def observe_tui_runtime_response(handler: BaseHTTPRequestHandler, path: str, provider: str, model: str, body: dict[str, Any]) -> Any: return observe_runtime_response(handler, path, provider, model, body, TUI_OBSERVATION_BUS)
|
|
@@ -1946,14 +1944,14 @@ def set_external_event_config(key: str, value: Any) -> list[str]:
|
|
|
1946
1944
|
"External events use the private Runtime Input Gateway and are never published to Web Chat.",
|
|
1947
1945
|
]
|
|
1948
1946
|
|
|
1949
|
-
def remote_instruction_synchronizer() -> RemoteInstructionSynchronizer: return RemoteInstructionSynchronizer(load_config=load_config, workspace=Path
|
|
1950
|
-
|
|
1951
|
-
def
|
|
1947
|
+
def remote_instruction_synchronizer() -> RemoteInstructionSynchronizer: return RemoteInstructionSynchronizer(load_config=load_config, workspace=lambda: Path(ROUTER_WORKSPACE), state_dir=WORKSPACE_STATE_DIR, log=router_log)
|
|
1948
|
+
def remote_memory_synchronizer() -> RemoteMemorySynchronizer: return RemoteMemorySynchronizer(load_config=load_config, workspace=lambda: Path(ROUTER_WORKSPACE), state_dir=WORKSPACE_STATE_DIR, log=router_log)
|
|
1949
|
+
def body_with_remote_memory_prompt(body: dict[str, Any], protocol: MessageProtocol) -> dict[str, Any]: return project_inject_current_memory_prompt(body, protocol, WORKSPACE_STATE_DIR, load_config(), Path(ROUTER_WORKSPACE))
|
|
1950
|
+
def finalized_anthropic_upstream_body(body: dict[str, Any]) -> dict[str, Any]: return body_without_ciel_runtime_internal_metadata(body_with_remote_memory_prompt(body, "anthropic_messages"))
|
|
1952
1951
|
def sync_remote_instruction(runtime: str, *, reason: str) -> RemoteInstructionResult: return project_sync_instruction_with_memory_pointer(runtime, reason=reason, instruction_synchronizer=remote_instruction_synchronizer, memory_synchronizer=remote_memory_synchronizer, log=router_log)
|
|
1953
1952
|
def sync_remote_memory(runtime: str, *, reason: str) -> RemoteMemoryResult: return remote_memory_synchronizer().sync(runtime, reason=reason)
|
|
1954
1953
|
def sync_remote_launch_assets(runtime: str, *, reason: str) -> RemoteMemoryResult: return project_sync_launch_assets(runtime, reason=reason, instruction_sync=sync_remote_instruction, memory_sync=sync_remote_memory)
|
|
1955
1954
|
def sync_all_remote_memories() -> list[str]: return project_sync_all_memory_pointers(remote_memory_synchronizer())
|
|
1956
|
-
|
|
1957
1955
|
def remote_instruction_panel_rows(cfg: dict[str, Any]) -> tuple[list[str], list[str]]:
|
|
1958
1956
|
return project_remote_instruction_panel_rows(cfg)
|
|
1959
1957
|
|
|
@@ -2006,7 +2004,7 @@ def _latest_remote_instruction(runtime: str, *, reason: str) -> str:
|
|
|
2006
2004
|
if path is None or not path.is_file():
|
|
2007
2005
|
return ""
|
|
2008
2006
|
try:
|
|
2009
|
-
return path.read_text(encoding="utf-8")
|
|
2007
|
+
return project_without_memory_pointer(path.read_text(encoding="utf-8"))
|
|
2010
2008
|
except OSError as exc:
|
|
2011
2009
|
router_log("WARN", f"remote_instructions_read_failed runtime={runtime} error={type(exc).__name__}: {exc}")
|
|
2012
2010
|
return ""
|
|
@@ -2047,7 +2045,7 @@ def _refresh_anthropic_compact_body(body: dict[str, Any], runtime: str = "claude
|
|
|
2047
2045
|
_without_remote_instruction_blocks(body.get("system")),
|
|
2048
2046
|
[_with_latest_instruction_text("", instruction)],
|
|
2049
2047
|
)
|
|
2050
|
-
return updated
|
|
2048
|
+
return body_with_remote_memory_prompt(updated, "anthropic_messages")
|
|
2051
2049
|
|
|
2052
2050
|
def _refresh_chat_compact_messages(messages: list[dict[str, Any]], runtime: str = "claude") -> list[dict[str, Any]]:
|
|
2053
2051
|
instruction = _latest_remote_instruction(runtime, reason="pre-compact")
|
|
@@ -2057,15 +2055,17 @@ def _refresh_chat_compact_messages(messages: list[dict[str, Any]], runtime: str
|
|
|
2057
2055
|
for message in updated:
|
|
2058
2056
|
if str(message.get("role") or "") == "system" and isinstance(message.get("content"), str):
|
|
2059
2057
|
message["content"] = _with_latest_instruction_text(message.get("content"), instruction)
|
|
2060
|
-
|
|
2061
|
-
|
|
2058
|
+
break
|
|
2059
|
+
else:
|
|
2060
|
+
updated = [{"role": "system", "content": _with_latest_instruction_text("", instruction)}, *updated]
|
|
2061
|
+
return project_move_memory_pointer_to_system_end(updated, project_current_memory_prompt(WORKSPACE_STATE_DIR, load_config(), Path(ROUTER_WORKSPACE)))
|
|
2062
2062
|
|
|
2063
2063
|
def compact_responses_with_remote_instruction(body: dict[str, Any], budget: int, **kwargs: Any) -> dict[str, Any]:
|
|
2064
2064
|
instruction = _latest_remote_instruction("codex", reason="pre-compact")
|
|
2065
2065
|
updated = dict(body)
|
|
2066
2066
|
if instruction:
|
|
2067
2067
|
updated["instructions"] = _with_latest_instruction_text(body.get("instructions"), instruction)
|
|
2068
|
-
return run_responses_prompt_compaction(updated, budget, services=prompt_compaction_services(), **kwargs)
|
|
2068
|
+
return body_with_remote_memory_prompt(run_responses_prompt_compaction(updated, budget, services=prompt_compaction_services(), **kwargs), "openai_responses")
|
|
2069
2069
|
|
|
2070
2070
|
def _channel_compact_request_ttl_seconds() -> float: return compact_request_ttl(os.environ.get('CIEL_RUNTIME_CHANNEL_COMPACT_REQUEST_TTL_SECONDS'))
|
|
2071
2071
|
|
|
@@ -2304,7 +2304,7 @@ def normalize_request_for_provider_wire(provider: str, pcfg: dict[str, Any], bod
|
|
|
2304
2304
|
normalize_thinking_for_non_anthropic_provider=normalize_thinking_for_non_anthropic_provider,
|
|
2305
2305
|
normalize_tool_choice_for_provider=normalize_tool_choice_for_provider,
|
|
2306
2306
|
provider_wire_profile=provider_wire_profile,
|
|
2307
|
-
sanitize_assistant_pseudo_tool_text_history=sanitize_assistant_pseudo_tool_text_history
|
|
2307
|
+
sanitize_assistant_pseudo_tool_text_history=sanitize_assistant_pseudo_tool_text_history, sanitize_invalid_anthropic_tool_history=lambda body: sanitize_invalid_anthropic_tool_history(body, AnthropicToolTurnServices(log=router_log))
|
|
2308
2308
|
),
|
|
2309
2309
|
)
|
|
2310
2310
|
|
|
@@ -2409,8 +2409,8 @@ def chat_projection_services() -> ChatProjectionServices:
|
|
|
2409
2409
|
),
|
|
2410
2410
|
)
|
|
2411
2411
|
|
|
2412
|
-
def anthropic_messages_to_ollama(body: dict[str, Any]) -> list[dict[str, Any]]: return project_anthropic_messages_to_ollama(body, services=chat_projection_services())
|
|
2413
|
-
def anthropic_messages_to_openai(body: dict[str, Any], reasoning_passback: bool = False) -> list[dict[str, Any]]: return project_anthropic_messages_to_openai(body, reasoning_passback, services=chat_projection_services())
|
|
2412
|
+
def anthropic_messages_to_ollama(body: dict[str, Any]) -> list[dict[str, Any]]: return project_move_memory_pointer_to_system_end(project_anthropic_messages_to_ollama(body, services=chat_projection_services()), project_current_memory_prompt(WORKSPACE_STATE_DIR, load_config(), Path(ROUTER_WORKSPACE)))
|
|
2413
|
+
def anthropic_messages_to_openai(body: dict[str, Any], reasoning_passback: bool = False) -> list[dict[str, Any]]: return project_move_memory_pointer_to_system_end(project_anthropic_messages_to_openai(body, reasoning_passback, services=chat_projection_services()), project_current_memory_prompt(WORKSPACE_STATE_DIR, load_config(), Path(ROUTER_WORKSPACE)))
|
|
2414
2414
|
missing_openai_tool_result_message = project_missing_openai_tool_result_message
|
|
2415
2415
|
orphan_openai_tool_message_to_user = project_orphan_openai_tool_message_to_user
|
|
2416
2416
|
def repair_openai_tool_call_adjacency(messages: list[dict[str, Any]]) -> list[dict[str, Any]]: return project_repair_openai_tool_call_adjacency(messages, OpenAiHistoryServices(log=router_log))
|
|
@@ -2534,7 +2534,7 @@ def provider_request_builder() -> ProviderRequestBuilder:
|
|
|
2534
2534
|
sampling_allowed=lambda provider, config: configured_provider_adapter(provider, config).allows_sampling_overrides(provider_contract_config(provider, config)),
|
|
2535
2535
|
omit_tool_choice=should_omit_openai_chat_tool_choice, tool_choice=anthropic_tool_choice_to_openai, normalize_request=apply_provider_adapter_request_policy,
|
|
2536
2536
|
),
|
|
2537
|
-
ProviderOptionPorts(frozenset(PROVIDER_SAMPLING_OPTION_PROVIDERS), tuple(PROVIDER_SAMPLING_OPTIONS), anthropic_model_runtime_hints, router_log),
|
|
2537
|
+
ProviderOptionPorts(frozenset(PROVIDER_SAMPLING_OPTION_PROVIDERS), tuple(PROVIDER_SAMPLING_OPTIONS), anthropic_model_runtime_hints, router_log, lambda messages: project_move_memory_pointer_to_system_end(messages, project_current_memory_prompt(WORKSPACE_STATE_DIR, load_config(), Path(ROUTER_WORKSPACE)))),
|
|
2538
2538
|
)
|
|
2539
2539
|
|
|
2540
2540
|
_PROVIDER_REQUEST_API = ProviderRequestCompatibilityApi(provider_request_builder)
|
|
@@ -2843,7 +2843,7 @@ def response_collection_context() -> ResponseCollectionContext:
|
|
|
2843
2843
|
anthropic=AnthropicCollectionServices(
|
|
2844
2844
|
request=AnthropicCollectionRequest(normalize_thinking_for_non_anthropic_provider, normalize_anthropic_system_role_messages_for_provider, cap_anthropic_body_for_provider,
|
|
2845
2845
|
apply_provider_request_options, rehydrate_suppressed_thinking_passback, resolve_requested_model, provider_upstream_model,
|
|
2846
|
-
resolve_tool_model_references, normalize_anthropic_model_request_options,
|
|
2846
|
+
resolve_tool_model_references, normalize_anthropic_model_request_options, finalized_anthropic_upstream_body),
|
|
2847
2847
|
transport=AnthropicCollectionTransport(provider_native_compat_enabled, native_anthropic_base_url, provider_upstream_request_base, join_url, upstream_messages_query,
|
|
2848
2848
|
provider_headers, apply_router_rate_limit, open_provider_request_with_key_retry, provider_request_timeout_seconds),
|
|
2849
2849
|
projection=AnthropicCollectionProjection(normalize_response_thinking_for_non_anthropic_provider, append_synthetic_tasklist_to_message,
|
|
@@ -2881,7 +2881,8 @@ def codex_backend_context() -> CodexBackendContext:
|
|
|
2881
2881
|
provider_projection=ProviderPassthroughProjectionPorts(provider_headers, lambda *args, **kwargs: provider_chat_headers(*args, **kwargs),
|
|
2882
2882
|
lambda *args, **kwargs: provider_responses_headers(*args, **kwargs),
|
|
2883
2883
|
provider_upstream_model, resolve_requested_model, apply_provider_adapter_request_policy,
|
|
2884
|
-
lambda provider, pcfg: configured_provider_adapter(provider, pcfg).responses_request_max_bytes(provider_contract_config(provider, pcfg))
|
|
2884
|
+
lambda provider, pcfg: configured_provider_adapter(provider, pcfg).responses_request_max_bytes(provider_contract_config(provider, pcfg)),
|
|
2885
|
+
lambda body: body_with_remote_memory_prompt(body, "openai_chat"), lambda body: body_with_remote_memory_prompt(body, "openai_responses")),
|
|
2885
2886
|
provider_transport=ProviderPassthroughTransportPorts(provider_upstream_request_base, join_url, provider_urlopen,
|
|
2886
2887
|
provider_request_timeout_seconds,
|
|
2887
2888
|
lambda *args, **kwargs: _copy_upstream_response_headers(*args, **kwargs), write_router_activity),
|
|
@@ -2907,7 +2908,8 @@ forward_codex_backend_get = _CODEX_BACKEND_API.forward_get
|
|
|
2907
2908
|
forward_codex_responses = _CODEX_BACKEND_API.forward_responses
|
|
2908
2909
|
|
|
2909
2910
|
def body_with_codex_compat_instructions(cfg: dict[str, Any], provider: str, pcfg: dict[str, Any], body: dict[str, Any]) -> dict[str, Any]:
|
|
2910
|
-
|
|
2911
|
+
body = codex_turn_recovery.body_with_codex_compat_instructions(body, ROUTED_CODEX_COMPAT_PROMPT, is_native_codex=codex_routed_enabled(provider, pcfg), compat_enabled=should_append_compat_prompt(provider, pcfg, cfg))
|
|
2912
|
+
return body_with_remote_memory_prompt(body, "openai_responses")
|
|
2911
2913
|
|
|
2912
2914
|
def _codex_turn_recovery_services() -> codex_turn_recovery.CodexTurnRecoveryServices:
|
|
2913
2915
|
return codex_turn_recovery.CodexTurnRecoveryServices(should_retry=should_retry_preamble_only_turn, collect_message=collect_provider_message_for_responses, log=router_log)
|
|
@@ -2939,7 +2941,7 @@ def _router_request_context() -> RouterRequestContext:
|
|
|
2939
2941
|
assembly.ClaudeRouterRoutingPorts(forward_ollama_api_chat, forward_openai_compatible_chat, select_provider_protocol, provider_request_policy, resolve_requested_model, PROVIDER_LABELS, write_json),
|
|
2940
2942
|
assembly.ClaudeRouterNormalizationPorts(normalize_request_for_provider_wire, normalize_thinking_for_non_anthropic_provider, normalize_anthropic_system_role_messages_for_provider,
|
|
2941
2943
|
cap_anthropic_body_for_provider, apply_provider_request_options, rehydrate_suppressed_thinking_passback, ncp_model_id_for_nvidia_hosted,
|
|
2942
|
-
resolve_tool_model_references, normalize_anthropic_model_request_options,
|
|
2944
|
+
resolve_tool_model_references, normalize_anthropic_model_request_options, finalized_anthropic_upstream_body),
|
|
2943
2945
|
assembly.ClaudeRouterTransportPorts(native_anthropic_base_url, provider_native_compat_enabled, provider_upstream_request_base, join_url, upstream_messages_query,
|
|
2944
2946
|
provider_headers, apply_router_rate_limit, open_provider_request_with_key_retry, provider_request_timeout_seconds, provider_stream_idle_timeout_seconds),
|
|
2945
2947
|
assembly.ClaudeRouterResponsePorts(_rebatch_anthropic_sse_text, preserves_anthropic_thinking_contract, should_normalize_anthropic_stream_tool_use,
|
|
@@ -2962,9 +2964,7 @@ runtime_router_capability_matrix = _ROUTER_REQUEST_API.capability_matrix
|
|
|
2962
2964
|
runtime_router_capability_gaps = _ROUTER_REQUEST_API.capability_gaps
|
|
2963
2965
|
route_runtime_get = _ROUTER_REQUEST_API.route_get
|
|
2964
2966
|
route_runtime_post = _ROUTER_REQUEST_API.route_post
|
|
2965
|
-
|
|
2966
2967
|
_ROUTER_REQUEST_BODY_POLICY: RouterRequestBodyPolicy | None = None
|
|
2967
|
-
|
|
2968
2968
|
def router_request_body_policy() -> RouterRequestBodyPolicy:
|
|
2969
2969
|
global _ROUTER_REQUEST_BODY_POLICY
|
|
2970
2970
|
if _ROUTER_REQUEST_BODY_POLICY is None:
|
|
@@ -2975,17 +2975,21 @@ def router_request_body_policy() -> RouterRequestBodyPolicy:
|
|
|
2975
2975
|
for warning in _ROUTER_REQUEST_BODY_POLICY.configuration_warnings:
|
|
2976
2976
|
router_log("WARN", f"router_request_limit_configuration {warning}")
|
|
2977
2977
|
return _ROUTER_REQUEST_BODY_POLICY
|
|
2978
|
-
|
|
2979
2978
|
def _router_server_context() -> RouterServerContext:
|
|
2979
|
+
usage_services = UsageRuntimeServices(USAGE_LEDGER, USAGE_API_KEYS, UsagePushDeliveryService(USAGE_LEDGER, load_config, os.environ, router_log), CONFIG_DIR, WORKSPACE_STATE_DIR, ROUTER_WORKSPACE_ID, USAGE_EVENTS_PATH, router_log)
|
|
2980
|
+
def start_router_services() -> None:
|
|
2981
|
+
external_event_receiver_service().start()
|
|
2982
|
+
usage_services.start()
|
|
2983
|
+
def stop_router_services() -> None:
|
|
2984
|
+
usage_services.stop()
|
|
2985
|
+
external_event_receiver_service().stop()
|
|
2980
2986
|
http_services = RouterHttpServices(
|
|
2981
2987
|
core=RouterHttpCore(load_config, reject_external_router_request, get_current_provider, parse_json_body, is_client_disconnect_error, router_log, observe_tui_runtime_response, router_request_body_policy()),
|
|
2982
|
-
|
|
2983
|
-
# not expose the legacy endpoint or register it with Claude.
|
|
2984
|
-
get=RouterHttpGetEndpoints(handle_tui_observation_get, handle_events_get, handle_llm_config_get, lambda _handler, _path: False, handle_web_get,
|
|
2988
|
+
get=RouterHttpGetEndpoints(handle_tui_observation_get, handle_observability_get, handle_llm_config_get, lambda _handler, _path: False, handle_web_get,
|
|
2985
2989
|
lambda handler, path: speech_http_controller().get(handler, path), handle_chat_get, handle_plan_get, route_runtime_get,
|
|
2986
2990
|
handle_external_event_get),
|
|
2987
|
-
post=RouterHttpPostEndpoints(lambda handler, path, raw, content_type: speech_http_controller().post(handler, path, raw, content_type), handle_llm_config_post,
|
|
2988
|
-
handle_plan_post, route_runtime_post, handle_external_event_raw_post, handle_external_event_config_post),
|
|
2991
|
+
post=RouterHttpPostEndpoints(lambda handler, path, raw, content_type: speech_http_controller().post(handler, path, raw, content_type), handle_llm_config_post, handle_channel_mcp_post, handle_chat_post,
|
|
2992
|
+
handle_plan_post, route_runtime_post, handle_external_event_raw_post, handle_external_event_config_post, handle_usage_post),
|
|
2989
2993
|
presentation=RouterHttpPresentation(render_router_home_html, router_health_payload, write_text_response, write_json, list_model_objects_for_request,
|
|
2990
2994
|
resolve_requested_model, model_object),
|
|
2991
2995
|
errors=RouterHttpErrors(write_openai_responses_error, try_write_json),
|
|
@@ -2997,8 +3001,8 @@ def _router_server_context() -> RouterServerContext:
|
|
|
2997
3001
|
router_server_runtime.RouterServerEffects(os.chmod, sys.stderr, ThreadingHTTPServer, start_managed_router_lifetime_watchdog,
|
|
2998
3002
|
lambda bind_host: configure_requested_web_endpoints(ROUTER_PORT, ROUTER_HOST, bind_host,
|
|
2999
3003
|
config=load_config()),
|
|
3000
|
-
|
|
3001
|
-
|
|
3004
|
+
start_router_services,
|
|
3005
|
+
stop_router_services),
|
|
3002
3006
|
)
|
|
3003
3007
|
return RouterServerContext(
|
|
3004
3008
|
health=RouterHealthPresentationPorts(VERSION, SOURCE_FINGERPRINT, RouterHealthRuntimePorts(os.getpid, active_router_client_pids), getpass.getuser, HOME, ROUTER_INSTANCE_DIR, ROUTER_WORKSPACE, ROUTER_PORT, ROUTER_INSTANCE_ID, current_alias),
|
|
@@ -4412,7 +4416,7 @@ def channel_wake_context() -> ChannelWakeContext:
|
|
|
4412
4416
|
pending_state=ChannelPendingStatePorts(_channel_stdin_active_tool_call, _channel_stdin_active_turn, _channel_stdin_recover_cursor_from_queued_only, _channel_pending_scan_limit,
|
|
4413
4417
|
_channel_superseded_message_ids, _channel_message_is_web_chat_request, _channel_llm_message_skip_reason, _channel_message_event_identity_key,
|
|
4414
4418
|
_channel_stdin_wake_state_for_message, _channel_stdin_wake_queued_is_stale_for_message),
|
|
4415
|
-
pending_delivery=ChannelPendingDeliveryPorts(format_channel_llm_delivery_wake_prompt, format_channel_web_chat_wake_batch_prompt, format_channel_wake_batch_prompt, _channel_enter_label,
|
|
4419
|
+
pending_delivery=ChannelPendingDeliveryPorts(format_channel_llm_delivery_wake_prompt, format_channel_visible_llm_delivery_wake_prompt, format_channel_web_chat_wake_batch_prompt, format_channel_wake_batch_prompt, _channel_enter_label,
|
|
4416
4420
|
_channel_wake_store_release_stale, _CHANNEL_WAKE_DELIVERY_REPOSITORY.mark_delivered, _channel_wake_store_record_prompts,
|
|
4417
4421
|
_channel_wake_store_rollback, _commit_channel_llm_cursor_if_newer),
|
|
4418
4422
|
pending_io=ChannelPendingIoPorts(_CHANNEL_STDIN_INJECT_LOCK, read_runtime_inputs, _write_channel_wake_prompt, _read_channel_compact_request, _clear_channel_compact_request, _runtime_input_storage_path(), router_log),
|
|
@@ -4429,7 +4433,7 @@ def _channel_prompt_references_message_id(text: str, message_id: int, prompt_tex
|
|
|
4429
4433
|
def _channel_message_ids_already_in_request(body: dict[str, Any]) -> set[int]: return channel_wake_context().message_ids_already_in_request(body)
|
|
4430
4434
|
def _channel_llm_commit_cursor_locked(last_id: int) -> None: channel_wake_context().commit_cursor(last_id)
|
|
4431
4435
|
def _channel_llm_stdin_skip_reason(message_id: int) -> str: return channel_wake_context().stdin_skip_reason(message_id)
|
|
4432
|
-
def body_with_pending_channel_messages(body: dict[str, Any]) -> dict[str, Any]: return channel_wake_context().body_with_pending_messages(body)
|
|
4436
|
+
def body_with_pending_channel_messages(body: dict[str, Any]) -> dict[str, Any]: return channel_wake_context().body_with_pending_messages(body_with_remote_memory_prompt(body, "anthropic_messages"))
|
|
4433
4437
|
def _write_fd_all(fd: int, data: bytes) -> None: ChannelWakeContext.write_all(fd, data)
|
|
4434
4438
|
def _channel_wake_enter_bytes(value: str | bytes | None = None) -> bytes: return channel_wake_context().enter_bytes(value)
|
|
4435
4439
|
def _channel_wake_input_bytes(prompt: str, enter_bytes: bytes | None = None) -> bytes: return channel_wake_context().input_bytes(prompt, enter_bytes)
|
|
@@ -4444,8 +4448,8 @@ def _windows_channel_startup_grace_seconds() -> float:
|
|
|
4444
4448
|
def _windows_channel_wake_max_attempts() -> int:
|
|
4445
4449
|
return channel_runtime_environment_policy().windows_wake_max_attempts()
|
|
4446
4450
|
|
|
4447
|
-
def _write_channel_wake_prompt( master_fd: int, prompt: str, enter_bytes: bytes | None = None, *, submit_retry_count: int = 1, confirm_submit: bool = False, bracketed_paste: bool = False, submit_delay_seconds: float | None = None, ) ->
|
|
4448
|
-
channel_wake_context().write_prompt(
|
|
4451
|
+
def _write_channel_wake_prompt( master_fd: int, prompt: str, enter_bytes: bytes | None = None, *, submit_retry_count: int = 1, confirm_submit: bool = False, bracketed_paste: bool = False, submit_delay_seconds: float | None = None, ) -> bool:
|
|
4452
|
+
return channel_wake_context().write_prompt(
|
|
4449
4453
|
master_fd, prompt, enter_bytes, submit_retry_count=submit_retry_count,
|
|
4450
4454
|
confirm_submit=confirm_submit, bracketed_paste=bracketed_paste,
|
|
4451
4455
|
submit_delay_seconds=submit_delay_seconds, write_all=_write_fd_all,
|
|
@@ -4458,11 +4462,9 @@ _CHANNEL_TRANSCRIPT_SCOPE: dict[str, Any] = {
|
|
|
4458
4462
|
'session_id': '', 'bound_path': None,
|
|
4459
4463
|
}
|
|
4460
4464
|
_CHANNEL_STDIN_RECOVERY_CACHE: dict[str, Any] = {'checked_at': 0.0, 'last_id': None, 'marker': None, 'recovered_last_id': None}
|
|
4465
|
+
_TRANSCRIPT_DELIVERY_SERVICE = TranscriptDeltaDeliveryService(WORKSPACE_STATE_DIR / "transcript-event-cursors.json", ROUTER_WORKSPACE_ID, TranscriptDeliveryPorts(load_config, lambda: _latest_claude_transcript_path(ttl_seconds=0.5), lambda: dict(_CHANNEL_TRANSCRIPT_SCOPE), router_log))
|
|
4461
4466
|
def channel_transcript_repository() -> ChannelTranscriptRepository: return channel_wake_context().transcript_repository()
|
|
4462
|
-
|
|
4463
|
-
def _set_channel_transcript_scope(runtime: str, *, started_at: float | None = None, codex_home: Path | None = None, cwd: Path | None = None, session_id: str | None = None) -> None:
|
|
4464
|
-
channel_wake_context().set_transcript_scope(runtime, started_at=started_at, codex_home=codex_home, cwd=cwd, session_id=session_id)
|
|
4465
|
-
|
|
4467
|
+
def _set_channel_transcript_scope(runtime: str, *, started_at: float | None = None, codex_home: Path | None = None, cwd: Path | None = None, session_id: str | None = None) -> None: return (channel_wake_context().set_transcript_scope(runtime, started_at=started_at, codex_home=codex_home, cwd=cwd, session_id=session_id), _TRANSCRIPT_DELIVERY_SERVICE.start())[1]
|
|
4466
4468
|
def _channel_transcript_roots() -> tuple[tuple[Path, str], ...]: return channel_wake_context().transcript_roots()
|
|
4467
4469
|
def _latest_claude_transcript_path(ttl_seconds: float = 2.0) -> Path | None: return channel_wake_context().latest_transcript_path(ttl_seconds)
|
|
4468
4470
|
_read_file_tail_text = ChannelTranscriptRepository.read_tail_text
|
|
@@ -4507,7 +4509,7 @@ def _inject_pending_channel_messages(
|
|
|
4507
4509
|
enter_bytes: bytes | None = None,
|
|
4508
4510
|
*,
|
|
4509
4511
|
web_chat_only: bool = False,
|
|
4510
|
-
wake_for_llm_delivery: bool = False,
|
|
4512
|
+
wake_for_llm_delivery: bool = False, display_llm_delivery_body: bool = False,
|
|
4511
4513
|
commit_cursor: bool = True,
|
|
4512
4514
|
injected_message_ids: list[int] | None = None,
|
|
4513
4515
|
submit_retry_count: int = 1,
|
|
@@ -4522,6 +4524,7 @@ def _inject_pending_channel_messages(
|
|
|
4522
4524
|
enter_bytes,
|
|
4523
4525
|
web_chat_only=web_chat_only,
|
|
4524
4526
|
wake_for_llm_delivery=wake_for_llm_delivery,
|
|
4527
|
+
display_llm_delivery_body=display_llm_delivery_body,
|
|
4525
4528
|
commit_cursor=commit_cursor,
|
|
4526
4529
|
injected_message_ids=injected_message_ids,
|
|
4527
4530
|
submit_retry_count=submit_retry_count,
|
|
@@ -4948,7 +4951,7 @@ def cli_parser_services() -> cli_parser.CliParserServices:
|
|
|
4948
4951
|
return cli_assembly.CliParserAssembly(
|
|
4949
4952
|
launch=cli_parser.CliParserLaunch(cmd_cli, cmd_launch, cmd_launch_codex, cmd_launch_codex_app_server, cmd_launch_agy, serve, cmd_launch_grok),
|
|
4950
4953
|
runtime=cli_parser.CliParserRuntime(cmd_version, cmd_status, cmd_env, cmd_stop, cmd_test),
|
|
4951
|
-
settings=cli_parser.CliParserSettings(cmd_language, cmd_web_search, cmd_web_fetch, cmd_log_level, *event_settings_cli.handlers(event_settings_cli.EventSettingsCliPorts(load_config, save_config, external_event_receiver_service, lambda: set_remote_instruction_config('sync', ''), sync_all_remote_memories, print))),
|
|
4954
|
+
settings=cli_parser.CliParserSettings(cmd_language, cmd_web_search, cmd_web_fetch, cmd_log_level, *event_settings_cli.handlers(event_settings_cli.EventSettingsCliPorts(load_config, save_config, external_event_receiver_service, lambda: set_remote_instruction_config('sync', ''), sync_all_remote_memories, print, lambda: USAGE_API_KEYS))),
|
|
4952
4955
|
provider=cli_parser.CliParserProvider(cmd_ollama_native, cmd_ollama_options, cmd_provider_options, cmd_ollama_catalog, cmd_provider,
|
|
4953
4956
|
cmd_api_key, cmd_set_api_key, cmd_set_api_keys, cmd_base_url, cmd_copilot_oauth),
|
|
4954
4957
|
models=cli_parser.CliParserModels(cmd_model, cmd_advisor_model, cmd_models),
|
|
@@ -12,6 +12,80 @@ class AnthropicToolTurnServices:
|
|
|
12
12
|
log: Callable[[str, str], Any]
|
|
13
13
|
|
|
14
14
|
|
|
15
|
+
def sanitize_invalid_anthropic_tool_history(
|
|
16
|
+
body: dict[str, Any],
|
|
17
|
+
services: AnthropicToolTurnServices,
|
|
18
|
+
) -> dict[str, Any]:
|
|
19
|
+
"""Remove schema-invalid tool uses and only their paired results.
|
|
20
|
+
|
|
21
|
+
Provider-native compaction validates retained messages again. A historical
|
|
22
|
+
assistant tool block with an empty name therefore makes every later
|
|
23
|
+
compaction fail, even when the current runtime no longer emits that block.
|
|
24
|
+
Preserve valid and unmatched history; remove only invalid tool blocks and
|
|
25
|
+
results that explicitly reference their IDs.
|
|
26
|
+
"""
|
|
27
|
+
messages = body.get("messages")
|
|
28
|
+
if not isinstance(messages, list):
|
|
29
|
+
return body
|
|
30
|
+
|
|
31
|
+
invalid_ids = {
|
|
32
|
+
str(block.get("id") or "")
|
|
33
|
+
for message in messages
|
|
34
|
+
if isinstance(message, dict)
|
|
35
|
+
and str(message.get("role") or "") == "assistant"
|
|
36
|
+
for block in _content_blocks(message)
|
|
37
|
+
if isinstance(block, dict)
|
|
38
|
+
and block.get("type") == "tool_use"
|
|
39
|
+
and not str(block.get("name") or "").strip()
|
|
40
|
+
and str(block.get("id") or "")
|
|
41
|
+
}
|
|
42
|
+
invalid_uses = 0
|
|
43
|
+
invalid_results = 0
|
|
44
|
+
normalized_messages: list[Any] = []
|
|
45
|
+
for message in messages:
|
|
46
|
+
if not isinstance(message, dict) or not isinstance(message.get("content"), list):
|
|
47
|
+
normalized_messages.append(message)
|
|
48
|
+
continue
|
|
49
|
+
role = str(message.get("role") or "")
|
|
50
|
+
next_content: list[Any] = []
|
|
51
|
+
content_changed = False
|
|
52
|
+
for block in message["content"]:
|
|
53
|
+
if (
|
|
54
|
+
role == "assistant"
|
|
55
|
+
and isinstance(block, dict)
|
|
56
|
+
and block.get("type") == "tool_use"
|
|
57
|
+
and not str(block.get("name") or "").strip()
|
|
58
|
+
):
|
|
59
|
+
invalid_uses += 1
|
|
60
|
+
content_changed = True
|
|
61
|
+
continue
|
|
62
|
+
if (
|
|
63
|
+
role == "user"
|
|
64
|
+
and isinstance(block, dict)
|
|
65
|
+
and block.get("type") == "tool_result"
|
|
66
|
+
and str(block.get("tool_use_id") or "") in invalid_ids
|
|
67
|
+
):
|
|
68
|
+
invalid_results += 1
|
|
69
|
+
content_changed = True
|
|
70
|
+
continue
|
|
71
|
+
next_content.append(block)
|
|
72
|
+
if not content_changed:
|
|
73
|
+
normalized_messages.append(message)
|
|
74
|
+
elif next_content:
|
|
75
|
+
normalized_messages.append(_with_content(message, next_content))
|
|
76
|
+
|
|
77
|
+
if not invalid_uses and not invalid_results:
|
|
78
|
+
return body
|
|
79
|
+
out = dict(body)
|
|
80
|
+
out["messages"] = normalized_messages
|
|
81
|
+
services.log(
|
|
82
|
+
"WARN",
|
|
83
|
+
"discarded schema-invalid historical Anthropic tool blocks "
|
|
84
|
+
f"tool_uses={invalid_uses} tool_results={invalid_results}",
|
|
85
|
+
)
|
|
86
|
+
return out
|
|
87
|
+
|
|
88
|
+
|
|
15
89
|
def normalize_historical_anthropic_tool_turns(
|
|
16
90
|
provider: str,
|
|
17
91
|
body: dict[str, Any],
|
|
@@ -133,3 +207,10 @@ def _with_content(message: dict[str, Any], content: list[Any]) -> dict[str, Any]
|
|
|
133
207
|
updated = dict(message)
|
|
134
208
|
updated["content"] = content
|
|
135
209
|
return updated
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
__all__ = [
|
|
213
|
+
"AnthropicToolTurnServices",
|
|
214
|
+
"normalize_historical_anthropic_tool_turns",
|
|
215
|
+
"sanitize_invalid_anthropic_tool_history",
|
|
216
|
+
]
|
|
@@ -699,6 +699,23 @@ class ProviderAdapter(ABC):
|
|
|
699
699
|
del config
|
|
700
700
|
return request
|
|
701
701
|
|
|
702
|
+
def normalize_request_options_for_protocol(
|
|
703
|
+
self,
|
|
704
|
+
config: ProviderConfig,
|
|
705
|
+
request: Mapping[str, Any],
|
|
706
|
+
protocol: MessageProtocol | None,
|
|
707
|
+
) -> Mapping[str, Any]:
|
|
708
|
+
"""Normalize request options for the selected upstream wire protocol.
|
|
709
|
+
|
|
710
|
+
Existing adapters remain protocol-neutral by default. Providers that
|
|
711
|
+
expose more than one wire surface can override this hook without
|
|
712
|
+
inferring the protocol from overlapping request fields such as
|
|
713
|
+
``messages`` and ``max_tokens``.
|
|
714
|
+
"""
|
|
715
|
+
|
|
716
|
+
del protocol
|
|
717
|
+
return self.normalize_request_options(config, request)
|
|
718
|
+
|
|
702
719
|
def anthropic_system_role_strategy(
|
|
703
720
|
self, config: ProviderConfig
|
|
704
721
|
) -> Literal["inline_user", "hoist_top_level"]:
|
|
@@ -23,6 +23,16 @@ class InputTransport(Protocol):
|
|
|
23
23
|
|
|
24
24
|
def input_snapshot(self) -> str | None: ...
|
|
25
25
|
|
|
26
|
+
def prompt_readiness_checkpoint(self) -> object: ...
|
|
27
|
+
|
|
28
|
+
def wait_until_prompt_ready(
|
|
29
|
+
self,
|
|
30
|
+
previous_snapshot: object,
|
|
31
|
+
timeout_seconds: float = 2.0,
|
|
32
|
+
*,
|
|
33
|
+
expected_prompt: str | None = None,
|
|
34
|
+
) -> bool | None: ...
|
|
35
|
+
|
|
26
36
|
|
|
27
37
|
@dataclass(frozen=True)
|
|
28
38
|
class RuntimeInjectionPolicy:
|
|
@@ -36,6 +46,7 @@ class RuntimeInjectionPolicy:
|
|
|
36
46
|
confirm_submission: bool = False
|
|
37
47
|
bracketed_paste: bool = False
|
|
38
48
|
input_drain_timeout_seconds: float = 2.0
|
|
49
|
+
prompt_render_timeout_seconds: float = 30.0
|
|
39
50
|
|
|
40
51
|
def __post_init__(self) -> None:
|
|
41
52
|
if not self.runtime.strip():
|
|
@@ -46,6 +57,8 @@ class RuntimeInjectionPolicy:
|
|
|
46
57
|
raise ValueError("submit_delay_seconds cannot be negative")
|
|
47
58
|
if not 1 <= self.submit_attempts <= 8:
|
|
48
59
|
raise ValueError("submit_attempts must be between 1 and 8")
|
|
60
|
+
if self.prompt_render_timeout_seconds < 0:
|
|
61
|
+
raise ValueError("prompt_render_timeout_seconds cannot be negative")
|
|
49
62
|
|
|
50
63
|
|
|
51
64
|
@dataclass(frozen=True)
|
|
@@ -70,8 +83,18 @@ class ChannelPromptInjector:
|
|
|
70
83
|
self._snapshot = snapshot
|
|
71
84
|
self._log = log
|
|
72
85
|
|
|
73
|
-
def inject(self, transport: InputTransport, request: PromptInjection) ->
|
|
86
|
+
def inject(self, transport: InputTransport, request: PromptInjection) -> bool:
|
|
74
87
|
policy = request.policy
|
|
88
|
+
prompt_ready_wait = bool(
|
|
89
|
+
getattr(transport, "supports_prompt_ready_wait", False)
|
|
90
|
+
)
|
|
91
|
+
before_prompt = (
|
|
92
|
+
self._prompt_readiness_checkpoint(transport)
|
|
93
|
+
if prompt_ready_wait
|
|
94
|
+
and policy.confirm_submission
|
|
95
|
+
and policy.submit_attempts > 1
|
|
96
|
+
else None
|
|
97
|
+
)
|
|
75
98
|
normalize = getattr(transport, "normalize_prompt", None)
|
|
76
99
|
prompt_text = (
|
|
77
100
|
str(normalize(request.prompt)) if callable(normalize) else request.prompt
|
|
@@ -101,10 +124,28 @@ class ChannelPromptInjector:
|
|
|
101
124
|
):
|
|
102
125
|
self._log("WARN", "channel_input_drain_timeout")
|
|
103
126
|
|
|
127
|
+
if prompt_ready_wait:
|
|
128
|
+
prompt_ready = self._wait_until_prompt_ready(
|
|
129
|
+
transport,
|
|
130
|
+
before_prompt,
|
|
131
|
+
prompt_text,
|
|
132
|
+
policy,
|
|
133
|
+
)
|
|
134
|
+
if prompt_ready is False:
|
|
135
|
+
self._log(
|
|
136
|
+
"WARN",
|
|
137
|
+
"channel_input_submit_deferred reason=prompt_render_timeout",
|
|
138
|
+
)
|
|
139
|
+
return False
|
|
140
|
+
|
|
104
141
|
if policy.submit_delay_seconds:
|
|
105
142
|
self._sleep(policy.submit_delay_seconds)
|
|
106
143
|
|
|
107
|
-
before =
|
|
144
|
+
before = (
|
|
145
|
+
self._submission_snapshot(transport)
|
|
146
|
+
if policy.confirm_submission and policy.submit_attempts > 1
|
|
147
|
+
else None
|
|
148
|
+
)
|
|
108
149
|
for attempt in range(policy.submit_attempts):
|
|
109
150
|
if bool(getattr(transport, "separate_input_stages", False)):
|
|
110
151
|
self._write_stage(
|
|
@@ -120,10 +161,52 @@ class ChannelPromptInjector:
|
|
|
120
161
|
retry_delay = self._retry_delay_seconds()
|
|
121
162
|
if retry_delay:
|
|
122
163
|
self._sleep(retry_delay)
|
|
123
|
-
after = self.
|
|
164
|
+
after = self._submission_snapshot(transport)
|
|
124
165
|
if after and after != before:
|
|
125
166
|
self._log("INFO", f"channel_stdin_proxy_submit_confirmed attempt={attempt + 1}")
|
|
126
167
|
break
|
|
168
|
+
return True
|
|
169
|
+
|
|
170
|
+
def _submission_snapshot(self, transport: InputTransport) -> str | None:
|
|
171
|
+
"""Use the host snapshot when available, then the transport's own view."""
|
|
172
|
+
|
|
173
|
+
captured = self._snapshot()
|
|
174
|
+
if captured is not None:
|
|
175
|
+
return captured
|
|
176
|
+
snapshot = getattr(transport, "input_snapshot", None)
|
|
177
|
+
return snapshot() if callable(snapshot) else None
|
|
178
|
+
|
|
179
|
+
def _prompt_readiness_checkpoint(self, transport: InputTransport) -> object:
|
|
180
|
+
"""Capture the transport's own output position before prompt input."""
|
|
181
|
+
|
|
182
|
+
checkpoint = getattr(transport, "prompt_readiness_checkpoint", None)
|
|
183
|
+
if callable(checkpoint):
|
|
184
|
+
return checkpoint()
|
|
185
|
+
return self._submission_snapshot(transport)
|
|
186
|
+
|
|
187
|
+
def _wait_until_prompt_ready(
|
|
188
|
+
self,
|
|
189
|
+
transport: InputTransport,
|
|
190
|
+
previous_snapshot: object,
|
|
191
|
+
prompt: str,
|
|
192
|
+
policy: RuntimeInjectionPolicy,
|
|
193
|
+
) -> bool | None:
|
|
194
|
+
if not policy.confirm_submission or policy.submit_attempts <= 1:
|
|
195
|
+
return None
|
|
196
|
+
wait = getattr(transport, "wait_until_prompt_ready", None)
|
|
197
|
+
if not callable(wait):
|
|
198
|
+
return None
|
|
199
|
+
ready = wait(
|
|
200
|
+
previous_snapshot,
|
|
201
|
+
policy.prompt_render_timeout_seconds,
|
|
202
|
+
expected_prompt=prompt,
|
|
203
|
+
)
|
|
204
|
+
if ready is not None:
|
|
205
|
+
self._log(
|
|
206
|
+
"INFO" if ready else "WARN",
|
|
207
|
+
f"channel_input_prompt_ready result={'observed' if ready else 'timeout'}",
|
|
208
|
+
)
|
|
209
|
+
return ready
|
|
127
210
|
|
|
128
211
|
def _write_stage(
|
|
129
212
|
self,
|
|
@@ -188,6 +271,9 @@ class CallableInputTransport:
|
|
|
188
271
|
self.supports_input_snapshot = callable(
|
|
189
272
|
getattr(target, "input_snapshot", None)
|
|
190
273
|
)
|
|
274
|
+
self.supports_prompt_ready_wait = callable(
|
|
275
|
+
getattr(target, "wait_until_prompt_ready", None)
|
|
276
|
+
)
|
|
191
277
|
|
|
192
278
|
def write(self, data: bytes) -> None:
|
|
193
279
|
self._write(self._target, data)
|
|
@@ -213,6 +299,30 @@ class CallableInputTransport:
|
|
|
213
299
|
snapshot = getattr(self._target, "input_snapshot", None)
|
|
214
300
|
return snapshot() if callable(snapshot) else None
|
|
215
301
|
|
|
302
|
+
def prompt_readiness_checkpoint(self) -> object:
|
|
303
|
+
checkpoint = getattr(self._target, "prompt_readiness_checkpoint", None)
|
|
304
|
+
if callable(checkpoint):
|
|
305
|
+
return checkpoint()
|
|
306
|
+
return self.input_snapshot()
|
|
307
|
+
|
|
308
|
+
def wait_until_prompt_ready(
|
|
309
|
+
self,
|
|
310
|
+
previous_snapshot: object,
|
|
311
|
+
timeout_seconds: float = 2.0,
|
|
312
|
+
*,
|
|
313
|
+
expected_prompt: str | None = None,
|
|
314
|
+
) -> bool | None:
|
|
315
|
+
wait = getattr(self._target, "wait_until_prompt_ready", None)
|
|
316
|
+
if not callable(wait):
|
|
317
|
+
return None
|
|
318
|
+
return bool(
|
|
319
|
+
wait(
|
|
320
|
+
previous_snapshot,
|
|
321
|
+
timeout_seconds,
|
|
322
|
+
expected_prompt=expected_prompt,
|
|
323
|
+
)
|
|
324
|
+
)
|
|
325
|
+
|
|
216
326
|
|
|
217
327
|
__all__ = [
|
|
218
328
|
"CallableInputTransport",
|