|
4 | 4 |
|
5 | 5 | import os |
6 | 6 | import re |
7 | | -import shlex |
8 | | -import subprocess |
9 | 7 | from pathlib import Path |
10 | 8 |
|
11 | 9 | from ucode.agent_updates import available_npm_package_update |
|
22 | 20 | build_tool_base_url, |
23 | 21 | get_databricks_token, |
24 | 22 | ) |
| 23 | +from ucode.intelligent_routing.codex_hooks import ( |
| 24 | + remove_intelligent_routing_hooks, |
| 25 | + sync_intelligent_routing_hooks, |
| 26 | +) |
25 | 27 | from ucode.launcher import exec_or_spawn |
26 | 28 | from ucode.state import mark_tool_managed, save_state |
27 | 29 | from ucode.telemetry import agent_version, ucode_version |
|
38 | 40 | MINIMUM_ROUTING_CODEX_VERSION = (0, 145, 0) |
39 | 41 | MINIMUM_ROUTING_CODEX_VERSION_TEXT = "0.145.0" |
40 | 42 | INTELLIGENT_ROUTING_STATE_KEY = "codex_intelligent_routing_enabled" |
41 | | -ROUTING_HOOK_COMMAND_MARKER = "codex-router-hook" |
42 | 43 |
|
43 | 44 |
|
44 | 45 | SPEC: ToolSpec = { |
@@ -369,7 +370,7 @@ def write_tool_config(state: dict, model: str | None = None, provider: str | Non |
369 | 370 | # deep_merge can't drop keys, so clear a `model` pinned by an earlier |
370 | 371 | # non-provider run that the provider overlay omits. |
371 | 372 | doc.pop("model", None) |
372 | | - _sync_intelligent_routing_hooks( |
| 373 | + sync_intelligent_routing_hooks( |
373 | 374 | doc, |
374 | 375 | state, |
375 | 376 | enabled=intelligent_routing_enabled(state) and provider is None, |
@@ -443,153 +444,15 @@ def disable_intelligent_routing(state: dict) -> bool: |
443 | 444 | if not path.exists(): |
444 | 445 | continue |
445 | 446 | doc = read_toml_safe(path) |
446 | | - if _remove_intelligent_routing_hooks(doc): |
| 447 | + if remove_intelligent_routing_hooks(doc): |
447 | 448 | write_toml_file(path, doc) |
448 | 449 | changed = True |
449 | | - from ucode.codex_routing import clear_routing_artifacts |
| 450 | + from ucode.intelligent_routing.codex_routing import clear_routing_artifacts |
450 | 451 |
|
451 | 452 | clear_routing_artifacts() |
452 | 453 | return changed |
453 | 454 |
|
454 | 455 |
|
455 | | -def route_launch_model(state: dict, tool_args: list[str]): |
456 | | - """Route a root Codex launch before the Codex process starts.""" |
457 | | - from ucode.codex_routing import request_routing_decision |
458 | | - |
459 | | - workspace = state.get("workspace") |
460 | | - models = state.get("codex_models") |
461 | | - if not isinstance(workspace, str) or not isinstance(models, list): |
462 | | - return None, "workspace model metadata is unavailable" |
463 | | - try: |
464 | | - token = get_databricks_token(workspace, state.get("profile")) |
465 | | - except RuntimeError as exc: |
466 | | - return None, f"could not authenticate the routing request: {exc}" |
467 | | - task = _launch_routing_task(tool_args) |
468 | | - return request_routing_decision(workspace, token, task, models) |
469 | | - |
470 | | - |
471 | | -def _launch_routing_task(tool_args: list[str]) -> str: |
472 | | - if "exec" in tool_args: |
473 | | - prompt_parts = tool_args[tool_args.index("exec") + 1 :] |
474 | | - if prompt_parts: |
475 | | - return " ".join(prompt_parts) |
476 | | - if tool_args: |
477 | | - return "Start a Codex session with options: " + " ".join(tool_args) |
478 | | - return f"Start an interactive Codex coding session in {Path.cwd().name}." |
479 | | - |
480 | | - |
481 | | -def _sync_intelligent_routing_hooks(doc: dict, state: dict, *, enabled: bool) -> None: |
482 | | - _remove_intelligent_routing_hooks(doc) |
483 | | - if not enabled: |
484 | | - return |
485 | | - hooks = doc.setdefault("hooks", {}) |
486 | | - for event, groups in _routing_hook_groups(state).items(): |
487 | | - existing = hooks.get(event) |
488 | | - if not isinstance(existing, list): |
489 | | - existing = [] |
490 | | - hooks[event] = [*existing, *groups] |
491 | | - |
492 | | - |
493 | | -def _routing_hook_groups(state: dict) -> dict[str, list[dict]]: |
494 | | - route_argv = _routing_hook_argv(state, "route-subagent") |
495 | | - session_argv = _routing_hook_argv(state, "session-start") |
496 | | - subagent_argv = _routing_hook_argv(state, "record-subagent") |
497 | | - return { |
498 | | - "PreToolUse": [ |
499 | | - { |
500 | | - "matcher": "Agent|.*spawn_agent$", |
501 | | - "hooks": [_routing_command_hook(route_argv, status="Routing subagent model")], |
502 | | - } |
503 | | - ], |
504 | | - "SessionStart": [ |
505 | | - { |
506 | | - "matcher": "startup|resume|clear", |
507 | | - "hooks": [_routing_command_hook(session_argv)], |
508 | | - } |
509 | | - ], |
510 | | - "SubagentStart": [ |
511 | | - { |
512 | | - "hooks": [_routing_command_hook(subagent_argv)], |
513 | | - } |
514 | | - ], |
515 | | - } |
516 | | - |
517 | | - |
518 | | -def _routing_hook_argv(state: dict, event: str) -> list[str]: |
519 | | - workspace = str(state.get("workspace") or "") |
520 | | - argv = [ |
521 | | - build_auth_token_argv(workspace, state.get("profile"), use_pat=bool(state.get("use_pat")))[ |
522 | | - 0 |
523 | | - ], |
524 | | - ROUTING_HOOK_COMMAND_MARKER, |
525 | | - event, |
526 | | - ] |
527 | | - if event != "route-subagent": |
528 | | - return argv |
529 | | - argv += ["--host", workspace] |
530 | | - profile = state.get("profile") |
531 | | - if isinstance(profile, str) and profile: |
532 | | - argv += ["--profile", profile] |
533 | | - if state.get("use_pat"): |
534 | | - argv.append("--use-pat") |
535 | | - for model in state.get("codex_models") or []: |
536 | | - if isinstance(model, str) and model: |
537 | | - argv += ["--model", model] |
538 | | - return argv |
539 | | - |
540 | | - |
541 | | -def _routing_command_hook(argv: list[str], *, status: str | None = None) -> dict: |
542 | | - hook = { |
543 | | - "type": "command", |
544 | | - "command": shlex.join(argv), |
545 | | - "command_windows": subprocess.list2cmdline(argv), |
546 | | - "timeout": 35, |
547 | | - } |
548 | | - if status: |
549 | | - hook["statusMessage"] = status |
550 | | - return hook |
551 | | - |
552 | | - |
553 | | -def _remove_intelligent_routing_hooks(doc: dict) -> bool: |
554 | | - hooks = doc.get("hooks") |
555 | | - if not isinstance(hooks, dict): |
556 | | - return False |
557 | | - changed = False |
558 | | - for event in list(hooks): |
559 | | - groups = hooks.get(event) |
560 | | - if not isinstance(groups, list): |
561 | | - continue |
562 | | - kept_groups = [] |
563 | | - for group in groups: |
564 | | - if not isinstance(group, dict): |
565 | | - kept_groups.append(group) |
566 | | - continue |
567 | | - handlers = group.get("hooks") |
568 | | - if not isinstance(handlers, list): |
569 | | - kept_groups.append(group) |
570 | | - continue |
571 | | - kept_handlers = [ |
572 | | - handler |
573 | | - for handler in handlers |
574 | | - if not ( |
575 | | - isinstance(handler, dict) |
576 | | - and ROUTING_HOOK_COMMAND_MARKER in str(handler.get("command") or "") |
577 | | - ) |
578 | | - ] |
579 | | - if len(kept_handlers) != len(handlers): |
580 | | - changed = True |
581 | | - if kept_handlers: |
582 | | - group["hooks"] = kept_handlers |
583 | | - kept_groups.append(group) |
584 | | - if kept_groups: |
585 | | - hooks[event] = kept_groups |
586 | | - else: |
587 | | - hooks.pop(event, None) |
588 | | - if not hooks: |
589 | | - doc.pop("hooks", None) |
590 | | - return changed |
591 | | - |
592 | | - |
593 | 456 | def validate_cmd(binary: str) -> list[str]: |
594 | 457 | return [ |
595 | 458 | binary, |
|
0 commit comments