From 109bd1d6d724f2e83dfd9f6829414482f44c6270 Mon Sep 17 00:00:00 2001 From: jenkins Date: Mon, 10 Aug 2026 05:16:30 -0300 Subject: [PATCH] fix(hermes): route delegated work independently --- dockerfiles/Dockerfile.hermes-agent | 41 ++++++++ services/hermes/agent-configmap.yaml | 59 +++++++++--- services/hermes/agent-deployment.yaml | 2 +- .../hermes/plugins/auto-router/__init__.py | 95 ++++++++++++++++--- .../hermes/plugins/auto-router/plugin.yaml | 1 + services/hermes/scripts/herdr_dispatch.py | 53 ++++++++++- .../references/architecture.md | 2 +- testing/tests/test_hermes_auto_router.py | 58 +++++++++++ testing/tests/test_hermes_chat_quality.py | 2 + testing/tests/test_hermes_herdr.py | 49 ++++++++++ 10 files changed, 330 insertions(+), 32 deletions(-) diff --git a/dockerfiles/Dockerfile.hermes-agent b/dockerfiles/Dockerfile.hermes-agent index 6ca6faeb9..5c596df15 100644 --- a/dockerfiles/Dockerfile.hermes-agent +++ b/dockerfiles/Dockerfile.hermes-agent @@ -135,6 +135,7 @@ hooks_before = ''' "pre_llm_call", hooks_after = ''' "pre_llm_call", "pre_turn_route", "pre_internal_route", + "pre_subagent_route", "post_llm_call", ''' if plugins.count(hooks_before) != 1: @@ -209,6 +210,42 @@ if loop.count(loop_before) != 1: f"found {loop.count(loop_before)}" ) loop_path.write_text(loop.replace(loop_before, loop_after, 1)) + +delegate_path = Path("/opt/hermes/tools/delegate_tool.py") +delegate = delegate_path.read_text() +delegate_before = ''' # Override with correct parent tool names (before child construction mutated global) + child._delegate_saved_tool_names = _parent_tool_names + children.append((i, t, child)) +''' +delegate_after = ''' # Route each bounded child independently before its first LLM call. + # This keeps one multi-part objective from pinning every leaf to + # the parent coordinator's provider/model/effort. + try: + from hermes_cli.plugins import has_hook, invoke_hook + + if has_hook("pre_subagent_route"): + invoke_hook( + "pre_subagent_route", + agent=child, + parent_agent=parent_agent, + goal=t["goal"], + context=t.get("context"), + task_index=i, + task_count=n_tasks, + ) + except Exception: + logger.warning("pre_subagent_route hook failed", exc_info=True) + + # Override with correct parent tool names (before child construction mutated global) + child._delegate_saved_tool_names = _parent_tool_names + children.append((i, t, child)) +''' +if delegate.count(delegate_before) != 1: + raise SystemExit( + "Hermes subagent routing context changed: expected 1, " + f"found {delegate.count(delegate_before)}" + ) +delegate_path.write_text(delegate.replace(delegate_before, delegate_after, 1)) PY # Hermes WebUI sends its model/provider/reasoning selection on /v1/runs. @@ -366,13 +403,17 @@ RUN cd /opt/hermes/web \ /opt/hermes/gateway/platforms/api_server.py \ && grep -Fq '"pre_turn_route"' /opt/hermes/hermes_cli/plugins.py \ && grep -Fq '"pre_internal_route"' /opt/hermes/hermes_cli/plugins.py \ + && grep -Fq '"pre_subagent_route"' /opt/hermes/hermes_cli/plugins.py \ && grep -Fq 'invoke_hook(' /opt/hermes/agent/turn_context.py \ && grep -Fq 'pre_internal_route hook failed' \ /opt/hermes/agent/conversation_loop.py \ + && grep -Fq 'pre_subagent_route hook failed' \ + /opt/hermes/tools/delegate_tool.py \ && /opt/hermes/.venv/bin/python -m py_compile \ /opt/hermes/gateway/platforms/api_server.py \ /opt/hermes/agent/turn_context.py \ /opt/hermes/agent/conversation_loop.py \ + /opt/hermes/tools/delegate_tool.py \ /opt/hermes/tools/web_tools.py \ /opt/hermes/tools/python_sandbox_tool.py \ /opt/hermes/plugins/web/public_extract/provider.py \ diff --git a/services/hermes/agent-configmap.yaml b/services/hermes/agent-configmap.yaml index 63e4a2e66..624f5affd 100644 --- a/services/hermes/agent-configmap.yaml +++ b/services/hermes/agent-configmap.yaml @@ -27,11 +27,20 @@ data: agent: api_max_retries: 1 - # The coordinator supervises long-running Herdr workers; give it enough - # room to inspect, steer, review, and synthesize without truncating. + # The coordinator supervises native children and durable CLI workers; + # give it enough room to inspect, steer, review, and synthesize. max_turns: 180 reasoning_effort: medium + delegation: + # Native Hermes owns decomposition and fan-out. Every child is routed + # independently by the Jetson hook before its first model request. + max_concurrent_children: 4 + max_iterations: 120 + max_spawn_depth: 2 + orchestrator_enabled: true + subagent_auto_approve: true + toolsets: - kanban @@ -158,9 +167,11 @@ data: Keep every project's conversation, objectives, tasks, evidence, and blockers in that project's Hermes Project and Kanban board. Cassandra is - the initial project. Use Herdr as the execution fabric for persistent Codex - and Claude Code workers; you remain responsible for planning, routing, - fallback, review, and the final synthesized answer. + the initial project. Use native Hermes delegation as the normal planning, + fan-out, and synthesis path. Use Herdr only as a hidden durability adapter + when work specifically benefits from a persistent real Codex or Claude Code + CLI session. You remain responsible for planning, routing, fallback, review, + and the final synthesized answer. Prefer Codex for implementation, debugging, test loops, and focused repo changes. Prefer Claude Code for architecture, long-context investigation, @@ -169,8 +180,9 @@ data: Use the browser for live or dynamic pages when search/extraction is insufficient. Use terminal and file tools for direct engineering work; use - Herdr workers when an objective benefits from persistent Codex or Claude - Code execution, parallel review, or cross-provider fallback. + native delegated children for independent bounded work. Use Herdr workers + only when an objective benefits from persistent Codex or Claude Code CLI + execution or a long-lived CLI session that can be resumed later. Local Jetson inference is the first provider-independent fallback. Use it for bounded classification, summaries, and continuity when hosted capacity @@ -206,12 +218,27 @@ data: Profiles are `codex-{low,medium,high,xhigh}` and `claude-{low,medium,high,xhigh}`, plus `synthesis-xhigh`. - For persistent coding work, plan or launch a worker with: + ## Decomposition and delegation - `herdr-dispatch --shape --effort [--provider codex|claude]` + Treat a long objective, checklist, plan, or referential instruction such as + "do it" as a task graph, not one homogeneous model request. Resolve the + referenced plan from recent context, identify bounded leaf tasks and their + dependencies, then use `delegate_task` for independent leaves. Run only + dependency-free leaves in parallel. Each native child and every nested + child is independently classified by the Jetson before its first model + request, so cheap leaves may use low effort while difficult or risky leaves + are raised to high or xhigh. Verify and synthesize all child evidence in the + foreground coordinator. Do not delegate a one-tool mechanical action merely + to create an agent. - Add `--start --project --task --prompt ` to - create a separate Herdr worker space and launch the selected CLI. The visible + For persistent real Codex or Claude Code CLI work, launch a worker with: + + `herdr-dispatch --auto --start --project --task --prompt ` + + AUTO sends that bounded worker objective through the same Jetson classifier + before selecting the real CLI, model, and effort. Explicit `--shape`, + `--effort`, and `--provider` remain available for a deliberate manual + override. The command creates a separate Herdr worker space. The visible project tab remains a single Hermes coordinator pane. Never split Codex, Claude, or a second Hermes process into that tab. Worker-space labels include the parent project, provider, and task (for example @@ -237,10 +264,12 @@ data: # Agent Hermes The authenticated root of agent.hermes.bstein.dev opens the persistent - Herdr terminal interface. Give Hermes the outcome you want and it will - classify the difficulty, choose Codex or Claude Code, preserve the task on - the Cassandra board, supervise the worker through Herdr, and synthesize the - evidence. Use `/route status` to inspect the current decision, `/route auto` + browser terminal with the stock Hermes TUI in the foreground. Give Hermes + the outcome you want and it will decompose dependent work, classify every + delegated leaf on the Jetson, choose Codex or Claude, preserve the task on + the Cassandra board, and synthesize the evidence. Persistent real Codex and + Claude Code CLI sessions run through Herdr behind that interface only when + useful. Use `/route status` to inspect the current decision, `/route auto` for automatic routing, or `/route manual [model]` for a persistent override. The first native Codex worker requires one device-code login; subsequent sessions persist on diff --git a/services/hermes/agent-deployment.yaml b/services/hermes/agent-deployment.yaml index 0a6abca50..0e8ea88e0 100644 --- a/services/hermes/agent-deployment.yaml +++ b/services/hermes/agent-deployment.yaml @@ -21,7 +21,7 @@ spec: app: hermes-agent annotations: ai.bstein.dev/role: project-coordinator - ai.bstein.dev/execution: Herdr-supervised Codex and Claude Code + ai.bstein.dev/execution: Hermes delegation with durable Codex and Claude Code CLI workers ai.bstein.dev/model-policy: Jetson-assisted AUTO routing, low through xhigh, cross-provider fallback ai.bstein.dev/placement: rpi5 preferred; Jetson deferred until state storage is available ai.bstein.dev/config-rev: "20260810-per-internal-prompt-routing" diff --git a/services/hermes/plugins/auto-router/__init__.py b/services/hermes/plugins/auto-router/__init__.py index 216ed7342..32bb17fd2 100644 --- a/services/hermes/plugins/auto-router/__init__.py +++ b/services/hermes/plugins/auto-router/__init__.py @@ -469,7 +469,14 @@ def _apply_route(ctx: Any, agent: Any, plan: dict[str, Any]) -> None: target_provider = str(plan["provider"]) target_model = str(plan["model"]) effort = str(plan["effort"]) - cli = getattr(ctx._manager, "_cli_ref", None) + # A delegated child shares the plugin manager with the foreground TUI. Do + # not let routing that child rewrite the visible coordinator's model state. + runtime_agent = _runtime_agent(ctx) + cli = ( + getattr(ctx._manager, "_cli_ref", None) + if agent is runtime_agent + else None + ) if agent.provider != target_provider or agent.model != target_model: from hermes_cli.inventory import load_picker_context @@ -521,18 +528,19 @@ def _apply_route(ctx: Any, agent: Any, plan: dict[str, Any]) -> None: agent._fallback_index = 0 agent._fallback_activated = False agent._fallback_model = fallbacks[0] if fallbacks else None - try: - from agent.auxiliary_client import set_runtime_main + if agent is runtime_agent: + try: + from agent.auxiliary_client import set_runtime_main - set_runtime_main( - agent.provider or "", - agent.model or "", - base_url=agent.base_url or "", - api_key=agent.api_key or "", - api_mode=agent.api_mode or "", - ) - except Exception: - pass + set_runtime_main( + agent.provider or "", + agent.model or "", + base_url=agent.base_url or "", + api_key=agent.api_key or "", + api_mode=agent.api_mode or "", + ) + except Exception: + pass def _current_policy() -> dict[str, Any]: @@ -568,6 +576,29 @@ def _record_internal_plan( _write_policy(policy) +def _record_subagent_plan( + policy: dict[str, Any], plan: dict[str, Any], goal: str, task_index: int +) -> None: + """Persist a bounded audit trail for independently routed child work.""" + recorded = { + **plan, + "scope": "subagent", + "task_index": task_index, + "goal": goal[:500], + "updated_at": datetime.now(timezone.utc).isoformat(), + } + decisions = policy.get("subagent_decisions") + if not isinstance(decisions, list): + decisions = [] + decisions.append(recorded) + policy["subagent_decisions"] = decisions[-50:] + policy["last_subagent_decision"] = recorded + policy["subagent_decisions_total"] = int( + policy.get("subagent_decisions_total") or 0 + ) + 1 + _write_policy(policy) + + def _runtime_agent(ctx: Any) -> Any | None: """Return the active agent without assuming a single CLI lifecycle.""" cli = getattr(ctx._manager, "_cli_ref", None) @@ -709,6 +740,43 @@ def _pre_internal_route(ctx: Any, **kwargs: Any) -> None: ) +def _pre_subagent_route(ctx: Any, **kwargs: Any) -> None: + """Classify and route each native Hermes child before it starts work.""" + policy = _current_policy() + if policy["mode"] != "auto": + return + child = kwargs.get("agent") + goal = str(kwargs.get("goal") or "").strip() + context = str(kwargs.get("context") or "").strip() + if child is None or not goal: + return + + task_text = goal + if context: + task_text += f"\n\nDelegated context:\n{context[-6000:]}" + decision = classify_task(task_text) + decision = Decision( + decision.shape, + decision.effort, + decision.provider, + f"{decision.classifier}-subagent", + f"{decision.reason}; independently classified delegated task", + decision.latency_ms, + ) + plan = select_route(_load_json(ROUTING_PATH), decision) + _apply_route(ctx, child, plan) + task_index = int(kwargs.get("task_index") or 0) + _record_subagent_plan(policy, plan, goal, task_index) + + parent = kwargs.get("parent_agent") or _runtime_agent(ctx) + emit = getattr(parent, "_emit_status", None) + if callable(emit): + emit( + f"AUTO child #{task_index + 1} → " + f"{plan['provider']}/{plan['model']} · {plan['effort']} (Jetson)" + ) + + def _status_text(ctx: Any) -> str: policy = _current_policy() cli = getattr(ctx._manager, "_cli_ref", None) @@ -786,6 +854,9 @@ def register(ctx: Any) -> None: ctx.register_hook( "pre_internal_route", lambda **kwargs: _pre_internal_route(ctx, **kwargs) ) + ctx.register_hook( + "pre_subagent_route", lambda **kwargs: _pre_subagent_route(ctx, **kwargs) + ) ctx.register_hook("post_llm_call", lambda **kwargs: _post_turn_route(ctx, **kwargs)) ctx.register_command( "route", diff --git a/services/hermes/plugins/auto-router/plugin.yaml b/services/hermes/plugins/auto-router/plugin.yaml index 9c961998e..7b0fd2d92 100644 --- a/services/hermes/plugins/auto-router/plugin.yaml +++ b/services/hermes/plugins/auto-router/plugin.yaml @@ -4,3 +4,4 @@ description: Jetson-assisted provider, model, and reasoning-effort routing for A provides_hooks: - pre_turn_route - pre_internal_route + - pre_subagent_route diff --git a/services/hermes/scripts/herdr_dispatch.py b/services/hermes/scripts/herdr_dispatch.py index e3d346292..4cbcf81ce 100644 --- a/services/hermes/scripts/herdr_dispatch.py +++ b/services/hermes/scripts/herdr_dispatch.py @@ -4,10 +4,12 @@ from __future__ import annotations import argparse +import importlib.util import json import os import re import subprocess +import sys from pathlib import Path from typing import Any @@ -16,6 +18,7 @@ ALLOWED_EFFORTS = ("low", "medium", "high", "xhigh") ROUTING_PATH = Path("/opt/data/workspace/coordinator/model-routing.json") HERDR_BIN = Path("/opt/data/tools/bin/herdr") CODEX_AUTH = Path("/opt/data/home/.codex/auth.json") +AUTO_ROUTER_PATH = Path("/opt/data/plugins/auto-router/__init__.py") PROMPT_READY_MARKERS = { "codex": "OpenAI Codex", "claude": "Claude Code", @@ -67,6 +70,37 @@ def select_plan( } +def select_auto_plan( + status: dict[str, Any], prompt: str, router_path: Path = AUTO_ROUTER_PATH +) -> dict[str, Any]: + """Classify one bounded CLI objective on the Jetson and resolve its route.""" + if not prompt.strip(): + raise ValueError("AUTO routing requires a non-empty worker prompt") + if not router_path.is_file(): + raise RuntimeError(f"AUTO router is unavailable: {router_path}") + spec = importlib.util.spec_from_file_location("hermes_auto_router_dispatch", router_path) + if spec is None or spec.loader is None: + raise RuntimeError(f"AUTO router could not be loaded: {router_path}") + router = importlib.util.module_from_spec(spec) + sys.modules[spec.name] = router + spec.loader.exec_module(router) + decision = router.classify_task(prompt) + plan = select_plan( + status, + str(decision.shape), + str(decision.effort), + str(decision.provider), + ) + plan.update( + { + "classifier": str(decision.classifier), + "classification_reason": str(decision.reason), + "classification_latency_ms": int(decision.latency_ms), + } + ) + return plan + + def _slug(value: str, limit: int = 32) -> str: """Return a Herdr-safe stable label.""" cleaned = re.sub(r"[^a-z0-9_-]+", "-", value.lower()).strip("-") @@ -221,8 +255,9 @@ def launch_worker( def main() -> int: parser = argparse.ArgumentParser(description=__doc__) - parser.add_argument("--shape", choices=("implementation", "architecture", "review"), required=True) - parser.add_argument("--effort", choices=ALLOWED_EFFORTS, required=True) + parser.add_argument("--auto", action="store_true") + parser.add_argument("--shape", choices=("implementation", "architecture", "review", "question")) + parser.add_argument("--effort", choices=ALLOWED_EFFORTS) parser.add_argument("--provider", choices=("codex", "claude")) parser.add_argument("--routes", type=Path, default=ROUTING_PATH) parser.add_argument("--start", action="store_true") @@ -231,7 +266,19 @@ def main() -> int: parser.add_argument("--prompt") args = parser.parse_args() - plan = select_plan(_load_routes(args.routes), args.shape, args.effort, args.provider) + status = _load_routes(args.routes) + if args.auto: + if args.provider is not None: + parser.error("--provider cannot be combined with --auto") + if args.shape is not None or args.effort is not None: + parser.error("--shape/--effort cannot be combined with --auto") + if not args.prompt: + parser.error("--prompt is required with --auto") + plan = select_auto_plan(status, args.prompt) + else: + if args.shape is None or args.effort is None: + parser.error("--shape and --effort are required unless --auto is used") + plan = select_plan(status, args.shape, args.effort, args.provider) if args.start: if args.project is None: parser.error("--project is required with --start") diff --git a/services/hermes/skills/master-hermes-on-atlas/references/architecture.md b/services/hermes/skills/master-hermes-on-atlas/references/architecture.md index 37d41caa9..9ec70a103 100644 --- a/services/hermes/skills/master-hermes-on-atlas/references/architecture.md +++ b/services/hermes/skills/master-hermes-on-atlas/references/architecture.md @@ -8,7 +8,7 @@ asserting health, placement, ownership, or current model availability. | Surface | Purpose | Identity boundary | State and permissions | | --- | --- | --- | --- | | `triage.hermes.bstein.dev` | Brad's automated testing triage | Keycloak plus an outer oauth2-proxy exact-email allow-list for `brad@bstein.dev` | `hermes` namespace, its own PVC and service account; read-only cluster triage plus approved internal evidence endpoints | -| `agent.hermes.bstein.dev` | Brad's project coordinator | Keycloak plus an outer oauth2-proxy exact-email allow-list for `brad@bstein.dev` | `hermes` namespace, separate PVC and no Kubernetes RBAC; Herdr supervises Codex and Claude Code workers | +| `agent.hermes.bstein.dev` | Brad's project coordinator | Keycloak plus an outer oauth2-proxy exact-email allow-list for `brad@bstein.dev` | `hermes` namespace and separate PVC; native Hermes delegates bounded work while Herdr preserves real Codex and Claude Code CLI sessions when needed | | `chat.hermes.bstein.dev` | Private consumer chat and research through Hermes WebUI or a linked Telegram DM | Keycloak login plus one-time Telegram account link | One Hermes process and PVC per assigned Keycloak subject; no Kubernetes RBAC, terminal, or private-service access | The instances do not share conversation state, credentials, profiles, skills diff --git a/testing/tests/test_hermes_auto_router.py b/testing/tests/test_hermes_auto_router.py index a264acaa9..eb85a5fab 100644 --- a/testing/tests/test_hermes_auto_router.py +++ b/testing/tests/test_hermes_auto_router.py @@ -319,6 +319,64 @@ def test_manual_route_skips_internal_reclassification(monkeypatch): ) +def test_every_native_subagent_is_classified_and_routed_independently(monkeypatch): + calls = [] + monkeypatch.setattr(router, "_current_policy", lambda: {"mode": "auto"}) + monkeypatch.setattr(router, "_load_json", lambda path: _status()) + monkeypatch.setattr( + router, + "classify_task", + lambda text: calls.append(text) + or router.Decision( + "implementation", "medium", "codex", "jetson", "test", 9 + ), + ) + applied = [] + recorded = [] + monkeypatch.setattr( + router, "_apply_route", lambda ctx, agent, plan: applied.append((agent, plan)) + ) + monkeypatch.setattr( + router, + "_record_subagent_plan", + lambda policy, plan, goal, index: recorded.append((plan, goal, index)), + ) + + class Parent: + def _emit_status(self, message): + self.message = message + + child = object() + parent = Parent() + router._pre_subagent_route( + object(), + agent=child, + parent_agent=parent, + goal="Implement the bounded parser fix", + context="Run the focused tests.", + task_index=2, + ) + + assert len(calls) == 1 + assert "Run the focused tests" in calls[0] + assert applied[0][0] is child + assert applied[0][1]["profile"] == "codex-medium" + assert applied[0][1]["classifier"] == "jetson-subagent" + assert recorded[0][1:] == ("Implement the bounded parser fix", 2) + assert parent.message.startswith("AUTO child #3") + + +def test_manual_route_leaves_native_subagent_on_parent_override(monkeypatch): + monkeypatch.setattr(router, "_current_policy", lambda: {"mode": "manual"}) + monkeypatch.setattr( + router, + "classify_task", + lambda text: (_ for _ in ()).throw(AssertionError("should not classify")), + ) + + router._pre_subagent_route(object(), agent=object(), goal="Review the diff") + + def test_manual_policy_is_reapplied_on_every_non_command_turn(monkeypatch): monkeypatch.setattr( router, diff --git a/testing/tests/test_hermes_chat_quality.py b/testing/tests/test_hermes_chat_quality.py index db2537b71..d2da21a4f 100644 --- a/testing/tests/test_hermes_chat_quality.py +++ b/testing/tests/test_hermes_chat_quality.py @@ -117,6 +117,8 @@ def test_gateway_image_honors_ui_model_and_caps_reasoning(): assert "specific not in _LEGACY_WEB_BACKENDS" in dockerfile assert '"pre_internal_route"' in dockerfile assert "pre_internal_route hook failed" in dockerfile + assert '"pre_subagent_route"' in dockerfile + assert "pre_subagent_route hook failed" in dockerfile def test_chat_oauth_allows_stale_service_worker_retirement(): diff --git a/testing/tests/test_hermes_herdr.py b/testing/tests/test_hermes_herdr.py index de734fe97..1b0edcbc1 100644 --- a/testing/tests/test_hermes_herdr.py +++ b/testing/tests/test_hermes_herdr.py @@ -54,6 +54,48 @@ def test_herdr_plan_chooses_task_shape_and_caps_effort(): dispatch.select_plan(status, "review", "max") +def test_herdr_auto_classifies_each_cli_worker_with_jetson_router( + tmp_path: Path, +): + router = tmp_path / "auto_router.py" + router.write_text( + """ +class Decision: + shape = "review" + effort = "xhigh" + provider = "claude" + classifier = "jetson" + reason = "bounded local vote" + latency_ms = 17 + +def classify_task(prompt): + assert "security review" in prompt + return Decision() +""", + encoding="utf-8", + ) + status = { + "routes": { + "claude-xhigh": [ + "anthropic/claude-opus-5", + "openai-codex/gpt-5.6-sol", + ] + } + } + + plan = dispatch.select_auto_plan( + status, + "Perform the final security review.", + router, + ) + + assert plan["worker"] == "claude" + assert plan["model"] == "claude-opus-5" + assert plan["effort"] == "xhigh" + assert plan["classifier"] == "jetson" + assert plan["classification_latency_ms"] == 17 + + def test_claude_worker_waits_for_prompt_readiness(tmp_path: Path, monkeypatch): herdr = tmp_path / "herdr" herdr.touch() @@ -647,6 +689,13 @@ def test_agent_coordinator_has_a_long_running_tool_budget(): config = yaml.safe_load(configmap["data"]["config.yaml"]) assert config["agent"]["max_turns"] == 180 + assert config["delegation"] == { + "max_concurrent_children": 4, + "max_iterations": 120, + "max_spawn_depth": 2, + "orchestrator_enabled": True, + "subagent_auto_approve": True, + } assert config["tool_loop_guardrails"]["hard_stop_enabled"] is True for platform in ("cli", "api_server"): tools = config["platform_toolsets"][platform]